You've already forked guix-tribes
7dec823794
Source: guix-tribes master2ea4cae872Base: previous supertest-dev4fee530b68Mode: tree sync, preserving dev channel authorization
514 lines
16 KiB
Perl
Executable File
514 lines
16 KiB
Perl
Executable File
#!/usr/bin/env perl
|
|
|
|
use strict;
|
|
use warnings;
|
|
|
|
use Cwd qw(abs_path);
|
|
use File::Basename qw(dirname);
|
|
use File::Spec;
|
|
use File::Temp qw(tempdir tempfile);
|
|
use Getopt::Long qw(GetOptionsFromArray);
|
|
use JSON::PP qw(decode_json encode_json);
|
|
|
|
sub usage {
|
|
print <<'EOF';
|
|
Usage: update-plugin-pin [options] plugin [rev]
|
|
|
|
Pin a Tribes external plugin and refresh fixed-output hashes.
|
|
|
|
PLUGIN is the plugin slug. REV defaults to "master" resolved from the
|
|
plugin checkout. The plugin manifest.json is the source of truth for plugin
|
|
id, slug, version, provides, and requires metadata. By default the script expects
|
|
the plugin checkout at ../tribes-plugin-$PLUGIN and the Guix plugin file at
|
|
tribes/plugins/$PLUGIN.scm relative to the guix-tribes checkout.
|
|
|
|
Options:
|
|
--plugin-repo PATH Local plugin git checkout
|
|
--plugin-file PATH Guix plugin definition file to update
|
|
--tribes-repo PATH Local Tribes git checkout used for the host plugin API
|
|
--tribes-rev REV Tribes commit/rev for host API hashing (default: current guix-tribes pin)
|
|
--guix-repo PATH Local guix-tribes checkout
|
|
--build-host HOST SSH host used for Guix builds and hashing
|
|
-h, --help Show this help
|
|
|
|
Hashing and Guix builds run with local `guix` unless --build-host is provided.
|
|
EOF
|
|
}
|
|
|
|
sub fail {
|
|
die "@_\n";
|
|
}
|
|
|
|
sub read_file {
|
|
my ($path) = @_;
|
|
open my $fh, '<', $path or fail("Failed to read $path: $!");
|
|
local $/;
|
|
return <$fh>;
|
|
}
|
|
|
|
sub write_file {
|
|
my ($path, $content) = @_;
|
|
open my $fh, '>', $path or fail("Failed to write $path: $!");
|
|
print {$fh} $content or fail("Failed to write $path: $!");
|
|
close $fh or fail("Failed to close $path: $!");
|
|
}
|
|
|
|
sub command_exists {
|
|
my ($command) = @_;
|
|
|
|
for my $dir (split /:/, ($ENV{PATH} // '')) {
|
|
my $path = File::Spec->catfile($dir, $command);
|
|
return 1 if -x $path;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
sub require_tool {
|
|
my ($tool) = @_;
|
|
command_exists($tool) or fail("Missing required tool: $tool");
|
|
}
|
|
|
|
sub run_capture {
|
|
my (@cmd) = @_;
|
|
my ($fh, $path) = tempfile('command-output.XXXXXX', TMPDIR => 1);
|
|
|
|
my $pid = fork();
|
|
defined $pid or fail("Failed to fork for @cmd: $!");
|
|
|
|
if ($pid == 0) {
|
|
open STDOUT, '>&', $fh or die "Failed to redirect stdout: $!\n";
|
|
open STDERR, '>&', $fh or die "Failed to redirect stderr: $!\n";
|
|
exec @cmd or die "Failed to exec $cmd[0]: $!\n";
|
|
}
|
|
|
|
close $fh or fail("Failed to close $path: $!");
|
|
waitpid($pid, 0);
|
|
my $wait_status = $?;
|
|
my $status = $wait_status == -1 ? 255 : ($wait_status & 127 ? 128 + ($wait_status & 127) : $wait_status >> 8);
|
|
|
|
open my $out, '<', $path or fail("Failed to read $path: $!");
|
|
local $/;
|
|
my $output = <$out> // '';
|
|
close $out or fail("Failed to close $path: $!");
|
|
unlink $path;
|
|
|
|
return ($status, $output);
|
|
}
|
|
|
|
sub run_checked {
|
|
my (@cmd) = @_;
|
|
system(@cmd) == 0 or fail("Command failed: @cmd");
|
|
}
|
|
|
|
sub trim {
|
|
my ($value) = @_;
|
|
$value =~ s/^\s+//;
|
|
$value =~ s/\s+$//;
|
|
return $value;
|
|
}
|
|
|
|
sub scheme_string_list {
|
|
my (@values) = @_;
|
|
return '(' . join(' ', map { encode_json($_) } @values) . ')';
|
|
}
|
|
|
|
sub replace_once {
|
|
my ($text_ref, $pattern, $replacement, $label) = @_;
|
|
my $count = ($$text_ref =~ s/$pattern/$replacement/s);
|
|
$count == 1 or fail("failed to update $label");
|
|
}
|
|
|
|
sub replace_all {
|
|
my ($text_ref, $pattern, $replacement, $label) = @_;
|
|
my $count = ($$text_ref =~ s/$pattern/$replacement/sg);
|
|
$count >= 1 or fail("failed to update $label");
|
|
}
|
|
|
|
my $local_tmp = '';
|
|
my $remote_tmp = '';
|
|
|
|
my @argv = @ARGV;
|
|
my %opts;
|
|
|
|
GetOptionsFromArray(
|
|
\@argv,
|
|
'plugin-repo=s' => \$opts{plugin_repo},
|
|
'plugin-file=s' => \$opts{plugin_file},
|
|
'tribes-repo=s' => \$opts{tribes_repo},
|
|
'tribes-rev=s' => \$opts{tribes_rev},
|
|
'guix-repo=s' => \$opts{guix_repo},
|
|
'build-host=s' => \$opts{build_host},
|
|
'h|help' => \$opts{help},
|
|
) or do {
|
|
usage();
|
|
exit 1;
|
|
};
|
|
|
|
if ($opts{help}) {
|
|
usage();
|
|
exit 0;
|
|
}
|
|
|
|
my $plugin = shift @argv;
|
|
defined $plugin && length $plugin or do {
|
|
usage();
|
|
exit 1;
|
|
};
|
|
|
|
my $rev = shift(@argv) // 'master';
|
|
@argv == 0 or do {
|
|
usage();
|
|
exit 1;
|
|
};
|
|
|
|
my $script_dir = abs_path(dirname($0));
|
|
my $default_guix_repo = abs_path(File::Spec->catdir($script_dir, '..'));
|
|
my $default_tribes_repo = abs_path(File::Spec->catdir($default_guix_repo, '..', 'tribes'));
|
|
|
|
my $guix_repo = $opts{guix_repo} ? abs_path($opts{guix_repo}) : $default_guix_repo;
|
|
my $tribes_repo = $opts{tribes_repo} ? abs_path($opts{tribes_repo}) : $default_tribes_repo;
|
|
my $plugin_repo =
|
|
$opts{plugin_repo}
|
|
? abs_path($opts{plugin_repo})
|
|
: abs_path(File::Spec->catdir($guix_repo, '..', "tribes-plugin-$plugin"));
|
|
my $plugin_file =
|
|
$opts{plugin_file}
|
|
? abs_path($opts{plugin_file})
|
|
: File::Spec->catfile($guix_repo, 'tribes', 'plugins', "$plugin.scm");
|
|
my $source_file = File::Spec->catfile($guix_repo, 'tribes', 'packages', 'source.scm');
|
|
my $plugin_package_name = "tribes-plugin-$plugin";
|
|
|
|
-d File::Spec->catdir($plugin_repo, '.git') or fail("Plugin repo not found: $plugin_repo");
|
|
-d File::Spec->catdir($tribes_repo, '.git') or fail("Tribes repo not found: $tribes_repo");
|
|
-f $plugin_file or fail("Plugin file not found: $plugin_file");
|
|
-f $source_file or fail("guix-tribes source file not found: $source_file");
|
|
|
|
require_tool($_) for qw(env git tar perl);
|
|
|
|
my $use_remote = defined($opts{build_host}) && $opts{build_host} ne '' ? 1 : 0;
|
|
require_tool('guix') unless $use_remote;
|
|
|
|
my ($status, $commit_output) = run_capture('git', '-C', $plugin_repo, 'rev-parse', "$rev\^\{commit\}");
|
|
$status == 0 or fail(trim($commit_output));
|
|
my $commit = trim($commit_output);
|
|
|
|
my $tribes_rev = $opts{tribes_rev};
|
|
if (!defined $tribes_rev || $tribes_rev eq '') {
|
|
my $source_text = read_file($source_file);
|
|
$source_text =~ /\(define %tribes-commit\s+"([^"]+)"\)/
|
|
or fail("Failed to resolve %tribes-commit from $source_file");
|
|
$tribes_rev = $1;
|
|
}
|
|
|
|
($status, my $host_commit_output) =
|
|
run_capture('git', '-C', $tribes_repo, 'rev-parse', "$tribes_rev\^\{commit\}");
|
|
$status == 0 or fail(trim($host_commit_output));
|
|
my $host_commit = trim($host_commit_output);
|
|
|
|
$local_tmp = tempdir('plugin-pin.XXXXXX', TMPDIR => 1, CLEANUP => 0);
|
|
|
|
END {
|
|
if (defined $remote_tmp && $remote_tmp ne '') {
|
|
system('ssh', $opts{build_host}, "rm -rf '$remote_tmp'");
|
|
}
|
|
|
|
if (defined $local_tmp && $local_tmp ne '' && -d $local_tmp) {
|
|
system('rm', '-rf', $local_tmp);
|
|
}
|
|
}
|
|
|
|
my $plugin_source_dir = File::Spec->catdir($local_tmp, 'plugin-source');
|
|
my $tribes_source_dir = File::Spec->catdir($local_tmp, 'tribes-source');
|
|
mkdir $plugin_source_dir or fail("Failed to create $plugin_source_dir: $!");
|
|
mkdir $tribes_source_dir or fail("Failed to create $tribes_source_dir: $!");
|
|
|
|
my $plugin_tar = File::Spec->catfile($local_tmp, 'plugin-source.tar');
|
|
my $tribes_tar = File::Spec->catfile($local_tmp, 'tribes-source.tar');
|
|
|
|
run_checked('git', '-C', $plugin_repo, 'archive', '--output', $plugin_tar, $commit);
|
|
run_checked('git', '-C', $tribes_repo, 'archive', '--output', $tribes_tar, $host_commit);
|
|
run_checked('tar', '-xf', $plugin_tar, '-C', $plugin_source_dir);
|
|
run_checked('tar', '-xf', $tribes_tar, '-C', $tribes_source_dir);
|
|
|
|
my $manifest_file = File::Spec->catfile($plugin_source_dir, 'manifest.json');
|
|
-f $manifest_file or fail("Plugin manifest not found at $manifest_file");
|
|
|
|
my $manifest = decode_json(read_file($manifest_file));
|
|
for my $key (qw(id slug version provides requires)) {
|
|
exists $manifest->{$key} or fail("manifest missing required key: $key");
|
|
}
|
|
|
|
ref($manifest->{provides}) eq 'ARRAY'
|
|
&& !grep { ref($_) || !defined($_) } @{ $manifest->{provides} }
|
|
or fail('manifest provides must be a list of strings');
|
|
|
|
ref($manifest->{requires}) eq 'ARRAY'
|
|
&& !grep { ref($_) || !defined($_) } @{ $manifest->{requires} }
|
|
or fail('manifest requires must be a list of strings');
|
|
|
|
my $plugin_id = $manifest->{id};
|
|
my $plugin_slug = $manifest->{slug};
|
|
my $version = $manifest->{version};
|
|
my $provides_joined = join("\037", @{ $manifest->{provides} });
|
|
my $requires_joined = join("\037", @{ $manifest->{requires} });
|
|
|
|
$plugin_slug eq $plugin or fail("Plugin manifest slug mismatch: expected $plugin, got $plugin_slug");
|
|
|
|
my ($plugin_source_for_scheme, $tribes_source_for_scheme, $guix_load_path);
|
|
my $source_hash;
|
|
|
|
sub setup_remote {
|
|
require_tool($_) for qw(rsync ssh);
|
|
|
|
if ($remote_tmp eq '') {
|
|
print STDERR "Using build host $opts{build_host}.\n";
|
|
|
|
($status, my $remote_tmp_output) = run_capture('ssh', $opts{build_host}, 'mktemp -d /tmp/plugin-pin.XXXXXX');
|
|
$status == 0 or fail(trim($remote_tmp_output));
|
|
$remote_tmp = trim($remote_tmp_output);
|
|
|
|
run_checked('rsync', '-az', '--delete', '--exclude', '.git', "$guix_repo/", "$opts{build_host}:$remote_tmp/guix-tribes/");
|
|
run_checked('rsync', '-az', '--delete', "$plugin_source_dir/", "$opts{build_host}:$remote_tmp/plugin-source/");
|
|
run_checked('rsync', '-az', '--delete', "$tribes_source_dir/", "$opts{build_host}:$remote_tmp/tribes-source/");
|
|
}
|
|
|
|
$plugin_source_for_scheme = "$remote_tmp/plugin-source";
|
|
$tribes_source_for_scheme = "$remote_tmp/tribes-source";
|
|
$guix_load_path = "$remote_tmp/guix-tribes";
|
|
|
|
($status, my $source_hash_output) =
|
|
run_capture('ssh', $opts{build_host}, "guix hash -rx '$plugin_source_for_scheme'");
|
|
$status == 0 or fail(trim($source_hash_output));
|
|
$source_hash = trim($source_hash_output);
|
|
$source_hash =~ tr/\r//d;
|
|
}
|
|
|
|
if ($use_remote) {
|
|
setup_remote();
|
|
} else {
|
|
$plugin_source_for_scheme = $plugin_source_dir;
|
|
$tribes_source_for_scheme = $tribes_source_dir;
|
|
$guix_load_path = $guix_repo;
|
|
|
|
($status, my $source_hash_output) = run_capture('guix', 'hash', '-rx', $plugin_source_dir);
|
|
$status == 0 or fail(trim($source_hash_output));
|
|
$source_hash = trim($source_hash_output);
|
|
$source_hash =~ tr/\r//d;
|
|
}
|
|
|
|
sub run_scheme {
|
|
my ($name, $body) = @_;
|
|
my ($fh, $local_path) = tempfile("$name.XXXXXX", DIR => $local_tmp, SUFFIX => '.scm');
|
|
print {$fh} $body or fail("Failed to write $local_path: $!");
|
|
close $fh or fail("Failed to close $local_path: $!");
|
|
|
|
if ($use_remote) {
|
|
run_checked('rsync', '-az', $local_path, "$opts{build_host}:$remote_tmp/$name.scm");
|
|
my ($exit, $output) =
|
|
run_capture('ssh', $opts{build_host}, "guix build -L '$guix_load_path' -f '$remote_tmp/$name.scm' --no-grafts 2>&1");
|
|
return $output;
|
|
}
|
|
|
|
my ($exit, $output) =
|
|
run_capture('guix', 'build', '-L', $guix_load_path, '-f', $local_path, '--no-grafts');
|
|
return $output;
|
|
}
|
|
|
|
sub extract_hash {
|
|
my ($output) = @_;
|
|
my $hash;
|
|
|
|
for my $line (split /\n/, $output) {
|
|
if ($line =~ /\b(?:got|actual hash):\s*([0-9a-z]{52})/) {
|
|
$hash = $1;
|
|
}
|
|
}
|
|
|
|
defined $hash or fail("Failed to extract hash from build output.");
|
|
return $hash;
|
|
}
|
|
|
|
sub build_hash {
|
|
my ($label, $build) = @_;
|
|
|
|
my $output = $build->();
|
|
my $hash = eval { extract_hash($output) };
|
|
if ($@) {
|
|
my $hint = $use_remote
|
|
? "Build host Guix did not complete the hash refresh."
|
|
: "Local Guix did not complete the hash refresh. To run this step on a build host, rerun with --build-host HOST.";
|
|
fail("Failed to extract $label hash.\n$hint\n$output");
|
|
}
|
|
|
|
return $hash;
|
|
}
|
|
|
|
my $dummy_hash = '0' x 52;
|
|
|
|
my $text = read_file($plugin_file);
|
|
my @candidates;
|
|
for my $candidate ($plugin, ($plugin =~ tr/_/-/r)) {
|
|
push @candidates, $candidate unless grep { $_ eq $candidate } @candidates;
|
|
}
|
|
|
|
my ($symbol_base) =
|
|
grep { $text =~ /\(define %\Q$_\E-commit\s+"[^"]+"\)/ } @candidates;
|
|
defined $symbol_base
|
|
or fail("failed to locate plugin symbol prefix for '$plugin' in $plugin_file");
|
|
|
|
my $package_body = $text;
|
|
if ($text =~ /\(define\* \(\Q$symbol_base\E-package-from-source\b(.*?)(?:\n\(define \Q$symbol_base\E-package\b)/s) {
|
|
$package_body = $1;
|
|
}
|
|
|
|
sub package_reuses_host_libs {
|
|
my ($body) = @_;
|
|
|
|
return 1 if $body =~ /#:reuse-host-libs\?\s+#t\b/;
|
|
return 0 if $body =~ /#:reuse-host-libs\?\s+#f\b/;
|
|
|
|
if ($body =~ /#:reuse-host-libs\?\s+reuse-host-libs\?/) {
|
|
return 1 if $body =~ /\(reuse-host-libs\?\s+#t\)/;
|
|
return 0 if $body =~ /\(reuse-host-libs\?\s+#f\)/;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
sub package_includes_mix_deps {
|
|
my ($body, $reuse_host_libs) = @_;
|
|
|
|
return 1 if $body =~ /#:include-mix-deps\?\s+#t\b/;
|
|
return 0 if $body =~ /#:include-mix-deps\?\s+#f\b/;
|
|
|
|
if ($body =~ /#:include-mix-deps\?\s+include-mix-deps\?/) {
|
|
return 1 if $body =~ /\(include-mix-deps\?\s+#t\)/;
|
|
return 0 if $body =~ /\(include-mix-deps\?\s+#f\)/;
|
|
}
|
|
|
|
return $reuse_host_libs ? 0 : 1;
|
|
}
|
|
|
|
my $reuse_host_libs = package_reuses_host_libs($package_body);
|
|
my $include_mix_deps = package_includes_mix_deps($package_body, $reuse_host_libs);
|
|
|
|
sub host_setup_gexp {
|
|
return <<"EOF";
|
|
#~(begin
|
|
(let ((host-root (string-append work "/tribes")))
|
|
(when (file-exists? host-root)
|
|
(delete-file-recursively host-root))
|
|
(copy-recursively #+(local-file "$tribes_source_for_scheme" #:recursive? #t)
|
|
host-root
|
|
#:follow-symlinks? #t)
|
|
(invoke "chmod" "-R" "u+w" host-root)))
|
|
EOF
|
|
}
|
|
|
|
my $source_setup_gexp = $reuse_host_libs ? "#~(begin)" : host_setup_gexp();
|
|
|
|
sub mix_deps_output {
|
|
return run_scheme(
|
|
'mix-deps',
|
|
<<"EOF"
|
|
(use-modules (guix gexp) (tribes packages mix))
|
|
(fetch-mix-deps
|
|
(local-file "$plugin_source_for_scheme" #:recursive? #t)
|
|
#:name "$plugin_package_name-mix-deps"
|
|
#:version "$version"
|
|
#:sha256 "$dummy_hash"
|
|
#:setup-gexp $source_setup_gexp)
|
|
EOF
|
|
);
|
|
}
|
|
|
|
sub npm_deps_output {
|
|
return run_scheme(
|
|
'npm-deps',
|
|
<<"EOF"
|
|
(use-modules (guix gexp) (tribes packages mix))
|
|
(fetch-npm-deps
|
|
(local-file "$plugin_source_for_scheme" #:recursive? #t)
|
|
#:name "$plugin_package_name-npm-deps"
|
|
#:version "$version"
|
|
#:sha256 "$dummy_hash"
|
|
#:assets-dir "assets"
|
|
#:setup-gexp $source_setup_gexp)
|
|
EOF
|
|
);
|
|
}
|
|
|
|
$text =~ /\(define %\Q$symbol_base\E-mix-deps-sha256\s+"([^"]+)"\)/
|
|
or fail("failed to locate %$symbol_base-mix-deps-sha256 in $plugin_file");
|
|
my $mix_hash = $1;
|
|
if ($include_mix_deps) {
|
|
$mix_hash = build_hash('mix deps', \&mix_deps_output);
|
|
}
|
|
|
|
my $npm_hash = '';
|
|
if (-f File::Spec->catfile($plugin_source_dir, 'assets', 'package-lock.json')) {
|
|
$npm_hash = build_hash('npm deps', \&npm_deps_output);
|
|
}
|
|
|
|
replace_once(
|
|
\$text,
|
|
qr/\(define %\Q$symbol_base\E-commit\s+"[^"]+"\)/,
|
|
qq((define %$symbol_base-commit\n "$commit")),
|
|
"%$symbol_base-commit",
|
|
);
|
|
replace_once(
|
|
\$text,
|
|
qr/\(git-version "[^"]+" %\Q$symbol_base\E-revision %\Q$symbol_base\E-commit\)/,
|
|
qq((git-version "$version" %$symbol_base-revision %$symbol_base-commit)),
|
|
"%$symbol_base-version",
|
|
);
|
|
replace_once(
|
|
\$text,
|
|
qr/\(define %\Q$symbol_base\E-source-sha256\s+"[^"]+"\)/,
|
|
qq((define %$symbol_base-source-sha256\n "$source_hash")),
|
|
"%$symbol_base-source-sha256",
|
|
);
|
|
replace_once(
|
|
\$text,
|
|
qr/\(define %\Q$symbol_base\E-mix-deps-sha256\s+"[^"]+"\)/,
|
|
qq((define %$symbol_base-mix-deps-sha256\n "$mix_hash")),
|
|
"%$symbol_base-mix-deps-sha256",
|
|
);
|
|
my $npm_replacement =
|
|
$npm_hash ne ''
|
|
? qq((define %$symbol_base-npm-deps-sha256\n "$npm_hash"))
|
|
: qq((define %$symbol_base-npm-deps-sha256\n #f));
|
|
replace_once(
|
|
\$text,
|
|
qr/\(define %\Q$symbol_base\E-npm-deps-sha256\s+(?:"[^"]+"|#f)\)/,
|
|
$npm_replacement,
|
|
"%$symbol_base-npm-deps-sha256",
|
|
);
|
|
replace_all(
|
|
\$text,
|
|
qr/#:provides\s+'\([^)]*\)/,
|
|
"#:provides '" . scheme_string_list(@{ $manifest->{provides} }),
|
|
'plugin provides',
|
|
);
|
|
replace_all(
|
|
\$text,
|
|
qr/#:requires\s+'\([^)]*\)/,
|
|
"#:requires '" . scheme_string_list(@{ $manifest->{requires} }),
|
|
'plugin requires',
|
|
);
|
|
|
|
write_file($plugin_file, $text);
|
|
|
|
print "Updated $plugin_file\n";
|
|
print "plugin: $plugin\n";
|
|
print "plugin id: $plugin_id\n";
|
|
print "commit: $commit\n";
|
|
print "host tribes commit: $host_commit\n";
|
|
print "version: $version\n";
|
|
print "source sha256: $source_hash\n";
|
|
print "mix deps sha256: $mix_hash\n";
|
|
print "npm deps sha256: $npm_hash\n" if $npm_hash ne '';
|
|
print 'provides: ', join(',', @{ $manifest->{provides} }), "\n";
|
|
print 'requires: ', join(',', @{ $manifest->{requires} }), "\n";
|