# 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 # '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; ($self->config->apxs('-q' => 'INCLUDEDIR'), $self->config->mp_include_dir); } 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); $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}], platform => [qw{CRLF CR LF}], remotehost => [qw{REMOTE_}], satisfy => [qw{SATISFY_}], types => [qw{DIR_MAGIC_TYPE}], }, '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) }, '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 }; 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 %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 = 'xs/tables/current'; 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__