PAX

 view release on metacpan or  search on metacpan

lib/PAX/StandaloneImage.pm  view on Meta::CPAN

        lib_dirs => $lib_dirs,
        source_roots => $source_roots,
        assets => [
            map { $_->{source_path} }
                grep { defined($_->{source_path}) && $_->{source_path} ne '' && -f $_->{source_path} }
                @{ $manifest->{assets} // [] }
        ],
        asset_dirs => (($manifest->{asset_count} // 0) > 0 ? [ File::Spec->catdir($extract_root, 'assets') ] : []),
        cpanfiles => [],
        runtime_mode => $manifest->{runtime}{mode},
        app_name => $manifest->{app}{name},
        app_namespace => $manifest->{app}{namespace},
        app_legacy_namespace => $manifest->{app}{compat}{legacy_namespace},
        app_entrypoint_env => $manifest->{app}{entrypoint_env},
        app_entrypoint_fallback => $manifest->{app}{entrypoint_fallback},
        app_command => $manifest->{app}{command},
    };
}

# Detect executable Perl scripts that should be compiled as scripts rather than
# treated as already-built standalone binaries.
sub _looks_like_plain_script_entrypoint {
    my ($entrypoint) = @_;
    open my $fh, '<:raw', $entrypoint or return 0;
    read($fh, my $prefix, 128);
    close $fh;
    return $prefix =~ /\A#!/ ? 1 : 0;
}

sub _standalone_inspect_json {
    my ($entrypoint) = @_;
    my $pid = open my $fh, '-|';
    return '' if !defined $pid;
    if (!$pid) {
        open STDERR, '>', File::Spec->devnull or die "cannot open devnull: $!";
        exec {$entrypoint} $entrypoint, '--pax-standalone-inspect';
        exit 127;
    }
    local $/;
    my $json = <$fh> // '';
    close $fh;
    return ($? >> 8) == 0 ? $json : '';
}

sub _standalone_extract_quietly {
    my ($entrypoint, $extract_root) = @_;
    my $pid = fork();
    return 0 if !defined $pid;
    if (!$pid) {
        open STDOUT, '>', File::Spec->devnull or die "cannot open devnull: $!";
        open STDERR, '>', File::Spec->devnull or die "cannot open devnull: $!";
        exec {$entrypoint} $entrypoint, '--pax-standalone-extract', $extract_root;
        exit 127;
    }
    waitpid($pid, 0);
    return ($? >> 8) == 0 ? 1 : 0;
}

sub _extract_payload_path {
    my ($root, $prefix, $logical_path) = @_;
    my @parts = grep { defined && $_ ne '' } split m{/+}, ($logical_path // '');
    return File::Spec->catfile($root, $prefix, @parts);
}

sub _extracted_manifest_path {
    my ($root, $prefix, $logical_path) = @_;
    my $path = _extract_payload_path($root, $prefix, $logical_path);
    return '' if !defined $path || $path eq '' || !-f $path;
    return $path;
}

sub _materialize_entrypoint_source {
    my ($extract_root, $entrypoint) = @_;
    my $bytes = $entrypoint->{source_bytes} // '';
    return '' if $bytes eq '';

    my $name = _logical_name($entrypoint->{source_path} || $entrypoint->{logical_path} || 'entrypoint.pl');
    $name =~ s/\.(?:script|dispatch|cli-router|service)\.json\z/.pl/;
    $name .= '.pl' if $name !~ /\.[A-Za-z0-9]+\z/;

    my $dir = File::Spec->catdir($extract_root, 'source-entrypoint');
    mkdir $dir if !-d $dir;
    my $path = File::Spec->catfile($dir, $name);
    open my $fh, '>:raw', $path or return '';
    print {$fh} $bytes;
    close $fh or return '';
    return $path;
}

sub _materialize_manifest_source_tree {
    my ($extract_root, $manifest) = @_;
    my @source_items = grep {
        my $kind = $_->{unit_kind} // '';
        my $bytes = $_->{source_bytes} // '';
        ($kind eq 'lib' || $kind eq 'source' || $kind eq 'entrypoint')
            && $bytes ne ''
            && ($_->{source_path} // '') ne '';
    } @{ $manifest->{code_units} // [] };
    my $entry = $manifest->{entrypoint} // {};
    push @source_items, {
        unit_kind => 'entrypoint',
        source_path => $entry->{source_path},
        source_bytes => $entry->{source_bytes},
    } if ($entry->{source_path} // '') ne '' && ($entry->{source_bytes} // '') ne '';
    return {} if !@source_items;

    my $root = _common_source_parent(map { $_->{source_path} } @source_items);
    return {} if $root eq '';

    my $rebuild_root = File::Spec->catdir($extract_root, 'rebuild-source');
    make_path($rebuild_root) if !-d $rebuild_root;

    my %written;
    my $materialized_entrypoint = '';
    for my $item (@source_items) {
        my $source_path = $item->{source_path} // next;
        my $bytes = $item->{source_bytes} // '';
        next if $bytes eq '';
        my $rel = File::Spec->abs2rel($source_path, $root);
        next if !defined $rel || $rel eq '' || $rel =~ /^\.\.(?:\/|\\|$)/;
        my $dest = File::Spec->catfile($rebuild_root, split m{/+|\\+}, $rel);
        next if $written{$dest}++;
        my ($vol, $dirs) = File::Spec->splitpath($dest);
        make_path($dirs) if $dirs ne '' && !-d $dirs;
        open my $fh, '>:raw', $dest or return {};
        print {$fh} $bytes;
        close $fh or return {};
        if (($item->{unit_kind} // '') eq 'entrypoint') {
            $materialized_entrypoint = $dest;
        }
    }
    return {} if $materialized_entrypoint eq '' || !-f $materialized_entrypoint;

    return {
        entrypoint => $materialized_entrypoint,
        lib_dirs => _materialized_manifest_roots($manifest, 'lib_dirs', 'lib', $root, $rebuild_root),
        source_roots => _materialized_manifest_roots($manifest, 'source_roots', 'source', $root, $rebuild_root),
    };
}

sub _materialized_manifest_roots {
    my ($manifest, $field, $unit_kind, $source_root, $rebuild_root) = @_;
    my @roots;
    my %seen;
    for my $logical_root (@{ $manifest->{$field} // [] }) {
        next if !defined $logical_root || $logical_root eq '';
        my $original_root = _manifest_source_root_for_logical($manifest, $logical_root, $unit_kind);
        next if !defined $original_root || $original_root eq '';
        my $rel = File::Spec->abs2rel($original_root, $source_root);
        next if !defined $rel || $rel eq '' || $rel =~ /^\.\.(?:\/|\\|$)/;
        my $materialized = File::Spec->catdir($rebuild_root, split m{/+|\\+}, $rel);
        next if !-d $materialized || $seen{$materialized}++;
        push @roots, $materialized;
    }
    return \@roots;
}

sub _common_source_parent {
    my @paths = grep { defined && $_ ne '' } @_;
    return '' if !@paths;
    my @common = File::Spec->splitdir(dirname(shift @paths));
    for my $path (@paths) {
        my @parts = File::Spec->splitdir(dirname($path));
        my $limit = @common < @parts ? scalar(@common) : scalar(@parts);
        my $i = 0;
        $i++ while $i < $limit && $common[$i] eq $parts[$i];
        splice @common, $i;
        last if !@common;
    }
    return File::Spec->catdir(@common);
}

sub _extracted_manifest_roots {
    my ($root, $prefix, $logical_roots) = @_;
    my @roots;
    my %seen;
    for my $logical_root (@{ $logical_roots // [] }) {
        next if !defined $logical_root || $logical_root eq '';
        my @parts = grep { defined && $_ ne '' } split m{/+}, $logical_root;
        my $path = File::Spec->catdir($root, $prefix, @parts);
        next if !-d $path || $seen{$path}++;
        push @roots, $path;
    }
    return \@roots;
}

sub _original_manifest_roots {
    my ($manifest, $field, $unit_kind) = @_;
    my @roots;
    my %seen;
    for my $logical_root (@{ $manifest->{$field} // [] }) {
        next if !defined $logical_root || $logical_root eq '';
        my $root = _original_source_root_for_logical($manifest, $logical_root, $unit_kind);
        next if !defined $root || $root eq '' || !-d $root || $seen{$root}++;
        push @roots, $root;
    }
    return \@roots;
}

sub _original_source_root_for_logical {
    my ($manifest, $logical_root, $unit_kind) = @_;
    my $root = _manifest_source_root_for_logical($manifest, $logical_root, $unit_kind);
    return if !defined $root || $root eq '' || !-d $root;
    return $root;
}

sub _manifest_source_root_for_logical {
    my ($manifest, $logical_root, $unit_kind) = @_;
    for my $unit (@{ $manifest->{code_units} // [] }) {
        my $logical_path = $unit->{logical_path} // '';
        my $source_path = $unit->{source_path} // '';
        my $kind = $unit->{unit_kind} // '';
        next if $logical_path eq '' || $source_path eq '';
        next if defined $unit_kind && $unit_kind ne '' && $kind ne $unit_kind;
        next if index($logical_path, $logical_root . '/') != 0;
        my $rel = substr($logical_path, length($logical_root) + 1);
        next if $rel eq '';
        my @rel_parts = split m{/+}, $rel;
        my @source_parts = File::Spec->splitdir(dirname($source_path));
        splice @source_parts, -(@rel_parts - 1) if @rel_parts > 1;
        my $root = File::Spec->catdir(@source_parts);
        return $root if $root ne '';
    }
    return;
}

sub load {
    my ($self, %args) = @_;
    my $name = $args{name} // die 'name required';
    my $path = File::Spec->catfile($self->{root}, $name, 'manifest.json');
    open my $fh, '<', $path or die "cannot read standalone image $path: $!";
    local $/;
    return JSON::PP::decode_json(<$fh>);
}

sub path_for {
    my ($self, $name) = @_;
    return File::Spec->catfile($self->{root}, $name, 'manifest.json');
}

sub _default_name {
    my ($entrypoint) = @_;
    my ($vol, $dir, $file) = File::Spec->splitpath($entrypoint);
    $file =~ s/\.[^.]+\z//;
    $file =~ s/[^A-Za-z0-9_.-]+/-/g;
    return $file || 'pax-standalone';
}

sub _app_metadata {
    my (%args) = @_;
    my $entrypoint = $args{entrypoint} // '';
    my $image_name = $args{image_name} // 'pax-standalone';
    my $app_name = $args{app_name} // $image_name;
    my $command_fallback = _entrypoint_default_command($entrypoint);
    my $command = $args{app_command} // $command_fallback;
    my $entrypoint_env = $args{app_entrypoint_env};
    my $entrypoint_fallback = $args{app_entrypoint_fallback} // $command_fallback;
    my $legacy_namespace = _normalize_namespace($args{app_legacy_namespace});
    my $compat_namespace = _normalize_namespace($args{app_namespace});
    $legacy_namespace = $compat_namespace if $legacy_namespace eq '';

    return {
        name => $app_name,
        namespace => $compat_namespace,
        compat => {
            namespace => $compat_namespace,
            legacy_namespace => $legacy_namespace,
        },
        command => $command,
        entrypoint_env => $entrypoint_env // '',
        entrypoint_fallback => $entrypoint_fallback,
        entrypoint_command => $command,
    };
}

sub _safe_dir_abs {
    my ($path) = @_;
    return '' if !defined $path || $path eq '';

lib/PAX/StandaloneImage.pm  view on Meta::CPAN

    return @roots;
}

sub _pax_runtime_helper_modules {
    my @helpers = qw(
        PAX/StandaloneRuntime.pm
        PAX/NativeRunner.pm
        PAX/GuardManager.pm
        PAX/DeoptEngine.pm
    );
    my @roots = _pax_runtime_helper_lib_roots();
    return () if !@roots;
    my %seen;
    my @modules;
    for my $rel (@helpers) {
        my $path = _helper_module_path($rel, \@roots);
        next if !-f $path;
        my $source = _slurp_bytes($path);
        while ($source =~ /^\s*use\s+([A-Za-z_][A-Za-z0-9_:]*)\b/gm) {
            my $module = $1;
            next if $module =~ /^PAX::/;
            push @modules, $module if !$seen{$module}++;
        }
        while ($source =~ /^\s*require\s+([A-Za-z_][A-Za-z0-9_:]*)\b/gm) {
            my $module = $1;
            next if $module =~ /^PAX::/;
            push @modules, $module if !$seen{$module}++;
        }
    }
    for my $module (qw(XSLoader)) {
        push @modules, $module if !$seen{$module}++;
    }
    return @modules;
}

sub _pax_runtime_helper_module_files {
    my @roots = _pax_runtime_helper_lib_roots();
    my @modules = _pax_runtime_helper_modules();
    my @files;
    my %seen;
    for my $rel (_pax_runtime_helper_relative_paths()) {
        my $path = _helper_module_path($rel, \@roots);
        next if !$path;
        push @files, $path if !$seen{$path}++;
    }
    for my $module (@modules) {
        my $path = _locate_module_runtime_file($module) or next;
        push @files, $path if !$seen{$path}++;
    }
    for my $path (_probe_loaded_runtime_files(modules => \@modules, lib_dirs => [])) {
        push @files, $path if !$seen{$path}++;
    }
    return @files;
}

sub _helper_module_path {
    my ($rel, $roots) = @_;
    return if !$rel;
    for my $root (@{ $roots // [] }) {
        next if !defined $root || $root eq '';
        my $path = File::Spec->catfile($root, split m{/}, $rel);
        return $path if -f $path;
    }
    return;
}

sub _probe_loaded_runtime_files {
    my (%args) = @_;
    my @modules = @{ $args{modules} // [] };
    return () if !@modules;

    my ($fh, $path) = tempfile(
        'pax-runtime-probe-XXXXXX',
        SUFFIX => '.pl',
        TMPDIR => 1,
        UNLINK => 1,
    );
    print {$fh} <<'PL';
use strict;
use warnings;
use JSON::PP qw(encode_json decode_json);

my $payload = decode_json($ENV{PAX_RUNTIME_PROBE_PAYLOAD} // '{}');
unshift @INC, @{ $payload->{lib_dirs} // [] };
my %before = map { $_ => 1 } keys %INC;

for my $module (@{ $payload->{modules} // [] }) {
    (my $require_path = $module) =~ s{::}{/}g;
    $require_path .= '.pm';
    eval { require $require_path; 1 } or next;
}

my @files;
for my $key (sort keys %INC) {
    next if $before{$key};
    my $value = $INC{$key};
    next if !defined $value || ref $value;
    push @files, $value if $value ne '';
}

print encode_json(\@files);
PL
    close $fh;

    my $payload = JSON::PP->new->ascii(1)->canonical(1)->encode({
        modules => \@modules,
        lib_dirs => [ map { abs_path($_) || $_ } @{ $args{lib_dirs} // [] } ],
    });
    local $ENV{PAX_RUNTIME_PROBE_PAYLOAD} = $payload;
    my $output = qx{$^X $path};
    my $exit = $? >> 8;
    return () if $exit != 0 || !defined $output || $output eq '';
    my $decoded = eval { JSON::PP::decode_json($output) };
    return () if $@ || ref($decoded) ne 'ARRAY';
    my %seen;
    return grep { defined $_ && -f $_ && !$seen{$_}++ } @$decoded;
}

sub _related_xs_files {
    my ($module, $source, $inc_dirs) = @_;
    my @parts = split /::/, $module;



( run in 0.499 second using v1.01-cache-2.11-cpan-71847e10f99 )