@cryptotaxi247 / infra / commits / 448d763c

channel-scripts: vendor and use zstd-capable nix-index

Vendor nixos-channel-scripts instead of pulling it as a flake input, so the channel mirror no longer depends on a separate repository. The mirror builds the nixos-files.sqlite index with nix-index, but cache.nixos.org now serves .ls listings compressed with zstd, which the nixpkgs nix-index (0.1.10) cannot read. Build the package against a nix-index with zstd listing support until the fix is in nixpkgs.

Jörg Thalheim committed Jun 20, 2026 at 16:56 UTC 448d763ce0fde258a3131d8cf4ba07a5f25837e2
6 files changed +602 -29
flake.lock
+42 -24
@@ -193,6 +193,22 @@
193 }
194 },
195 "flake-compat_2": {
196 + "flake": false,
197 + "locked": {
198 + "lastModified": 1767039857,
199 + "narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
200 + "owner": "edolstra",
201 + "repo": "flake-compat",
202 + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
203 + "type": "github"
204 + },
205 + "original": {
206 + "owner": "edolstra",
207 + "repo": "flake-compat",
208 + "type": "github"
209 + }
210 + },
211 + "flake-compat_3": {
212 "flake": false,
213 "locked": {
214 "lastModified": 1761588595,
@@ -208,7 +224,7 @@
224 "type": "github"
225 }
226 },
211 - "flake-compat_3": {
227 + "flake-compat_4": {
228 "flake": false,
229 "locked": {
230 "lastModified": 1767039857,
@@ -555,6 +571,28 @@
571 "type": "github"
572 }
573 },
574 + "nix-index": {
575 + "inputs": {
576 + "flake-compat": "flake-compat_2",
577 + "nixpkgs": [
578 + "nixpkgs"
579 + ]
580 + },
581 + "locked": {
582 + "lastModified": 1781966668,
583 + "narHash": "sha256-4/OeeA8pf5g12YiPM5yZVwvr83Fg7TaNQfv38e3cE5s=",
584 + "owner": "Mic92",
585 + "repo": "nix-index",
586 + "rev": "9cc6166ffef55e7130712f8559865ecc711e7139",
587 + "type": "github"
588 + },
589 + "original": {
590 + "owner": "Mic92",
591 + "ref": "zstd-listings",
592 + "repo": "nix-index",
593 + "type": "github"
594 + }
595 + },
596 "nixocaine": {
597 "inputs": {
598 "iocaine-unstable": "iocaine-unstable",
@@ -580,26 +618,6 @@
618 "url": "https://git.madhouse-project.org/iocaine/nixocaine"
619 }
620 },
583 - "nixos-channel-scripts": {
584 - "inputs": {
585 - "nixpkgs": [
586 - "nixpkgs"
587 - ]
588 - },
589 - "locked": {
590 - "lastModified": 1781506056,
591 - "narHash": "sha256-hqMReyFcAuer7H+ZPgL4DdugcxeU4R57T8mggLDkia8=",
592 - "owner": "NixOS",
593 - "repo": "nixos-channel-scripts",
594 - "rev": "6d6c1337aabf7e209df14d481abcff50bc9a43e0",
595 - "type": "github"
596 - },
597 - "original": {
598 - "owner": "NixOS",
599 - "repo": "nixos-channel-scripts",
600 - "type": "github"
601 - }
602 - },
621 "nixpkgs": {
622 "locked": {
623 "lastModified": 1781483490,
@@ -707,7 +725,7 @@
725 },
726 "pre-commit-hooks": {
727 "inputs": {
710 - "flake-compat": "flake-compat_2",
728 + "flake-compat": "flake-compat_3",
729 "gitignore": "gitignore",
730 "nixpkgs": [
731 "nixocaine",
@@ -761,8 +779,8 @@
779 "geolite2-asn-mmdb": "geolite2-asn-mmdb",
780 "hydra": "hydra",
781 "nix": "nix",
782 + "nix-index": "nix-index",
783 "nixocaine": "nixocaine",
765 - "nixos-channel-scripts": "nixos-channel-scripts",
784 "nixpkgs": "nixpkgs",
785 "nixpkgs-swh": "nixpkgs-swh",
786 "nixpkgs-unstable": "nixpkgs-unstable",
@@ -778,7 +796,7 @@
796 "simple-nixos-mailserver": {
797 "inputs": {
798 "blobs": "blobs",
781 - "flake-compat": "flake-compat_3",
799 + "flake-compat": "flake-compat_4",
800 "git-hooks": "git-hooks",
801 "nixpkgs": [
802 "nixpkgs"
flake.nix
+3 -2
@@ -34,8 +34,9 @@
34 inputs.treefmt-nix.follows = "treefmt-nix";
35 };
36
37 - nixos-channel-scripts = {
38 - url = "github:NixOS/nixos-channel-scripts";
37 + nix-index = {
38 + # zstd .ls listing support, pending https://github.com/nix-community/nix-index/pull/320
39 + url = "github:Mic92/nix-index/zstd-listings";
40 inputs.nixpkgs.follows = "nixpkgs";
41 };
42
modules/hydra-mirror.nix
+6 -3
@@ -15,9 +15,12 @@ let
15 name = "update-${channelName}";
16 value = {
17 description = "Update Channel ${channelName}";
18 - path = with pkgs; [
19 - git
20 - inputs.nixos-channel-scripts.packages.${pkgs.stdenv.hostPlatform.system}.default
18 + path = [
19 + pkgs.git
20 + (pkgs.callPackage ../pkgs/nixos-channel-scripts {
21 + # nixpkgs nix-index cannot read zstd-compressed .ls listings yet
22 + nix-index = inputs.nix-index.packages.${pkgs.stdenv.hostPlatform.system}.default;
23 + })
24 ];
25 script = ''
26 # Hardcoded in channel scripts.
pkgs/nixos-channel-scripts/default.nix new
+88
@@ -0,0 +1,88 @@
1 +{
2 + lib,
3 + stdenv,
4 + pkg-config,
5 + nixVersions,
6 + nlohmann_json,
7 + boost,
8 + makeWrapper,
9 + perl,
10 + perlPackages,
11 + wget,
12 + git,
13 + nix,
14 + gnutar,
15 + xz,
16 + rsync,
17 + openssh,
18 + nix-index,
19 +}:
20 +let
21 + nixos-channel-native-programs = stdenv.mkDerivation {
22 + name = "nixos-channel-native-programs";
23 +
24 + strictDeps = true;
25 +
26 + nativeBuildInputs = [ pkg-config ];
27 +
28 + buildInputs = [
29 + nixVersions.nix_2_28
30 + nlohmann_json
31 + boost
32 + ];
33 +
34 + buildCommand = ''
35 + mkdir -p $out/bin
36 +
37 + $CXX \
38 + -Os -g -Wall \
39 + -std=c++14 \
40 + $(pkg-config --libs --cflags nix-store) \
41 + $(pkg-config --libs --cflags nix-main) \
42 + -I . \
43 + ${./index-debuginfo.cc} \
44 + -o $out/bin/index-debuginfo
45 + '';
46 + };
47 +in
48 +stdenv.mkDerivation {
49 + name = "nixos-channel-scripts";
50 +
51 + strictDeps = true;
52 +
53 + nativeBuildInputs = [ makeWrapper ];
54 +
55 + buildInputs = with perlPackages; [
56 + perl
57 + FileSlurp
58 + LWP
59 + LWPProtocolHttps
60 + ListMoreUtils
61 + DBDSQLite
62 + NetAmazonS3
63 + ];
64 +
65 + buildCommand = ''
66 + mkdir -p $out/bin
67 +
68 + cp ${./mirror-nixos-branch.pl} $out/bin/mirror-nixos-branch
69 + wrapProgram $out/bin/mirror-nixos-branch \
70 + --set PERL5LIB $PERL5LIB \
71 + --set XZ_OPT "-T0" \
72 + --prefix PATH : ${
73 + lib.makeBinPath [
74 + wget
75 + git
76 + nix
77 + gnutar
78 + xz
79 + rsync
80 + openssh
81 + nix-index
82 + nixos-channel-native-programs
83 + ]
84 + }
85 +
86 + patchShebangs $out/bin
87 + '';
88 +}
pkgs/nixos-channel-scripts/index-debuginfo.cc new
+68
@@ -0,0 +1,68 @@
1 +#include <regex>
2 +
3 +#include <nlohmann/json.hpp>
4 +
5 +#include <nix/util/signals.hh>
6 +#include <nix/util/thread-pool.hh>
7 +
8 +#include <nix/store/nar-info.hh>
9 +#include <nix/store/s3-binary-cache-store.hh>
10 +#include <nix/store/sqlite.hh>
11 +
12 +#include <nix/main/shared.hh>
13 +
14 +// cache.nixos.org/debuginfo/<build-id>
15 +// => redirect to NAR
16 +
17 +using namespace nix;
18 +
19 +void mainWrapped(int argc, char * * argv)
20 +{
21 + initNix();
22 +
23 + if (argc != 3) throw Error("usage: index-debuginfo DEBUG-DB BINARY-CACHE-URI");
24 +
25 + Path debugDbPath = argv[1];
26 + std::string binaryCacheUri = argv[2];
27 +
28 + if (hasSuffix(binaryCacheUri, "/")) binaryCacheUri.pop_back();
29 + auto binaryCache = openStore(binaryCacheUri).cast<S3BinaryCacheStore>();
30 +
31 + ThreadPool threadPool(25);
32 +
33 + auto doFile = [&](std::string build_id, std::string url, std::string filename) {
34 + checkInterrupt();
35 +
36 + nlohmann::json json;
37 + json["archive"] = url;
38 + json["member"] = filename;
39 +
40 + std::string key = "debuginfo/" + build_id;
41 +
42 + // FIXME: or should we overwrite? The previous link may point
43 + // to a GC'ed file, so overwriting might be useful...
44 + if (binaryCache->fileExists(key)) return;
45 +
46 + printError("redirecting ‘%s’ to ‘%s’", key, filename);
47 +
48 + binaryCache->upsertFile(key, json.dump(), "application/json");
49 + };
50 +
51 + auto db = SQLite(debugDbPath);
52 +
53 + auto stmt = SQLiteStmt(db, "select build_id, url, filename from DebugInfo;");
54 + auto query = stmt.use();
55 +
56 + while (query.next()) {
57 + threadPool.enqueue(std::bind(doFile, query.getStr(0), query.getStr(1), query.getStr(2)));
58 + }
59 +
60 + threadPool.process();
61 +}
62 +
63 +int main(int argc, char * * argv)
64 +{
65 + return handleExceptions(argv[0], [&]() {
66 + mainWrapped(argc, argv);
67 + });
68 +}
pkgs/nixos-channel-scripts/mirror-nixos-branch.pl new
+395
@@ -0,0 +1,395 @@
1 +#! /usr/bin/env perl
2 +
3 +use strict;
4 +use warnings;
5 +use Data::Dumper;
6 +use Digest::SHA;
7 +use Fcntl qw(:flock);
8 +use File::Basename;
9 +use File::Path;
10 +use File::Slurp;
11 +use File::stat;
12 +use JSON::PP;
13 +use LWP::UserAgent;
14 +use List::MoreUtils qw(uniq);
15 +use Net::Amazon::S3;
16 +use POSIX qw(strftime);
17 +
18 +# Runs the given command, printing the (unescaped) command.
19 +# This command continues on failure.
20 +sub runAllowFailure {
21 + print STDERR " \$ ", join(" ", @_), "\n";
22 + system(@_);
23 +}
24 +
25 +# Runs the given command, printing the (unescaped) command.
26 +# This command dies on failure.
27 +sub run {
28 + my $context = caller(0);
29 + my $code = runAllowFailure(@_);
30 + unless ($code == 0) {
31 + my $exit = $code >> 8;
32 + my $errno = $code - ($exit << 8);
33 + die "Command failed with code ($exit) errno ($errno).\n";
34 + }
35 +
36 + return $code;
37 +}
38 +
39 +my $channelName = $ARGV[0];
40 +my $releaseUrl = $ARGV[1];
41 +
42 +die "Usage: $0 CHANNEL-NAME RELEASE-URL\n" unless defined $channelName && defined $releaseUrl;
43 +
44 +$channelName =~ /^([a-z]+)-(.*)$/ or die;
45 +my $channelDirRel = $channelName eq "nixpkgs-unstable" ? "nixpkgs" : "$1/$2";
46 +
47 +
48 +# Configuration.
49 +my $TMPDIR = $ENV{'TMPDIR'} // "/tmp";
50 +my $filesCache = "${TMPDIR}/nixos-files.sqlite";
51 +my $bucketReleasesName = "nix-releases";
52 +my $bucketChannelsName = "nix-channels";
53 +my $dryRun = $ENV{'DRY_RUN'} // 0;
54 +
55 +$ENV{'GIT_DIR'} = "/home/hydra-mirror/nixpkgs-channels";
56 +
57 +my $bucketReleases;
58 +my $bucketChannels;
59 +
60 +unless ($dryRun) {
61 + # S3 setup.
62 + my $aws_access_key_id = $ENV{'AWS_ACCESS_KEY_ID'} or die "No AWS_ACCESS_KEY_ID given.";
63 + my $aws_secret_access_key = $ENV{'AWS_SECRET_ACCESS_KEY'} or die "No AWS_SECRET_ACCESS_KEY given.";
64 +
65 + my $s3 = Net::Amazon::S3->new(
66 + { aws_access_key_id => $aws_access_key_id,
67 + aws_secret_access_key => $aws_secret_access_key,
68 + retry => 1,
69 + host => "s3-eu-west-1.amazonaws.com",
70 + });
71 +
72 + $bucketReleases = $s3->bucket($bucketReleasesName) or die;
73 +
74 + my $s3_us = Net::Amazon::S3->new(
75 + { aws_access_key_id => $aws_access_key_id,
76 + aws_secret_access_key => $aws_secret_access_key,
77 + retry => 1,
78 + });
79 +
80 + $bucketChannels = $s3_us->bucket($bucketChannelsName) or die;
81 +} else {
82 + print STDERR "WARNING: Running in dry-run.\n";
83 +}
84 +
85 +sub fetch {
86 + my ($url, $type) = @_;
87 +
88 + my $ua = LWP::UserAgent->new;
89 + $ua->default_header('Accept', $type) if defined $type;
90 +
91 + my $response = $ua->get($url);
92 + die "could not download $url: ", $response->status_line, "\n" unless $response->is_success;
93 +
94 + return $response->decoded_content;
95 +}
96 +
97 +my $releaseInfo = decode_json(fetch($releaseUrl, 'application/json'));
98 +
99 +my $releaseId = $releaseInfo->{id} or die;
100 +my $releaseName = $releaseInfo->{nixname} or die;
101 +$releaseName =~ /-([0-9].+)/ or die;
102 +my $releaseVersion = $1;
103 +my $evalId = $releaseInfo->{jobsetevals}->[0] or die;
104 +my $evalUrl = "https://hydra.nixos.org/eval/$evalId";
105 +my $evalInfo = decode_json(fetch($evalUrl, 'application/json'));
106 +my $releasePrefix = "$channelDirRel/$releaseName";
107 +
108 +my $rev = $evalInfo->{jobsetevalinputs}->{nixpkgs}->{revision} or die;
109 +
110 +# Get commit date of $rev as unixtime and formatted string
111 +run("git fetch origin $rev >&2");
112 +my $revUnix = `git show --no-patch --format='%ct' $rev` or die;
113 +my $revDate = strftime("%F %T %Z", localtime($revUnix));
114 +
115 +print STDERR "\nRelease information:\n";
116 +print STDERR " - release is: $releaseName (build $releaseId)\n - eval is: $evalId\n - prefix is: $releasePrefix\n - Git commit is: $rev\n - Git commit date is: $revDate\n\n";
117 +
118 +if ($bucketChannels) {
119 + # Guard against the channel going back in time.
120 + my $curRelease = "";
121 +
122 + if (defined(my $object = $bucketChannels->get_key($channelName))) {
123 + $curRelease = $object->{'x-amz-website-redirect-location'} // "";
124 + }
125 +
126 + if (!defined $ENV{'FORCE'}) {
127 + print STDERR "previous release is $curRelease\n";
128 + $! = 0; # Clear errno to avoid reporting non-fork/exec-related issues
129 + my $d = `NIX_PATH= nix-instantiate --eval -E "builtins.compareVersions (builtins.parseDrvName \\"$curRelease\\").version (builtins.parseDrvName \\"$releaseName\\").version"`;
130 + if ($? != 0) {
131 + warn "Could not execute nix-instantiate: exit $?; errno $!\n";
132 + exit 1;
133 + }
134 + chomp $d;
135 + if ($d == 1) {
136 + warn("channel would go back in time from $curRelease to $releaseName, bailing out\n");
137 + exit;
138 + }
139 + exit if $d == 0;
140 + }
141 +}
142 +
143 +if ($bucketReleases && $bucketReleases->head_key("$releasePrefix")) {
144 + print STDERR "release already exists\n";
145 +} else {
146 + my $tmpDir = "$TMPDIR/release-$channelName/$releaseName";
147 + File::Path::make_path($tmpDir);
148 +
149 + write_file("$tmpDir/src-url", $evalUrl);
150 + write_file("$tmpDir/git-revision", $rev);
151 + write_file("$tmpDir/binary-cache-url", "https://cache.nixos.org");
152 +
153 + if (! -e "$tmpDir/store-paths.xz") {
154 + my $storePaths = decode_json(fetch("$evalUrl/store-paths", 'application/json'));
155 + write_file("$tmpDir/store-paths", join("\n", uniq(@{$storePaths})) . "\n");
156 + }
157 +
158 + sub downloadFile {
159 + my ($jobName, $dstName, $productType) = @_;
160 +
161 + my $buildInfo = decode_json(fetch("$evalUrl/job/$jobName", 'application/json'));
162 +
163 + my $products = ();
164 + # Key the products by subtype.
165 + foreach my $key (keys $buildInfo->{buildproducts}->%*) {
166 + my $subType = $buildInfo->{buildproducts}->{$key}->{subtype};
167 + if ($products->{$subType}) {
168 + die "Job $jobName has multiple products of the same subtype $subType.\nThis is a bad assumption from this script";
169 + }
170 + $products->{$subType} = $buildInfo->{buildproducts}->{$key};
171 + }
172 + my $size = keys %{$products};
173 +
174 + if ($size > 1 && !$productType) {
175 + my $types = join(", ", keys %{$products});
176 + die "Job $jobName has $size build products. Select the right product by subtype [$types]";
177 + }
178 +
179 + my $product;
180 + if (!$productType) {
181 + # Take the only element
182 + my ($key) = keys %{$products};
183 + $product = $products->{$key};
184 + } else {
185 + # Take the selected element
186 + $product = $products->{$productType};
187 + }
188 +
189 + unless ($product) {
190 + die "No product could be selected for $jobName, with type $productType";
191 + }
192 +
193 + my $srcFile = $product->{path} or die "job '$jobName' lacks a store path";
194 + $dstName //= basename($srcFile);
195 + my $dstFile = "$tmpDir/" . $dstName;
196 +
197 + my $sha256_expected = $product->{sha256hash} or die;
198 +
199 + if (! -e $dstFile) {
200 + print STDERR "downloading $srcFile to $dstFile...\n";
201 + write_file("$dstFile.sha256", "$sha256_expected $dstName");
202 + runAllowFailure("NIX_REMOTE=s3://nix-cache nix --experimental-features nix-command store cat '$srcFile' > '$dstFile.tmp'") == 0
203 + or die "unable to fetch $srcFile\n";
204 + rename("$dstFile.tmp", $dstFile) or die;
205 + }
206 +
207 + if (-e "$dstFile.sha256") {
208 + my $sha256_actual = `nix --experimental-features nix-command hash file --base16 --type sha256 '$dstFile'`;
209 + chomp $sha256_actual;
210 + if ($sha256_expected ne $sha256_actual) {
211 + print STDERR "file $dstFile is corrupt $sha256_expected $sha256_actual\n";
212 + exit 1;
213 + }
214 + }
215 + }
216 +
217 + if ($channelName =~ /nixos/) {
218 + downloadFile("nixos.channel", "nixexprs.tar.xz");
219 + downloadFile("nixpkgs.tarball", "packages.json.br", "json-br");
220 + downloadFile("nixos.options", "options.json.br", "json-br");
221 +
222 + # Minimal installer ISOs were dropped from the small channel
223 + if ($channelName !~ /-small/ ||
224 + $channelName =~ /nixos-2([0123]\...|4\.05)-small/) {
225 + downloadFile("nixos.iso_minimal.aarch64-linux");
226 + downloadFile("nixos.iso_minimal.x86_64-linux");
227 + }
228 +
229 + # All of these jobs are not present in small channels
230 + if ($channelName !~ /-small/) {
231 + # These jobs were combined into a single job
232 + if ($channelName =~ /nixos-2[01234]/) {
233 + if ($channelName =~ /nixos-2[0123]/) {
234 + downloadFile("nixos.iso_plasma5.aarch64-linux");
235 + downloadFile("nixos.iso_plasma5.x86_64-linux");
236 + } else {
237 + downloadFile("nixos.iso_plasma6.aarch64-linux");
238 + downloadFile("nixos.iso_plasma6.x86_64-linux");
239 + }
240 +
241 + downloadFile("nixos.iso_gnome.aarch64-linux");
242 + downloadFile("nixos.iso_gnome.x86_64-linux");
243 + } else {
244 + downloadFile("nixos.iso_graphical.aarch64-linux");
245 + downloadFile("nixos.iso_graphical.x86_64-linux");
246 + }
247 +
248 + if ($channelName =~ /nixos-2[0123]/) { # i686 dropped for > 23.11
249 + downloadFile("nixos.iso_minimal.i686-linux");
250 + }
251 +
252 + if ($channelName =~ /nixos-2([0123]\...|4\.05)/) {
253 + downloadFile("nixos.ova.x86_64-linux");
254 + }
255 + }
256 +
257 + } else {
258 + downloadFile("tarball", "nixexprs.tar.xz", "source-dist");
259 + downloadFile("tarball", "packages.json.br", "json-br");
260 + }
261 +
262 + # Generate the programs.sqlite database and put it in
263 + # nixexprs.tar.xz. Also maintain the debug info repository at
264 + # https://cache.nixos.org/debuginfo.
265 + if ($channelName =~ /nixos/ && -e "$tmpDir/store-paths") {
266 + File::Path::make_path("$tmpDir/unpack");
267 + run("tar", "xfJ", "$tmpDir/nixexprs.tar.xz", "-C", "$tmpDir/unpack");
268 + my $exprDir = glob("$tmpDir/unpack/*");
269 + run("nix-channel-index", "-o", "$exprDir/programs.sqlite", "-d", "$exprDir/debug.sqlite", "-f", "$exprDir/nixpkgs", "-s", "aarch64-linux", "-s", "x86_64-linux");
270 + run("index-debuginfo", "$exprDir/debug.sqlite", "s3://nix-cache");
271 + run("rm", "-f", "$tmpDir/nixexprs.tar.xz", "$exprDir/debug.sqlite");
272 + unlink("$tmpDir/nixexprs.tar.xz.sha256");
273 + run("tar", "cfJ", "$tmpDir/nixexprs.tar.xz", "-C", "$tmpDir/unpack", basename($exprDir));
274 + run("rm", "-rf", "$tmpDir/unpack");
275 + }
276 +
277 + if (-e "$tmpDir/store-paths") {
278 + run("xz", "$tmpDir/store-paths");
279 + }
280 +
281 + my $now = strftime("%F %T %Z", localtime);
282 + my $title = "$channelName release $releaseName";
283 + my $githubLink = "https://github.com/NixOS/nixpkgs/commits/$rev";
284 +
285 + my $html = "<html><head>";
286 + $html .= "<title>$title</title></head>";
287 + $html .= "<body><h1>$title</h1>";
288 + $html .= "<p>Released on $now from <a href='$githubLink'>Git commit <tt>$rev</tt></a> from $revDate ";
289 + $html .= "via <a href='$evalUrl'>Hydra evaluation $evalId</a>.</p>";
290 + $html .= "<table><thead><tr><th>File name</th><th>Size</th><th>SHA-256 hash</th></tr></thead><tbody>";
291 +
292 + if ($bucketReleases) {
293 + # Upload the release to S3.
294 + for my $fn (sort glob("$tmpDir/*")) {
295 + my $basename = basename $fn;
296 + my $key = "$releasePrefix/" . $basename;
297 +
298 + unless (defined $bucketReleases->head_key($key)) {
299 + print STDERR "mirroring $fn to s3://$bucketReleasesName/$key...\n";
300 +
301 + # Default headers
302 + my $configuration = ();
303 + $configuration->{content_type} = "application/octet-stream";
304 +
305 + if ($fn =~ /.sha256|src-url|binary-cache-url|git-revision/) {
306 + # Text files
307 + $configuration->{content_type} = "text/plain";
308 + } elsif ($fn =~ /.json.br$/) {
309 + # JSON encoded as brotli
310 + $configuration->{content_type} = "application/json";
311 + $configuration->{content_encoding} = "br";
312 + }
313 +
314 + $bucketReleases->add_key_filename(
315 + $key, $fn, $configuration
316 + ) or die $bucketReleases->err . ": " . $bucketReleases->errstr;
317 + }
318 +
319 + next if $basename =~ /.sha256$/;
320 +
321 + my $size = stat($fn)->size;
322 + my $sha256 = Digest::SHA::sha256_hex(read_file($fn));
323 + $html .= "<tr>";
324 + $html .= "<td><a href='/$key'>$basename</a></td>";
325 + $html .= "<td align='right'>$size</td>";
326 + $html .= "<td><tt>$sha256</tt></td>";
327 + $html .= "</tr>";
328 + }
329 +
330 + $html .= "</tbody></table></body></html>";
331 +
332 + $bucketReleases->add_key($releasePrefix, $html,
333 + { content_type => "text/html" })
334 + or die $bucketReleases->err . ": " . $bucketReleases->errstr;
335 + }
336 +
337 + File::Path::remove_tree($tmpDir);
338 +}
339 +
340 +if ($dryRun) {
341 + print STDERR "WARNING: dry-run finished...\n";
342 + exit(0);
343 +}
344 +
345 +# Update the nixos-* branch in the nixpkgs repo.
346 +run("git remote update origin >&2");
347 +run("git push origin $rev:refs/heads/$channelName >&2");
348 +
349 +# maxage=600: Serve from cache for 5 minutes.
350 +# stale-while-revaliadate=1800: Serve from cache while updating in the background for 30 minutes.
351 +# https://web.dev/stale-while-revalidate/
352 +# https://developer.fastly.com/learning/concepts/cache-freshness/
353 +my $cache_control = "maxage=600,stale-while-revalidate=1800,public";
354 +
355 +sub redirect {
356 + my ($from, $to) = @_;
357 + $to = "https://releases.nixos.org/" . $to;
358 + print STDERR "redirect $from -> $to\n";
359 + $bucketChannels->add_key($from, "", { "x-amz-website-redirect-location" => $to, "cache-control" => $cache_control })
360 + or die $bucketChannels->err . ": " . $bucketChannels->errstr;
361 +}
362 +
363 +# Update channels on channels.nixos.org.
364 +redirect($channelName, $releasePrefix);
365 +redirect("$channelName/nixexprs.tar.xz", "$releasePrefix/nixexprs.tar.xz?rev=$rev&lastModified=$revUnix");
366 +redirect("$channelName/git-revision", "$releasePrefix/git-revision");
367 +redirect("$channelName/packages.json.br", "$releasePrefix/packages.json.br");
368 +redirect("$channelName/store-paths.xz", "$releasePrefix/store-paths.xz");
369 +
370 +# Create redirects relevant only to NixOS channels.
371 +# FIXME: create only redirects to files that exist.
372 +if ($channelName =~ /nixos/) {
373 + # Options listing
374 + redirect("$channelName/options.json.br", "$releasePrefix/options.json.br");
375 +
376 + # Redirects for latest images.
377 + for my $arch ("x86_64-linux", "i686-linux", "aarch64-linux") {
378 + # i686 dropped for > 23.11
379 + next if $arch eq "i686-linux" && $channelName !~ /nixos-2[0123]/;
380 +
381 + for my $artifact ("nixos-graphical",
382 + "nixos-plasma5",
383 + "nixos-plasma6",
384 + "nixos-gnome",
385 + "nixos-minimal",
386 + )
387 + {
388 + redirect("$channelName/latest-$artifact-$arch.iso", "$releasePrefix/$artifact-$releaseVersion-$arch.iso");
389 + redirect("$channelName/latest-$artifact-$arch.iso.sha256", "$releasePrefix/$artifact-$releaseVersion-$arch.iso.sha256");
390 + }
391 +
392 + redirect("$channelName/latest-nixos-$arch.ova", "$releasePrefix/nixos-$releaseVersion-$arch.ova");
393 + redirect("$channelName/latest-nixos-$arch.ova.sha256", "$releasePrefix/nixos-$releaseVersion-$arch.ova.sha256");
394 + }
395 +}