blob: 7dada948ae57a04412a9e13155a2c425104d139e [file] [log] [blame]
# please insert nothing before this line: -*- mode: cperl; cperl-indent-level: 4; cperl-continued-statement-offset: 4; indent-tabs-mode: nil -*-
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
package Apache2::ParseSource;
use strict;
use warnings FATAL => 'all';
use Apache2::Build ();
use Config;
use File::Basename;
use File::Spec::Functions qw(catdir);
our $VERSION = '0.02';
sub new {
my $class = shift;
my $self = bless {
config => Apache2::Build->build_config,
@_,
}, $class;
my $prefixes = join '|', @{ $self->{prefixes} || [qw(ap_ apr_)] };
$self->{prefix_re} = qr{^($prefixes)};
$Apache2::Build::APXS ||= $self->{apxs};
$self;
}
sub config {
shift->{config};
}
sub parse {
my $self = shift;
$self->{scan_filename} = $self->generate_cscan_file;
$self->{c} = $self->scan;
}
sub DESTROY {
my $self = shift;
unlink $self->{scan_filename}
}
{
package Apache2::ParseSource::Scan;
our @ISA = qw(ModPerl::CScan);
sub get {
local $SIG{__DIE__} = \&Carp::confess;
shift->SUPER::get(@_);
}
}
my @c_scan_defines = (
'CORE_PRIVATE', #so we get all of apache
'MP_SOURCE_SCAN', #so we can avoid some c-scan barfing
'_NETINET_TCP_H', #c-scan chokes on netinet/tcp.h
'_BYTESWAP_H', #c-scan chokes on byteswap.h
'_BITS_BYTESWAP_H', #c-scan chokes on byteswap.h
'Expat_INCLUDED', #c-scan chokes on expath.h
# 'APR_OPTIONAL_H', #c-scan chokes on apr_optional.h
'apr_table_do_callback_fn_t=void', #c-scan chokes on function pointers
);
# some types c-scan failing to resolve
push @c_scan_defines, map { "$_=void" }
qw(PPADDR_t PerlExitListEntry modperl_tipool_vtbl_t);
sub scan {
require ModPerl::CScan;
ModPerl::CScan->VERSION(0.75);
require Carp;
my $self = shift;
my $c = ModPerl::CScan->new(filename => $self->{scan_filename});
my $includes = $self->includes;
# where to find perl headers, but we don't want to parse them otherwise
my $perl_core_path = catdir $Config{installarchlib}, "CORE";
push @$includes, $perl_core_path;
$c->set(includeDirs => $includes);
my @defines = @c_scan_defines;
unless ($Config{useithreads} and $Config{useithreads} eq 'define') {
#fake -DITHREADS so function tables are the same for
#vanilla and ithread perls, that is,
#make sure THX and friends are always expanded
push @defines, 'MP_SOURCE_SCAN_NEED_ITHREADS';
}
$c->set(Defines => join ' ', map "-D$_", @defines);
bless $c, 'Apache2::ParseSource::Scan';
}
sub include_dirs {
my $self = shift;
my $inc = $self->config->apxs('-q' => 'INCLUDEDIR');
my @dirs = ($inc, $self->config->mp_include_dir);
my $aprinc = $self->config->apxs('-q' => 'APR_INCLUDEDIR');
unless ($aprinc eq $inc) {
# Add APR include directory if different to httpd includedir
push @dirs, $aprinc;
}
@dirs;
}
sub includes { shift->config->includes }
sub find_includes {
my $self = shift;
return $self->{includes} if $self->{includes};
require File::Find;
my @includes = ();
# don't pick preinstalled mod_perl headers if any, but pick the rest
{
my @dirs = $self->include_dirs;
die "could not find include directory (build the project first)"
unless -d $dirs[0];
my $unwanted = join '|', qw(ap_listen internal version
apr_optional mod_include mod_cgi
mod_proxy mod_ssl ssl_ apr_anylock
apr_rmm ap_config mod_log_config
mod_perl modperl_ apreq mod_cache
mod_serf mod_dav);
$unwanted = qr|^$unwanted|;
my $wanted = '';
push @includes, find_includes_wanted($wanted, $unwanted, @dirs);
}
# now add the live mod_perl headers (to make sure that we always
# work against the latest source)
{
my @dirs = map { catdir $self->config->{cwd}, $_ }
catdir(qw(src modules perl)), 'xs';
my $unwanted = '';
my $wanted = join '|', qw(mod_perl modperl_);
$wanted = qr|^$wanted|;
push @includes, find_includes_wanted($wanted, $unwanted, @dirs);
}
# now reorg the header files list, so the fragile scan won't choke
my @apr = ();
my @mp = ();
my @rest = ();
for (@includes) {
if (/mod_perl.h$/) {
# mod_perl.h needs to be included before other mod_perl
# headers
unshift @mp, $_;
}
elsif (/modperl_\w+.h$/) {
push @mp, $_;
}
elsif (/apr_\w+\.h$/ ) {
# apr headers need to be included first
push @apr, $_;
}
else {
push @rest, $_;
}
}
@includes = (@apr, @rest, @mp);
return $self->{includes} = \@includes;
}
sub find_includes_wanted {
my ($wanted, $unwanted, @dirs) = @_;
my @includes = ();
for my $dir (@dirs) {
File::Find::finddepth({
wanted => sub {
return unless /\.h$/;
if ($wanted) {
return unless /$wanted/;
}
else {
return if /$unwanted/;
}
my $dir = $File::Find::dir;
push @includes, "$dir/$_";
},
(Apache2::Build::WIN32 ? '' : follow => 1),
}, $dir);
}
return @includes;
}
sub generate_cscan_file {
my $self = shift;
my $includes = $self->find_includes;
my $filename = '.apache_includes';
open my $fh, '>', $filename or die "can't open $filename: $!";
for my $path (@$includes) {
my $filename = basename $path;
print $fh qq(\#include "$path"\n);
}
close $fh;
return $filename;
}
my %defines_wanted = (
'Apache2::Const' => {
common => [qw{OK DECLINED DONE}],
config => [qw{DECLINE_CMD}],
context => [qw(NOT_IN_ GLOBAL_ONLY)],
http => [qw{HTTP_}],
log => [qw(APLOG_)],
methods => [qw{M_ METHODS}],
mpmq => [qw{AP_MPMQ_}],
options => [qw{OPT_}],
override => [qw{OR_ EXEC_ON_READ ACCESS_CONF RSRC_CONF}],
proxy => [qw{PROXYREQ_}],
platform => [qw{CRLF CR LF}],
remotehost => [qw{REMOTE_}],
satisfy => [qw{SATISFY_}],
types => [qw{DIR_MAGIC_TYPE}],
auth => [qw{AUTHN_ AUTHZ AP_AUTH_ AUTH_ AUTHZ_}],
},
'APR::Const' => {
common => [qw{APR_SUCCESS}],
error => [qw{APR_E}],
filepath => [qw{APR_FILEPATH_}],
filetype => [qw{APR_FILETYPE_}],
fopen => [qw{APR_FOPEN_}],
fprot => [qw{APR_FPROT_}],
finfo => [qw{APR_FINFO_}],
flock => [qw{APR_FLOCK_}],
hook => [qw{APR_HOOK_}],
limit => [qw{APR_LIMIT}],
poll => [qw{APR_POLL}],
socket => [qw{APR_SO_}],
status => [qw{APR_TIMEUP}],
table => [qw{APR_OVERLAP_TABLES_}],
uri => [qw{APR_URI_}],
},
ModPerl => {
common => [qw{MODPERL_RC_}],
}
);
my %defines_wanted_re;
while (my ($class, $groups) = each %defines_wanted) {
while (my ($group, $wanted) = each %$groups) {
my $pat = join '|', @$wanted;
$defines_wanted_re{$class}->{$group} = $pat; #qr{^($pat)};
}
}
my %enums_wanted = (
'Apache2::Const' => { map { $_, 1 } qw(cmd_how input_mode filter_type conn_keepalive authn_status authz_status) },
'APR::Const' => { map { $_, 1 } qw(apr_shutdown_how apr_read_type apr_lockmech) },
);
my $defines_unwanted = join '|', qw{
HTTP_VERSION APR_EOL_STR APLOG_MARK APLOG_NOERRNO APR_SO_TIMEOUT
APR_HOOK_PROBES_ENABLED APR_HOOK_INT_DCL_UD
APLOG_MAX_LOGLEVEL
APR_BEGIN_DECLS APR_END_DECLS
};
sub get_constants {
my ($self) = @_;
my $includes = $self->find_includes;
my (%constants, %seen);
for my $file (@$includes) {
open my $fh, $file or die "open $file: $!";
while (<$fh>) {
if (s/^\#define\s+(\w+)\s+.*/$1/) {
chomp;
next if /_H$/;
next if $seen{$_}++;
$self->handle_constant(\%constants);
}
elsif (m/enum[^\{]+\{/) {
$self->handle_enum($fh, \%constants);
}
}
close $fh;
}
#maintain a few handy shortcuts from 1.xx
#aliases are defined in ModPerl::Code
push @{ $constants{'Apache2::Const'}->{common} },
qw(NOT_FOUND FORBIDDEN AUTH_REQUIRED SERVER_ERROR REDIRECT);
return \%constants;
}
sub handle_constant {
my ($self, $constants) = @_;
my $keys = keys %defines_wanted_re;
return if /^($defines_unwanted)/o;
while (my ($class, $groups) = each %defines_wanted_re) {
my $keys = keys %$groups;
while (my ($group, $re) = each %$groups) {
next unless /^($re)/;
push @{ $constants->{$class}->{$group} }, $_;
return;
}
}
}
sub handle_enum {
my ($self, $fh, $constants) = @_;
my ($name, $e) = $self->parse_enum($fh);
return unless $name;
$name =~ s/^ap_//;
$name =~ s/_(e|t)$//;
my $class;
for (keys %enums_wanted) {
next unless $enums_wanted{$_}->{$name};
$class = $_;
}
return unless $class;
$name =~ s/^apr_//;
push @{ $constants->{$class}->{$name} }, @$e if $e;
}
#this should win an award for worlds lamest parser
sub parse_enum {
my ($self, $fh) = @_;
my $code = $_;
my @e;
unless ($code =~ /;\s*$/) {
local $_;
while (<$fh>) {
$code .= $_;
last if /;\s*$/;
}
}
my $name;
if ($code =~ s/^\s*enum\s+(\w*)\s*//) {
$name = $1;
}
elsif ($code =~ s/^\s*typedef\s+enum\s+//) {
$code =~ s/\s*(\w+)\s*;\s*$//;
$name = $1;
}
$code =~ s:/\*.*?\*/::sg;
$code =~ s/\s*=\s*\w+//g;
$code =~ s/^[^\{]*\{//s;
$code =~ s/\}[^;]*;?//s;
$code =~ s/^\s*\n//gm;
while ($code =~ /\b(\w+)\b,?/g) {
push @e, $1;
}
return ($name, \@e);
}
sub wanted_functions { shift->{prefix_re} }
sub wanted_structures { shift->{prefix_re} }
sub get_functions {
my $self = shift;
my $key = 'parsed_fdecls';
return $self->{$key} if $self->{$key};
my $c = $self->{c};
my $fdecls = $c->get($key);
my $inlines = $c->get('parsed_inlines');
push @{$fdecls}, @{$inlines};
my %seen;
my $wanted = $self->wanted_functions;
my @functions;
for my $entry (@$fdecls) {
my ($rtype, $name, $args) = @$entry;
next unless $name =~ $wanted;
next if $seen{$name}++;
my @attr;
for (qw(static __inline__)) {
if ($rtype =~ s/^($_)\s+//) {
push @attr, $1;
}
}
#XXX: working around ModPerl::CScan confusion here
#macro defines ap_run_error_log causes
#cpp filename:linenumber to be included as part of the type
for (@$args) {
next unless $_->[0];
$_->[0] =~ s/^\#.*?\"\s+//;
$_->[0] =~ s/^register //;
}
my $func = {
name => $name,
return_type => $rtype,
args => [map {
{ type => $_->[0], name => $_->[1] }
} @$args],
};
$func->{attr} = \@attr if @attr;
push @functions, $func;
}
# sort the functions by the 'name' attribute to ensure a
# consistent output on different systems.
$self->{$key} = [sort { $a->{name} cmp $b->{name} } @functions];
}
sub get_structs {
my $self = shift;
my $key = 'typedef_structs';
return $self->{$key} if $self->{$key};
my $c = $self->{c};
my $typedef_structs = $c->get($key);
my %seen;
my $wanted = $self->wanted_structures;
my $other = join '|', qw(_rec module
piped_log uri_t htaccess_result
cmd_parms cmd_func cmd_how);
my @structures;
my $sx = qr(^struct\s+);
while (my ($type, $elts) = each %$typedef_structs) {
next unless $type =~ $wanted or $type =~ /($other)$/o;
$type =~ s/$sx//;
next if $seen{$type}++;
my $struct = {
type => $type,
elts => [map {
my $type = $_->[0];
$type =~ s/$sx//;
$type .= $_->[1] if $_->[1];
$type =~ s/:\d+$//; #unsigned:1
{ type => $type, name => $_->[2] }
} @$elts],
};
push @structures, $struct;
}
# sort the structs by the 'type' attribute to ensure a consistent
# output on different systems.
$self->{$key} = [sort { $a->{type} cmp $b->{type} } @structures];
}
sub write_functions_pm {
my $self = shift;
my $file = shift || 'FunctionTable.pm';
my $name = shift || 'Apache2::FunctionTable';
$self->write_pm($file, $name, $self->get_functions);
}
sub write_structs_pm {
my $self = shift;
my $file = shift || 'StructureTable.pm';
my $name = shift || 'Apache2::StructureTable';
$self->write_pm($file, $name, $self->get_structs);
}
sub write_constants_pm {
my $self = shift;
my $file = shift || 'ConstantsTable.pm';
my $name = shift || 'Apache2::ConstantsTable';
$self->write_pm($file, $name, $self->get_constants);
}
sub write_pm {
my ($self, $file, $name, $data) = @_;
require Data::Dumper;
local $Data::Dumper::Indent = 1;
my ($subdir) = (split '::', $name)[0];
my $tdir = '';
my $build = Apache2::Build->new(init => 1);
my $httpd_version = $build->httpd_version;
if ($httpd_version lt '2.4.0') {
$tdir='xs/tables/current';
}
else {
$tdir='xs/tables/current24';
}
if (-d "$tdir/$subdir") {
$file = "$tdir/$subdir/$file";
}
# sort the hashes (including nested ones) for a consistent dump
canonsort(\$data);
my $dump = Data::Dumper->new([$data],
[$name])->Dump;
my $package = ref($self) || $self;
my $version = $self->VERSION;
my $date = scalar localtime;
my $new_content = << "EOF";
package $name;
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# ! WARNING: generated by $package/$version
# ! $date
# ! do NOT edit, any changes will be lost !
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
$dump
1;
EOF
my $old_content = '';
if (-e $file) {
open my $pm, '<', $file or die "open $file: $!";
local $/ = undef; # slurp the file
$old_content = <$pm>;
close $pm;
}
my $overwrite = 1;
if ($old_content) {
# strip the date line, which will never be the same before
# comparing
my $table_header = qr{^\#\s!.*};
(my $old = $old_content) =~ s/$table_header//mg;
(my $new = $new_content) =~ s/$table_header//mg;
$overwrite = 0 if $old eq $new;
}
if ($overwrite) {
open my $pm, '>', $file or die "open $file: $!";
print $pm $new_content;
close $pm;
}
}
# canonsort(\$data);
# sort nested hashes in the data structure.
# the data structure itself gets modified
sub canonsort {
my $ref = shift;
my $type = ref $$ref;
return unless $type;
require Tie::IxHash;
my $data = $$ref;
if ($type eq 'ARRAY') {
for (@$data) {
canonsort(\$_);
}
}
elsif ($type eq 'HASH') {
for (keys %$data) {
canonsort(\$data->{$_});
}
tie my %ixhash, 'Tie::IxHash';
# reverse sort so we get the order of:
# return_type, name, args { type, name } for functions
# type, elts { type, name } for structures
for (sort { $b cmp $a } keys %$data) {
$ixhash{$_} = $data->{$_};
}
$$ref = \%ixhash;
}
}
1;
__END__