Alien-Xrepo

 view release on metacpan or  search on metacpan

lib/Alien/Xrepo.pm  view on Meta::CPAN


        # Fetch-first: an already-installed package answers with real paths immediately, so the mutating install is
        # skipped (and the result is memorized for next launch).
        my ( $data, $err ) = $self->_try_fetch( $full_spec, \%opts );
        if ( defined $data && $self->_info_installed($data) ) {
            say "[*] xrepo: $full_spec already installed, reusing fetch result..." if $verbose;
            if ( $meta && defined $key ) {
                $self->_cache_put( $key, $meta, $self->_cache_entry($data) );
                $self->_cache_save( $meta, %opts );
            }
            return $self->_finalize( $data, %opts );
        }

        # Cold path: install, then fetch (which must succeed, since it tells us where things land).
        my @install_cmd = $self->_argv( 'install', \@args, $full_spec );
        $self->_debug_cmd(@install_cmd);
        $self->blah("Running: @install_cmd");
        system(@install_cmd) == 0 or die "xrepo install failed for $full_spec";
        say "[*] xrepo: fetching paths..." if $verbose;
        my $fresh = $self->_require_fetch( $full_spec, \%opts, $err );
        if ( $meta && defined $key ) {
            $self->_cache_put( $key, $meta, $self->_cache_entry($fresh) );
            $self->_cache_save( $meta, %opts );
        }
        $self->_finalize( $fresh, %opts );
    }

    # LRU cap on cached resolutions. Entries are just the fetch JSON plus its install dir, so the bound keeps the file
    # a few hundred KB even after a long debugging session.
    sub _CACHE_MAX () {64}

    # Where the resolution cache lives: alongside the caller's/instance's store when one is set, else the xmake global
    # dir (~/.xmake) that xrepo itself uses for the default per-user store.
    method _cache_dir (%opts) {
        my $store = $self->_store_dir(%opts);
        return path($store)->child('.alien-xmake') if defined $store && length $store;
        my $home = $ENV{XMAKE_GLOBALDIR};
        $home //= $ENV{HOME};
        if ( !defined $home || !length $home ) {
            $home = $^O eq 'MSWin32' ? "$ENV{HOMEDRIVE}$ENV{HOMEPATH}" : ();
        }
        return () if !defined $home || !length $home;
        path($home)->child( '.xmake', '.alien-xmake' );
    }

    # Cache identity: everything that can move the installed layout. configs values run through the same boolean
    # stringifier as the CLI, so built-in true/false produce a stable key.
    method _cache_key ( $full_spec, $opts ) {
        my @parts = ( $full_spec, $opts->{kind} // '', $opts->{plat} // '', $opts->{arch} // '', $opts->{mode} // '' );
        if ( my $c = $opts->{configs} ) {
            my $str = ref $c eq 'HASH' ? join( ',', map { "$_=" . Alien::Xmake::_bool_str( $c->{$_} ) } sort keys %$c ) : "$c";
            push @parts, $str;
        }
        sha1_hex( join( "\0", @parts ) );
    }

    method _cache_load (%opts) {
        my $dir  = $self->_cache_dir(%opts) or return ();
        my $file = $dir->child('cache.json');
        return () unless -f $file;
        my $raw = eval { $file->slurp_utf8 };
        return () unless defined $raw && length $raw;
        my $meta = eval { decode_json($raw) };
        return () unless ref $meta eq 'HASH' && ref $meta->{order} eq 'ARRAY' && ref $meta->{entries} eq 'HASH';
        $meta;
    }

    method _cache_save ( $meta, %opts ) {
        my $dir = $self->_cache_dir(%opts) or return;
        my ( @order, %keep );
        for my $k ( @{ $meta->{order} // [] } ) {
            my $e = $meta->{entries}{$k} or next;
            next unless $self->_entry_alive($e);
            push @order, $k;
            $keep{$k} = $e;
        }
        while ( @order > _CACHE_MAX() ) { delete $keep{ pop @order }; }
        $dir->mkpath;
        my $file = $dir->child('cache.json');
        my $tmp  = $dir->child('cache.json.tmp');
        $tmp->remove if $tmp->exists;    # clear a tmp stranded by an interrupted save
        $tmp->spew_utf8( encode_json( { version => 1, order => \@order, entries => \%keep } ) );
        $file->remove if $^O eq 'MSWin32' && $file->exists;
        move( "$tmp", "$file" );
    }

    method _entry_alive ($e) {
        return () unless ref $e eq 'HASH';
        return -f $e->{libpath} if defined $e->{libpath} && length $e->{libpath};
        defined $e->{installdir} && length $e->{installdir} && -d $e->{installdir};
    }

    method _cache_get ( $key, $meta ) {
        my $e = $meta->{entries}{$key} or return ();
        return () unless $self->_entry_alive($e);
        $e->{last_used} = time;
        $meta->{order}  = [ $key, grep { $_ ne $key } @{ $meta->{order} } ];
        $e;
    }

    method _cache_put ( $key, $meta, $entry, $max //= _CACHE_MAX() ) {
        $meta->{entries}{$key} = $entry;
        $meta->{order} = [ $key, grep { $_ ne $key } @{ $meta->{order} } ];
        while ( @{ $meta->{order} } > $max ) {
            delete $meta->{entries}{ pop @{ $meta->{order} } };
        }
        $meta;
    }

    method _cache_entry ($data) {
        my $info  = ref $data eq 'ARRAY' ? $data->[0] : $data;
        my %entry = ( json => encode_json($data), last_used => time );
        if ( ref $info eq 'HASH' ) {
            my $art = $info->{artifacts};
            if    ( ref $art eq 'HASH' && defined $art->{installdir} ) { $entry{installdir} = $art->{installdir}; }
            elsif ( defined $info->{installdir} )                      { $entry{installdir} = $info->{installdir}; }
            if    ( @{ $info->{libfiles} // [] } )                     { $entry{libpath}    = $info->{libfiles}[0]; }
        }
        \%entry;
    }

    method _try_fetch ( $full_spec, $opts ) {
        my @fetch_args = $self->_build_args($opts);
        my @fetch_cmd  = $self->_argv( 'fetch', [ '--json', @fetch_args ], $full_spec );
        $self->_debug_cmd(@fetch_cmd);
        $self->blah("Running: @fetch_cmd");
        my ( $out, $err, $exit ) = capture { system @fetch_cmd };
        return ( undef, "Command: @fetch_cmd\nError:\n$err" ) if $exit != 0;
        return ( undef, "Command: @fetch_cmd\nNo JSON output" ) unless defined $out && length $out;
        my $data = eval { $self->_decode_json_output($out) };
        return ( undef, "Command: @fetch_cmd\n$@" ) if !defined $data;
        ( $data, undef );
    }

    # Mandatory fetch: after an install we have to know where the output landed, so a failure here is fatal. $why
    # carries the earlier probe error so the die explains which attempt failed.
    method _require_fetch ( $full_spec, $opts, $why //= () ) {
        my ( $data, $err ) = $self->_try_fetch( $full_spec, $opts );
        unless ( defined $data ) {
            die "xrepo fetch failed:\n$err\n" . ( defined $why && length $why ? "\n(an earlier fetch probe also failed:\n$why)" : () );
        }
        $data;



( run in 0.876 second using v1.01-cache-2.11-cpan-364913b4093 )