App-Ariza.git | lib/App/Ariza/ | Update.rakumod


use Template::Jinja2;

use App::Ariza::Resources;
use App::Ariza::Tools;

unit class App::Ariza::Update;

our constant UPDATE-PROTOCOL-VERSION is export = 1;
our constant UPDATE-HANDOFF-EXIT is export = 75;
our constant UPDATE-CHECK-INTERVAL is export = 604_800;
our constant UPDATE-VERSION-REGEX is export =
    /^ <[0..9]>+ '.' <[0..9]>+ '.' <[0..9]>+ $/;

my constant TEMPLATE = 'templates/update-coordinator.raku.j2';

our sub valid-update-version(Str:D $version --> Bool:D) is export {
    so $version ~~ UPDATE-VERSION-REGEX
}

our sub compare-update-versions(Str:D $left, Str:D $right --> Order:D) is export {
    die "ariza: update version '$left' must match X.Y.Z"
        unless valid-update-version($left);
    die "ariza: update version '$right' must match X.Y.Z"
        unless valid-update-version($right);
    for $left.split('.') Z $right.split('.') -> ($a, $b) {
        my $aa = $a.subst(/^ '0'+ /, '') || '0';
        my $bb = $b.subst(/^ '0'+ /, '') || '0';
        return More if $aa.chars > $bb.chars;
        return Less if $aa.chars < $bb.chars;
        return More if $aa gt $bb;
        return Less if $aa lt $bb;
    }
    Same
}

#| The coordinator and platform-private installer locations inside a bundle.
method coordinator-rel(--> Str) { 'libexec/ariza/update.raku' }

method installer-rel(Str:D $slug --> Str) {
    $slug.starts-with('windows-')
        ?? 'libexec/ariza/install.ps1'
        !! 'libexec/ariza/install.sh'
}

#| Quote one build-time value as a single-quoted Raku string literal.
#| Configuration strings are data, never generated source.
method raku-quote(Str:D $value --> Str) {
    "'" ~ $value
        .subst('\\', '\\\\', :g)
        .subst("'", "\\'", :g)
        .subst("\r", '\\r', :g)
        .subst("\n", '\\n', :g)
        .subst("\t", '\\t', :g)
      ~ "'"
}

#| Render-time facts for the bundle-private, App::Ariza-free coordinator.
#| Paths are bundle-relative and the installer path is selected here so the
#| generated program never has to infer its platform at run time.
method context(
    Str:D :$app-name!,
    Str:D :$app-exec!,
    Str:D :$app-display!,
    Str:D :$app-version!,
    Str:D :$repo!,
    Str:D :$slug!,
    --> Hash
) {
    die "ariza: update repository must be an owner/name GitHub repository"
        unless $repo ~~ /^ <[A..Za..z0..9_.-]>+ '/' <[A..Za..z0..9_.-]>+ $/;
    die "ariza: update-enabled version '$app-version' is not x.y.z"
        unless valid-update-version($app-version);

    my %values =
        app_name      => $app-name,
        app_exec      => $app-exec,
        app_display   => $app-display,
        app_version   => $app-version,
        repo          => $repo,
        installer_rel => self.installer-rel($slug),
    ;
    %values.map({ .key ~ '_q' => self.raku-quote(.value) }).Hash
}

method render(*%ctx --> Str) {
    my $out = Template::Jinja2.new
        .from-string(resource(TEMPLATE).slurp)
        .render(|%ctx);
    $out ~= "\n" unless $out.ends-with("\n");
    $out
}

#| Stage only the coordinator. The exact-candidate installer is supplied by
#| the platform installer work and lives at the already-baked path returned in
#| C<installer-rel>; launcher integration is deliberately a separate step.
method write(
    IO() :$bundle-dir!,
    Str:D :$app-name!,
    Str:D :$app-exec!,
    Str:D :$app-display!,
    Str:D :$app-version!,
    Str:D :$repo!,
    Str:D :$slug!,
    --> Hash
) {
    my %ctx = self.context(:$app-name, :$app-exec, :$app-display,
                           :$app-version, :$repo, :$slug);
    my $path = ensure-dir($bundle-dir.add('libexec').add('ariza'))
        .add('update.raku');
    $path.spurt(self.render(|%ctx));
    $path.chmod(0o644);
    %(
        coordinator => $path,
        installer   => $bundle-dir.add(self.installer-rel($slug)),
    )
}

=begin pod

=head1 NAME

App::Ariza::Update - render the bundle-private update coordinator

=head1 DESCRIPTION

This is build-time code only. It bakes application identity, current version,
repository and the platform-local installer path into one core-only Raku
program at C<libexec/ariza/update.raku>. The generated program has no run-time
dependency on App::Ariza.

=end pod