diff --git a/.github/workflows/cygwin.yml b/.github/workflows/cygwin.yml index 8f0333e..2ec845f 100644 --- a/.github/workflows/cygwin.yml +++ b/.github/workflows/cygwin.yml @@ -30,7 +30,7 @@ jobs: git config --global core.eol lf shell: powershell - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Cygwin uses: egor-tensin/setup-cygwin@v3 diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 96ab246..10c27f4 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -17,7 +17,11 @@ jobs: fail-fast: false matrix: cip_tag: - - "5.33" + - "5.41" + - "5.40" + - "5.38" + - "5.36" + - "5.34" - "5.32" - "5.30" - "5.28" @@ -36,7 +40,7 @@ jobs: CIP_TAG: ${{ matrix.cip_tag }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Bootstrap CIP run: | @@ -49,7 +53,7 @@ jobs: cip cache-key - name: Cache CPAN modules - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.cip key: ${{ runner.os }}-build-${{ steps.cache-key.outputs.key }} diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index e96d1ec..49eff36 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -20,7 +20,7 @@ jobs: runs-on: macOS-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Perl run: | diff --git a/.github/workflows/msys2-mingw.yml b/.github/workflows/msys2-mingw.yml index 7fa6f17..5b97827 100644 --- a/.github/workflows/msys2-mingw.yml +++ b/.github/workflows/msys2-mingw.yml @@ -27,7 +27,7 @@ jobs: git config --global core.eol lf shell: powershell - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Perl uses: msys2/setup-msys2@v2 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 503f04a..ed9602a 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -20,7 +20,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Perl run: | diff --git a/MANIFEST b/MANIFEST index bf14ff9..d93dc5f 100644 --- a/MANIFEST +++ b/MANIFEST @@ -1,5 +1,3 @@ -.appveyor.yml -.travis.yml .yath.rc Changes gen_multi_tests.pl diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP index 516c845..a762bc0 100644 --- a/MANIFEST.SKIP +++ b/MANIFEST.SKIP @@ -5,3 +5,4 @@ ^blib/ ^pm_to_blib$ \.swp$ +^\.github diff --git a/lib/PkgConfig.pm b/lib/PkgConfig.pm index 7220b6d..cded3df 100755 --- a/lib/PkgConfig.pm +++ b/lib/PkgConfig.pm @@ -7,16 +7,6 @@ # You may use and distribute this software under the same terms and conditions # as Perl itself. -package - PkgConfig::Vars; -# this is a namespace for .pc files to hold their variables without -# relying on lexical scope. - -package - PkgConfig::UDefs; -# This namespace provides user-defined variables which are to override any -# declarations within the .pc file itself. - package PkgConfig; #First two digits are Perl version, second two are pkg-config version @@ -59,8 +49,6 @@ BEGIN { } } -our $VarClassSerial = 0; - ################################################################################ ### Sane Defaults ### ################################################################################ @@ -475,9 +463,6 @@ struct( 'pkg_description' => '$', 'errmsg' => '$', - # classes used for storing persistent data - 'varclass' => '$', - 'udefclass' => '$', 'filevars' => '*%', 'uservars' => '*%', @@ -499,64 +484,41 @@ struct( ################################################################################ ################################################################################ -sub _get_pc_varname { - my ($self,$vname_base) = @_; - $self->varclass . "::" . $vname_base; -} - -sub _get_pc_udefname { - my ($self,$vname_base) = @_; - $self->udefclass . "::" . $vname_base; -} - -sub _pc_var { - my ($self,$vname) = @_; - $vname =~ s,\.,DOT,g; - no strict 'refs'; - $vname = $self->_get_pc_varname($vname); - no warnings qw(once); - my $glob = *{$vname}; - $glob ? $$glob : (); -} - -sub _quote_cvt($) { - join ' ', map { s/(\s|"|')/\\$1/g; $_ } shellwords(shift) -} - sub assign_var { - my ($self,$field,$value) = @_; - no strict 'refs'; - + my ($self,$field,$value,$force) = @_; # if the user has provided a definition, use that. - if(exists ${$self->udefclass."::"}{$field}) { + if (!$force && exists $self->uservars->{$field}) { log_debug("Prefix already defined by user"); return; } - my $evalstr = sprintf('$%s = PkgConfig::_quote_cvt(%s)', - $self->_get_pc_varname($field), $value); - - log_debug("EVAL", $evalstr); - do { - no warnings 'uninitialized'; - eval $evalstr; - }; - if($@) { - log_err($@); + my $filevars = $self->filevars; + if ($field =~ /(dir|prefix)$/ && $value !~ /\$/) { + # heuristic - it's a directory + $filevars->{$field} = [grep length, $value]; + return; + } + my (@inwords, @outwords) = shellwords $value; # FIRST split it + for my $inword (@inwords) { # THEN sub + my ($word, $outword) = ($inword, ''); + while ($word =~ s/(^|.*?[^\$])\$\{([a-z_.]+)\}//s) { + my ($start, $var) = ($1, $2); + $outword .= $start; + my $val = $filevars->{$var}; + next if !defined($val) or !grep length, @$val; + $outword .= "@$val"; + } + $outword .= $word; # what's left + next if !length $outword; + $outword =~ s/\$\$/\$/g; # pkg-config escapes a '$' with a '$$' + push @outwords, $outword; } + $filevars->{$field} = \@outwords; } sub prepare_vars { my $self = shift; - my $varclass = $self->varclass; - no strict 'refs'; - - %{$varclass . "::"} = (); - - while (my ($name,$glob) = each %{$self->udefclass."::"}) { - my $ref = *$glob{SCALAR}; - next unless defined $ref; - ${"$varclass\::$name"} = $$ref; - } + my $uv = $self->uservars; + $self->assign_var($_, $uv->{$_}, 1) for keys %$uv; } ################################################################################ @@ -587,20 +549,10 @@ sub find { #print "$basekey: " . Dumper($list); } - $VarClassSerial++; - $options{varclass} = sprintf("PkgConfig::Vars::SERIAL_%d", $VarClassSerial); - $options{udefclass} = sprintf("PkgConfig::UDefs::SERIAL_%d", $VarClassSerial); + $options{filevars} = {}; + $options{uservars} = {$options{VARS} ? %{ delete $options{VARS} } : ()}; $options{original} = \%original; - - my $udefs = delete $options{VARS} || {}; - - while (my ($k,$v) = each %$udefs) { - no strict 'refs'; - my $vname = join('::', $options{udefclass}, $k); - ${$vname} = $v; - } - my $o = $cls->new(%options); my @libraries; @@ -649,8 +601,7 @@ sub find { ################################################################################ ################################################################################ sub append_ldflags { - my ($self,@flags) = @_; - my @ld_flags = _split_flags(@flags); + my ($self,@ld_flags) = @_; foreach my $ldflag (@ld_flags) { next unless $ldflag =~ /^-Wl/; @@ -677,7 +628,7 @@ sub append_ldflags { # notify us about extra compiler flags sub append_cflags { my ($self,@flags) = @_; - push @{$self->cflags}, _split_flags(@flags); + push @{$self->cflags}, @flags; } @@ -724,10 +675,8 @@ sub get_requires { @ret; } - sub parse_line { - my ($self,$line,$evals) = @_; - no strict 'vars'; + my ($self,$line) = @_; $line =~ s/#[^#]+$//g; # strip comments return unless $line; @@ -751,37 +700,20 @@ sub parse_line { $field = lc($field); - #perl variables can't have '.' in them: - $field =~ s/\./DOT/g; + my $filevars = $self->filevars; + if ($tok eq ':' and $field !~ /^(cflags|libs)/) { + $value =~ s/(^|[^\$])\$\{([a-z_.]+)\}/$1$filevars->{$2}[0]/g; + $filevars->{$field} = [grep length, $value]; # no need quoting + return; + } #remove quotes from field names $field =~ s/['"]//g; - - # pkg-config escapes a '$' with a '$$'. This won't go in perl: - $value =~ s/[^\\]\$\$/\\\$/g; - $value =~ s/([@%&])/\$1/g; - - - # append our pseudo-package for persistence. - my $varclass = $self->varclass; $value =~ s/(\$\{[^}]+\})/lc($1)/ge; - $value =~ s/\$\{/\$\{$varclass\::/g; - - # preserve quoted space - $value = join ' ', map { s/(["'])/\\$1/g; "'$_'" } shellwords $value - if $value =~ /[\\"']/; - - #quote the value string, unless quoted already - $value = "\"$value\""; - #get existent variables from our hash: - - - #$value =~ s/'/"/g; #allow for interpolation $self->assign_var($field, $value); - } sub parse_pcfile { @@ -798,33 +730,31 @@ sub parse_pcfile { $text =~ s,\\[\r\n],,g; @lines = split(/[\r\n]/, $text); - my @eval_strings; - #Fold lines: my $pcfiledir = dirname $pcfile; $pcfiledir =~ s{\\}{/}g; foreach my $line ("pcfiledir=$pcfiledir", @lines) { - $self->parse_line($line, \@eval_strings); + $self->parse_line($line); } #now that we have eval strings, evaluate them all within the same #lexical scope: - $self->append_cflags( $self->_pc_var('cflags') ); + $self->append_cflags( $self->get_var('cflags') ); if($self->static) { - $self->append_cflags( $self->_pc_var('cflags.private') ); + $self->append_cflags( $self->get_var('cflags.private') ); } - $self->append_ldflags( $self->_pc_var('libs') ); + $self->append_ldflags( $self->get_var('libs') ); if($self->static) { - $self->append_ldflags( $self->_pc_var('libs.private') ); + $self->append_ldflags( $self->get_var('libs.private') ); } my @deps; - my @deps_dynamic = $self->get_requires( $self->_pc_var('requires')); - my @deps_static = $self->get_requires( $self->_pc_var('requires.private') ); + my @deps_dynamic = $self->get_requires( $self->get_var('requires')); + my @deps_static = $self->get_requires( $self->get_var('requires.private') ); @deps = @deps_dynamic; @@ -833,9 +763,9 @@ sub parse_pcfile { } if($self->recursion == 1 && (!$self->pkg_exists())) { - $self->pkg_version( $self->_pc_var('version') ); - $self->pkg_url( $self->_pc_var('url') ); - $self->pkg_description( $self->_pc_var('description') ); + $self->pkg_version( $self->get_var('version') ); + $self->pkg_url( $self->get_var('url') ); + $self->pkg_description( $self->get_var('description') ); $self->pkg_exists(1); } @@ -897,8 +827,15 @@ sub find_pcfile { ################################################################################ ################################################################################ -sub _return_context (@) { - wantarray ? (@_) : join(' ', map { s/(\s|['"])/\\$1/g; $_ } @_) +sub _quote_protect { + my ($v) = @_; + return qq{""} if !length($v); # empty strings get preserved + return $v if $v !~ /['"\s\\]/; + $v =~ s/["\\]/\\$&/g; # the "" will already protect from ' and space + $v =~ /['\s]/ ? qq{"$v"} : $v; +} +sub _return_context { + wantarray ? @_ : join ' ', map _quote_protect($_), @_; } sub get_cflags { @@ -932,7 +869,7 @@ sub get_ldflags { sub get_var { my($self, $name) = @_; - $self->_pc_var($name); + _return_context @{ $self->filevars->{$name} }; } sub get_list { @@ -945,7 +882,7 @@ sub get_list { for my $pc (bsd_glob("$d/*.pc")) { if ($pc =~ m|/([^\\\/]+)\.pc$|) { $self->parse_pcfile($pc); - push @rv, [$1, $self->_pc_var('name') . ' - ' . $self->_pc_var('description')]; + push @rv, [$1, $self->get_var('name') . ' - ' . $self->get_var('description')]; } } } @@ -968,7 +905,6 @@ sub _split_flags { if(@flags == 1) { my $str = shift @flags; return () if !$str; - #@flags = map { s/\\(\s)/$1/g; $_ } split(/(?print_variables) { } if($OutputVariableValue) { - my $val = ($o->_pc_var($OutputVariableValue) or ""); + my $val = $o->get_var($OutputVariableValue) || ""; print $val . "\n"; } diff --git a/t/01-script_detailed.t b/t/01-script_detailed.t index db7f4c5..d37d4c7 100644 --- a/t/01-script_detailed.t +++ b/t/01-script_detailed.t @@ -1,4 +1,3 @@ -#!perl use strict; use warnings; use Test::More; @@ -8,15 +7,39 @@ use PkgConfigTest; run_common("glib-2.0"); ok($RV == 0, "package name exists"); -run_common("--exists glib-2.0"); ok($RV == 0, "package name (--exists)"); +run_common(qw(--exists glib-2.0)); ok($RV == 0, "package name (--exists)"); -run_common("--libs glib-2.0"); like($S, qr/-lglib-2\.0/, "Got expected libs"); +run_common(qw(--libs glib-2.0)); like($S, qr/-lglib-2\.0/, "Got expected libs"); ok($S !~ /-L/, "No -L directive for standard search path"); - - -run_common("--cflags glib-2.0"); +run_common(qw(--cflags glib-2.0)); expect_flags("-I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include", "Got expected include flags"); +if (eval { symlink("",""); 1 }) { + # symlink to simulate place-with-space + require File::Temp; + require File::Spec; + require Text::ParseWords; + my $dir = File::Temp::tempdir( CLEANUP => 1 ); + my $sub = File::Spec->catdir($dir, 'in space'); + my $exp_stub = "-I".File::Spec->rel2abs($sub)."/../../include"; + $exp_stub =~ s|\\|/|g; # standard behaviour of this module + symlink File::Spec->rel2abs(File::Spec->catdir(qw(t data strawberry c lib pkgconfig))), $sub; + local $ENV{PKG_CONFIG_PATH} = $sub; + require PkgConfig; # after the environment variable is set + for (['freetype2','/freetype2'], ['gsl',''], ['libxml-2.0','/libxml2'], ['libexslt',['','/libxml2']]) { + my ($lib, $suffix) = @$_; + run_common(qw(--cflags), $lib); + chomp(my $out = $PkgConfigTest::S); + like $out, qr/^"/, "$lib cflags should be quote-protected to survive make"; + ($out) = Text::ParseWords::shellwords($out); + is $out, $exp_stub.(ref $suffix ? $suffix->[0] : $suffix), "$lib survived being in space"; + my $pkg = PkgConfig->find($lib); + my $arr = [$pkg->get_cflags]; + my $exp = ref $suffix ? [map "$exp_stub$_", @$suffix] : ["$exp_stub$suffix"]; + is_deeply $arr, $exp, "$lib get_cflags" or diag explain [$arr,$exp]; + } +} + done_testing(); diff --git a/t/PkgConfigTest.pm b/t/PkgConfigTest.pm index 91e24b2..4f31c83 100644 --- a/t/PkgConfigTest.pm +++ b/t/PkgConfigTest.pm @@ -47,11 +47,11 @@ $SCRIPT = $FindBin::Bin . "/../lib/PkgConfig.pm" sub run_common { my @args = @_; - my $pkg_config = join ' ', - map { /\s/ ? "\"$_\"" : $_ } - ($^X, $SCRIPT); - (my $ret = qx($pkg_config --env-only @args)) - =~ s/(?:^\s+)|($?:\s+$)//g; + unshift @args, $^X, $SCRIPT, '--env-only'; + open my $fh, "-|", @args or die "open @args: $!"; + local $/; + (my $ret = <$fh>) =~ s/(?:^\s+)|($?:\s+$)//g; + close $fh; $RV = $?; $S = $ret; } @@ -67,8 +67,8 @@ sub run_exists_test { foreach my $fname (@$flist) { next unless -f $fname; my ($base) = fileparse($fname, ".pc"); - run_common("$base"); - ok($RV == 0, "Package $base exists"); + run_common($base); + is $RV, 0, "Package $base exists"; } } @@ -76,8 +76,8 @@ sub _single_flags_test { my $fname = shift; return unless -f $fname; my ($base) = fileparse($fname, ".pc"); - run_common("--libs --cflags $base --define-variable=prefix=blah"); - ok($RV == 0, "Got OK for --libs and --cflags"); + run_common(qw(--libs --cflags), $base, qw(--define-variable=prefix=blah)); + is $RV, 0, "Got OK for '$fname' --libs and --cflags"; if($S =~ /-(?:L|I)/) { if($S !~ /blah/) { @@ -97,13 +97,13 @@ sub _single_flags_test { my @lines = <$fh>; if(grep /\$\{prefix\}/, @lines) { - ok(0, "Expected substituted prefix for $base"); + fail "Expected substituted prefix for $base"; } else { note "File $fname has no \${prefix} directive"; } return; } - ok($S =~ /blah/, "Found modified prefix for $base"); + like $S, qr/blah/, "Found modified prefix for $base"; } } diff --git a/t/pcfiledir.t b/t/pcfiledir.t index 9ffec18..2acc35a 100644 --- a/t/pcfiledir.t +++ b/t/pcfiledir.t @@ -16,5 +16,5 @@ is $pkg->errmsg, undef, 'no error'; isa_ok $pkg, 'PkgConfig'; -my $prefix = $pkg->get_var('prefix'); +my ($prefix) = $pkg->get_var('prefix'); # list in case in place with space! is $prefix, "$ENV{PKG_CONFIG_PATH}/../..", "prefix=$prefix"; diff --git a/t/quote.t b/t/quote.t index 6f1a029..331f10b 100644 --- a/t/quote.t +++ b/t/quote.t @@ -5,21 +5,21 @@ use FindBin (); use File::Spec; use Test::More tests => 6; -my $path = File::Spec->catfile($FindBin::Bin, 'data', 'quote'); +my $path = File::Spec->catdir($FindBin::Bin, 'data', 'quote'); foreach my $type (qw( doublequote singlequote backslash quotevar )) { subtest $type => sub { my $pkg = PkgConfig->find($type, - search_path => [File::Spec->catfile($FindBin::Bin, 'data', 'quote')], + search_path => [File::Spec->catdir($FindBin::Bin, 'data', 'quote')], ); isa_ok $pkg, 'PkgConfig'; is $pkg->errmsg, undef, 'no error'; - is_deeply [$pkg->get_cflags], ['-I/foo/include', '-DFOO=bar baz'], 'list context'; - is scalar $pkg->get_cflags, '-I/foo/include -DFOO=bar\\ baz', 'scalar context'; + is_deeply [$pkg->get_cflags], ['-I/foo/include', '-DFOO=bar baz'], "$type list context"; + is scalar $pkg->get_cflags, '-I/foo/include "-DFOO=bar baz"', "$type scalar context"; #note $_ for $pkg->get_cflags; done_testing; }; @@ -27,7 +27,7 @@ foreach my $type (qw( doublequote singlequote backslash quotevar )) subtest 'noquote' => sub { my $pkg = PkgConfig->find('noquote', - search_path => [File::Spec->catfile($FindBin::Bin, 'data', 'quote')], + search_path => [File::Spec->catdir($FindBin::Bin, 'data', 'quote')], ); isa_ok $pkg, 'PkgConfig'; @@ -40,7 +40,7 @@ subtest 'noquote' => sub { subtest 'escape' => sub { my $pkg = PkgConfig->find('escape', - search_path => [File::Spec->catfile($FindBin::Bin, 'data', 'quote')], + search_path => [File::Spec->catdir($FindBin::Bin, 'data', 'quote')], ); isa_ok $pkg, 'PkgConfig'; diff --git a/t/strawberry.t b/t/strawberry.t index ac597f1..ecc1a01 100644 --- a/t/strawberry.t +++ b/t/strawberry.t @@ -124,8 +124,8 @@ subtest 'pcfiles included' => sub { note $dir; } } - - ok $ok1, "good directory included"; + $dir ||= ''; + ok $ok1, "good directory included, found '$dir'"; }; }