forked from lix-project/lix
Compare commits
1 commit
b289df3ab7
...
260c3afdbb
Author | SHA1 | Date | |
---|---|---|---|
|
260c3afdbb |
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -28,4 +28,3 @@ buildtime.bin
|
||||||
# We generate this with a Nix shell hook
|
# We generate this with a Nix shell hook
|
||||||
/.pre-commit-config.yaml
|
/.pre-commit-config.yaml
|
||||||
/.nocontribmsg
|
/.nocontribmsg
|
||||||
/release
|
|
||||||
|
|
|
@ -1,62 +1,64 @@
|
||||||
# Using Lix within Docker
|
# Using Lix within Docker
|
||||||
|
|
||||||
Lix is available on the following two container registries:
|
Currently the Lix project doesn't ship docker images. However, we have the infrastructure to do it, it's just not yet been done. See https://git.lix.systems/lix-project/lix/issues/252
|
||||||
- [ghcr.io/lix-project/lix](https://ghcr.io/lix-project/lix)
|
|
||||||
- [git.lix.systems/lix-project/lix](https://git.lix.systems/lix-project/-/packages/container/lix)
|
<!--
|
||||||
|
|
||||||
To run the latest stable release of Lix with Docker run the following command:
|
To run the latest stable release of Lix with Docker run the following command:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
~ » sudo podman run -it ghcr.io/lix-project/lix:latest
|
$ docker run -ti nixos/nix
|
||||||
Trying to pull ghcr.io/lix-project/lix:latest...
|
Unable to find image 'nixos/nix:latest' locally
|
||||||
|
latest: Pulling from nixos/nix
|
||||||
bash-5.2# nix --version
|
5843afab3874: Pull complete
|
||||||
nix (Lix, like Nix) 2.90.0
|
b52bf13f109c: Pull complete
|
||||||
|
1e2415612aa3: Pull complete
|
||||||
|
Digest: sha256:27f6e7f60227e959ee7ece361f75d4844a40e1cc6878b6868fe30140420031ff
|
||||||
|
Status: Downloaded newer image for nixos/nix:latest
|
||||||
|
35ca4ada6e96:/# nix --version
|
||||||
|
nix (Nix) 2.3.12
|
||||||
|
35ca4ada6e96:/# exit
|
||||||
```
|
```
|
||||||
|
|
||||||
# What is included in Lix's Docker image?
|
# What is included in Lix's Docker image?
|
||||||
|
|
||||||
The official Docker image is created using [nix2container]
|
The official Docker image is created using `pkgs.dockerTools.buildLayeredImage`
|
||||||
(and not with `Dockerfile` as it is usual with Docker images). You can still
|
(and not with `Dockerfile` as it is usual with Docker images). You can still
|
||||||
base your custom Docker image on it as you would do with any other Docker
|
base your custom Docker image on it as you would do with any other Docker
|
||||||
image.
|
image.
|
||||||
|
|
||||||
[nix2container]: https://github.com/nlewo/nix2container
|
The Docker image is also not based on any other image and includes minimal set
|
||||||
|
of runtime dependencies that are required to use Lix:
|
||||||
|
|
||||||
The Docker image is also not based on any other image and includes the nixpkgs
|
- pkgs.nix
|
||||||
that Lix was built with along with a minimal set of tools in the system profile:
|
- pkgs.bashInteractive
|
||||||
|
- pkgs.coreutils-full
|
||||||
- bashInteractive
|
- pkgs.gnutar
|
||||||
- cacert.out
|
- pkgs.gzip
|
||||||
- coreutils-full
|
- pkgs.gnugrep
|
||||||
- curl
|
- pkgs.which
|
||||||
- findutils
|
- pkgs.curl
|
||||||
- gitMinimal
|
- pkgs.less
|
||||||
- gnugrep
|
- pkgs.wget
|
||||||
- gnutar
|
- pkgs.man
|
||||||
- gzip
|
- pkgs.cacert.out
|
||||||
- iana-etc
|
- pkgs.findutils
|
||||||
- less
|
|
||||||
- libxml2
|
|
||||||
- lix
|
|
||||||
- man
|
|
||||||
- openssh
|
|
||||||
- sqlite
|
|
||||||
- wget
|
|
||||||
- which
|
|
||||||
|
|
||||||
# Docker image with the latest development version of Lix
|
# Docker image with the latest development version of Lix
|
||||||
|
|
||||||
FIXME: There are not currently images of development versions of Lix. Tracking issue: https://git.lix.systems/lix-project/lix/issues/381
|
To get the latest image that was built by [Hydra](https://hydra.nixos.org) run
|
||||||
|
the following command:
|
||||||
You can build a Docker image from source yourself and copy it to either:
|
|
||||||
|
|
||||||
Podman: `nix run '.#dockerImage.copyTo' containers-storage:lix`
|
|
||||||
|
|
||||||
Docker: `nix run '.#dockerImage.copyToDockerDaemon'`
|
|
||||||
|
|
||||||
Then:
|
|
||||||
|
|
||||||
```console
|
```console
|
||||||
$ docker run -ti lix
|
$ curl -L https://hydra.nixos.org/job/nix/master/dockerImage.x86_64-linux/latest/download/1 | docker load
|
||||||
|
$ docker run -ti nix:2.5pre20211105
|
||||||
```
|
```
|
||||||
|
|
||||||
|
You can also build a Docker image from source yourself:
|
||||||
|
|
||||||
|
```console
|
||||||
|
$ nix build ./\#hydraJobs.dockerImage.x86_64-linux
|
||||||
|
$ docker load -i ./result/image.tar.gz
|
||||||
|
$ docker run -ti nix:2.5pre20211105
|
||||||
|
```
|
||||||
|
-->
|
||||||
|
|
148
docker.nix
148
docker.nix
|
@ -1,8 +1,5 @@
|
||||||
{
|
{
|
||||||
pkgs ? import <nixpkgs> { },
|
pkgs ? import <nixpkgs> { },
|
||||||
# Git commit ID, if available
|
|
||||||
lixRevision ? null,
|
|
||||||
nix2container,
|
|
||||||
lib ? pkgs.lib,
|
lib ? pkgs.lib,
|
||||||
name ? "lix",
|
name ? "lix",
|
||||||
tag ? "latest",
|
tag ? "latest",
|
||||||
|
@ -15,51 +12,26 @@
|
||||||
flake-registry ? null,
|
flake-registry ? null,
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
layerContents = with pkgs; [
|
defaultPkgs =
|
||||||
# pulls in glibc and openssl, about 60MB
|
with pkgs;
|
||||||
{ contents = [ coreutils-full ]; }
|
[
|
||||||
# some stuff that is low in the closure graph and small ish, mostly to make
|
nix
|
||||||
# incremental lix updates cheaper
|
|
||||||
{
|
|
||||||
contents = [
|
|
||||||
curl
|
|
||||||
libxml2
|
|
||||||
sqlite
|
|
||||||
];
|
|
||||||
}
|
|
||||||
# 50MB of git
|
|
||||||
{ contents = [ gitMinimal ]; }
|
|
||||||
# 144MB of nixpkgs
|
|
||||||
{
|
|
||||||
contents = [ channel ];
|
|
||||||
inProfile = false;
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
# These packages are left to be auto layered by nix2container, since it is
|
|
||||||
# less critical that they get layered sensibly and they tend to not be deps
|
|
||||||
# of anything in particular
|
|
||||||
autoLayered = with pkgs; [
|
|
||||||
bashInteractive
|
bashInteractive
|
||||||
|
coreutils-full
|
||||||
gnutar
|
gnutar
|
||||||
gzip
|
gzip
|
||||||
gnugrep
|
gnugrep
|
||||||
which
|
which
|
||||||
|
curl
|
||||||
less
|
less
|
||||||
wget
|
wget
|
||||||
man
|
man
|
||||||
cacert.out
|
cacert.out
|
||||||
findutils
|
findutils
|
||||||
iana-etc
|
iana-etc
|
||||||
|
git
|
||||||
openssh
|
openssh
|
||||||
nix
|
]
|
||||||
];
|
|
||||||
|
|
||||||
defaultPkgs =
|
|
||||||
lib.lists.flatten (
|
|
||||||
map (x: if !(x ? inProfile) || x.inProfile then x.contents else [ ]) layerContents
|
|
||||||
)
|
|
||||||
++ autoLayered
|
|
||||||
++ extraPkgs;
|
++ extraPkgs;
|
||||||
|
|
||||||
users =
|
users =
|
||||||
|
@ -167,17 +139,16 @@ let
|
||||||
))
|
))
|
||||||
+ "\n";
|
+ "\n";
|
||||||
|
|
||||||
nixpkgs = pkgs.path;
|
|
||||||
channel = pkgs.runCommand "channel-nixpkgs" { } ''
|
|
||||||
mkdir $out
|
|
||||||
${lib.optionalString bundleNixpkgs ''
|
|
||||||
ln -s ${nixpkgs} $out/nixpkgs
|
|
||||||
echo "[]" > $out/manifest.nix
|
|
||||||
''}
|
|
||||||
'';
|
|
||||||
|
|
||||||
baseSystem =
|
baseSystem =
|
||||||
let
|
let
|
||||||
|
nixpkgs = pkgs.path;
|
||||||
|
channel = pkgs.runCommand "channel-nixos" { inherit bundleNixpkgs; } ''
|
||||||
|
mkdir $out
|
||||||
|
if [ "$bundleNixpkgs" ]; then
|
||||||
|
ln -s ${nixpkgs} $out/nixpkgs
|
||||||
|
echo "[]" > $out/manifest.nix
|
||||||
|
fi
|
||||||
|
'';
|
||||||
rootEnv = pkgs.buildPackages.buildEnv {
|
rootEnv = pkgs.buildPackages.buildEnv {
|
||||||
name = "root-profile-env";
|
name = "root-profile-env";
|
||||||
paths = defaultPkgs;
|
paths = defaultPkgs;
|
||||||
|
@ -216,7 +187,7 @@ let
|
||||||
profile = pkgs.buildPackages.runCommand "user-environment" { } ''
|
profile = pkgs.buildPackages.runCommand "user-environment" { } ''
|
||||||
mkdir $out
|
mkdir $out
|
||||||
cp -a ${rootEnv}/* $out/
|
cp -a ${rootEnv}/* $out/
|
||||||
ln -sf ${manifest} $out/manifest.nix
|
ln -s ${manifest} $out/manifest.nix
|
||||||
'';
|
'';
|
||||||
flake-registry-path =
|
flake-registry-path =
|
||||||
if (flake-registry == null) then
|
if (flake-registry == null) then
|
||||||
|
@ -265,7 +236,6 @@ let
|
||||||
ln -s /nix/var/nix/profiles/share $out/usr/
|
ln -s /nix/var/nix/profiles/share $out/usr/
|
||||||
|
|
||||||
mkdir -p $out/nix/var/nix/gcroots
|
mkdir -p $out/nix/var/nix/gcroots
|
||||||
ln -s /nix/var/nix/profiles $out/nix/var/nix/gcroots/profiles
|
|
||||||
|
|
||||||
mkdir $out/tmp
|
mkdir $out/tmp
|
||||||
|
|
||||||
|
@ -278,14 +248,14 @@ let
|
||||||
mkdir -p $out/nix/var/nix/profiles/per-user/root
|
mkdir -p $out/nix/var/nix/profiles/per-user/root
|
||||||
|
|
||||||
ln -s ${profile} $out/nix/var/nix/profiles/default-1-link
|
ln -s ${profile} $out/nix/var/nix/profiles/default-1-link
|
||||||
ln -s /nix/var/nix/profiles/default-1-link $out/nix/var/nix/profiles/default
|
ln -s $out/nix/var/nix/profiles/default-1-link $out/nix/var/nix/profiles/default
|
||||||
ln -s /nix/var/nix/profiles/default $out/root/.nix-profile
|
ln -s /nix/var/nix/profiles/default $out/root/.nix-profile
|
||||||
|
|
||||||
ln -s ${channel} $out/nix/var/nix/profiles/per-user/root/channels-1-link
|
ln -s ${channel} $out/nix/var/nix/profiles/per-user/root/channels-1-link
|
||||||
ln -s /nix/var/nix/profiles/per-user/root/channels-1-link $out/nix/var/nix/profiles/per-user/root/channels
|
ln -s $out/nix/var/nix/profiles/per-user/root/channels-1-link $out/nix/var/nix/profiles/per-user/root/channels
|
||||||
|
|
||||||
mkdir -p $out/root/.nix-defexpr
|
mkdir -p $out/root/.nix-defexpr
|
||||||
ln -s /nix/var/nix/profiles/per-user/root/channels $out/root/.nix-defexpr/channels
|
ln -s $out/nix/var/nix/profiles/per-user/root/channels $out/root/.nix-defexpr/channels
|
||||||
echo "${channelURL} ${channelName}" > $out/root/.nix-channels
|
echo "${channelURL} ${channelName}" > $out/root/.nix-channels
|
||||||
|
|
||||||
mkdir -p $out/bin $out/usr/bin
|
mkdir -p $out/bin $out/usr/bin
|
||||||
|
@ -303,35 +273,21 @@ let
|
||||||
ln -s $globalFlakeRegistryPath $out/nix/var/nix/gcroots/auto/$rootName
|
ln -s $globalFlakeRegistryPath $out/nix/var/nix/gcroots/auto/$rootName
|
||||||
'')
|
'')
|
||||||
);
|
);
|
||||||
|
in
|
||||||
layers = builtins.foldl' (
|
pkgs.dockerTools.buildLayeredImageWithNixDb {
|
||||||
layersList: el:
|
|
||||||
let
|
|
||||||
layer = nix2container.buildLayer {
|
|
||||||
deps = el.contents;
|
|
||||||
layers = layersList;
|
|
||||||
};
|
|
||||||
in
|
|
||||||
layersList ++ [ layer ]
|
|
||||||
) [ ] layerContents;
|
|
||||||
|
|
||||||
image = nix2container.buildImage {
|
|
||||||
|
|
||||||
inherit name tag maxLayers;
|
inherit name tag maxLayers;
|
||||||
|
|
||||||
inherit layers;
|
contents = [ baseSystem ];
|
||||||
|
|
||||||
copyToRoot = [ baseSystem ];
|
extraCommands = ''
|
||||||
|
rm -rf nix-support
|
||||||
initializeNixDatabase = true;
|
ln -s /nix/var/nix/profiles nix/var/nix/gcroots/profiles
|
||||||
|
'';
|
||||||
perms = [
|
fakeRootCommands = ''
|
||||||
{
|
chmod 1777 tmp
|
||||||
path = baseSystem;
|
chmod 1777 var/tmp
|
||||||
regex = "(/var)?/tmp";
|
'';
|
||||||
mode = "1777";
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
Cmd = [ "/root/.nix-profile/bin/bash" ];
|
Cmd = [ "/root/.nix-profile/bin/bash" ];
|
||||||
|
@ -355,47 +311,5 @@ let
|
||||||
"NIX_SSL_CERT_FILE=/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt"
|
"NIX_SSL_CERT_FILE=/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt"
|
||||||
"NIX_PATH=/nix/var/nix/profiles/per-user/root/channels:/root/.nix-defexpr/channels"
|
"NIX_PATH=/nix/var/nix/profiles/per-user/root/channels:/root/.nix-defexpr/channels"
|
||||||
];
|
];
|
||||||
|
|
||||||
Labels = {
|
|
||||||
"org.opencontainers.image.title" = "Lix";
|
|
||||||
"org.opencontainers.image.source" = "https://git.lix.systems/lix-project/lix";
|
|
||||||
"org.opencontainers.image.vendor" = "Lix project";
|
|
||||||
"org.opencontainers.image.version" = pkgs.nix.version;
|
|
||||||
"org.opencontainers.image.description" = "Minimal Lix container image, with some batteries included.";
|
|
||||||
} // lib.optionalAttrs (lixRevision != null) { "org.opencontainers.image.revision" = lixRevision; };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
meta = {
|
|
||||||
description = "Docker image for Lix. This is built with nix2container; see that project's README for details";
|
|
||||||
longDescription = ''
|
|
||||||
Docker image for Lix, built with nix2container.
|
|
||||||
To copy it to your docker daemon, nix run .#dockerImage.copyToDockerDaemon
|
|
||||||
To copy it to podman, nix run .#dockerImage.copyTo containers-storage:lix
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
};
|
|
||||||
in
|
|
||||||
image
|
|
||||||
// {
|
|
||||||
# We don't ship the tarball as the default output because it is a strange thing to want imo
|
|
||||||
tarball =
|
|
||||||
pkgs.buildPackages.runCommand "docker-image-tarball-${pkgs.nix.version}"
|
|
||||||
{
|
|
||||||
nativeBuildInputs = [ pkgs.buildPackages.bubblewrap ];
|
|
||||||
meta.description = "Docker image tarball with Lix for ${pkgs.system}";
|
|
||||||
}
|
|
||||||
''
|
|
||||||
mkdir -p $out/nix-support
|
|
||||||
image=$out/image.tar
|
|
||||||
# bwrap for foolish temp dir selection code that forces /var/tmp:
|
|
||||||
# https://github.com/containers/skopeo.git/blob/60ee543f7f7c242f46cc3a7541d9ac8ab1c89168/vendor/github.com/containers/image/v5/internal/tmpdir/tmpdir.go#L15-L18
|
|
||||||
mkdir -p $TMPDIR/fake-var/tmp
|
|
||||||
args=(--unshare-user --bind "$TMPDIR/fake-var" /var)
|
|
||||||
for dir in /*; do
|
|
||||||
args+=(--dev-bind "/$dir" "/$dir")
|
|
||||||
done
|
|
||||||
bwrap ''${args[@]} -- ${lib.getExe image.copyTo} docker-archive:$image
|
|
||||||
gzip $image
|
|
||||||
echo "file binary-dist $image" >> $out/nix-support/hydra-build-products
|
|
||||||
'';
|
|
||||||
}
|
}
|
||||||
|
|
17
flake.lock
17
flake.lock
|
@ -16,22 +16,6 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nix2container": {
|
|
||||||
"flake": false,
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1712990762,
|
|
||||||
"narHash": "sha256-hO9W3w7NcnYeX8u8cleHiSpK2YJo7ecarFTUlbybl7k=",
|
|
||||||
"owner": "nlewo",
|
|
||||||
"repo": "nix2container",
|
|
||||||
"rev": "20aad300c925639d5d6cbe30013c8357ce9f2a2e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nlewo",
|
|
||||||
"repo": "nix2container",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1715123187,
|
"lastModified": 1715123187,
|
||||||
|
@ -83,7 +67,6 @@
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-compat": "flake-compat",
|
"flake-compat": "flake-compat",
|
||||||
"nix2container": "nix2container",
|
|
||||||
"nixpkgs": "nixpkgs",
|
"nixpkgs": "nixpkgs",
|
||||||
"nixpkgs-regression": "nixpkgs-regression",
|
"nixpkgs-regression": "nixpkgs-regression",
|
||||||
"pre-commit-hooks": "pre-commit-hooks"
|
"pre-commit-hooks": "pre-commit-hooks"
|
||||||
|
|
29
flake.nix
29
flake.nix
|
@ -8,10 +8,6 @@
|
||||||
url = "github:cachix/git-hooks.nix";
|
url = "github:cachix/git-hooks.nix";
|
||||||
flake = false;
|
flake = false;
|
||||||
};
|
};
|
||||||
nix2container = {
|
|
||||||
url = "github:nlewo/nix2container";
|
|
||||||
flake = false;
|
|
||||||
};
|
|
||||||
flake-compat = {
|
flake-compat = {
|
||||||
url = "github:edolstra/flake-compat";
|
url = "github:edolstra/flake-compat";
|
||||||
flake = false;
|
flake = false;
|
||||||
|
@ -24,7 +20,6 @@
|
||||||
nixpkgs,
|
nixpkgs,
|
||||||
nixpkgs-regression,
|
nixpkgs-regression,
|
||||||
pre-commit-hooks,
|
pre-commit-hooks,
|
||||||
nix2container,
|
|
||||||
flake-compat,
|
flake-compat,
|
||||||
}:
|
}:
|
||||||
|
|
||||||
|
@ -194,7 +189,7 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
nix = final.callPackage ./package.nix {
|
nix = final.callPackage ./package.nix {
|
||||||
inherit versionSuffix officialRelease;
|
inherit versionSuffix;
|
||||||
stdenv = currentStdenv;
|
stdenv = currentStdenv;
|
||||||
busybox-sandbox-shell = final.busybox-sandbox-shell or final.default-busybox-sandbox-shell;
|
busybox-sandbox-shell = final.busybox-sandbox-shell or final.default-busybox-sandbox-shell;
|
||||||
};
|
};
|
||||||
|
@ -212,6 +207,7 @@
|
||||||
overlays.default = overlayFor (p: p.stdenv);
|
overlays.default = overlayFor (p: p.stdenv);
|
||||||
|
|
||||||
hydraJobs = {
|
hydraJobs = {
|
||||||
|
|
||||||
# Binary package for various platforms.
|
# Binary package for various platforms.
|
||||||
build = forAllSystems (system: self.packages.${system}.nix);
|
build = forAllSystems (system: self.packages.${system}.nix);
|
||||||
|
|
||||||
|
@ -301,11 +297,6 @@
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
release-jobs = import ./releng/release-jobs.nix {
|
|
||||||
inherit (self) hydraJobs;
|
|
||||||
pkgs = nixpkgsFor.x86_64-linux.native;
|
|
||||||
};
|
|
||||||
|
|
||||||
# NOTE *do not* add fresh derivations to checks, always add them to
|
# NOTE *do not* add fresh derivations to checks, always add them to
|
||||||
# hydraJobs first (so CI will pick them up) and only link them here
|
# hydraJobs first (so CI will pick them up) and only link them here
|
||||||
checks = forAvailableSystems (
|
checks = forAvailableSystems (
|
||||||
|
@ -335,13 +326,19 @@
|
||||||
dockerImage =
|
dockerImage =
|
||||||
let
|
let
|
||||||
pkgs = nixpkgsFor.${system}.native;
|
pkgs = nixpkgsFor.${system}.native;
|
||||||
nix2container' = import nix2container { inherit pkgs system; };
|
image = import ./docker.nix {
|
||||||
in
|
|
||||||
import ./docker.nix {
|
|
||||||
inherit pkgs;
|
inherit pkgs;
|
||||||
nix2container = nix2container'.nix2container;
|
|
||||||
tag = pkgs.nix.version;
|
tag = pkgs.nix.version;
|
||||||
};
|
};
|
||||||
|
in
|
||||||
|
pkgs.runCommand "docker-image-tarball-${pkgs.nix.version}"
|
||||||
|
{ meta.description = "Docker image with Lix for ${system}"; }
|
||||||
|
''
|
||||||
|
mkdir -p $out/nix-support
|
||||||
|
image=$out/image.tar.gz
|
||||||
|
ln -s ${image} $image
|
||||||
|
echo "file binary-dist $image" >> $out/nix-support/hydra-build-products
|
||||||
|
'';
|
||||||
}
|
}
|
||||||
// builtins.listToAttrs (
|
// builtins.listToAttrs (
|
||||||
map (crossSystem: {
|
map (crossSystem: {
|
||||||
|
@ -364,7 +361,7 @@
|
||||||
pkgs: stdenv:
|
pkgs: stdenv:
|
||||||
let
|
let
|
||||||
nix = pkgs.callPackage ./package.nix {
|
nix = pkgs.callPackage ./package.nix {
|
||||||
inherit stdenv officialRelease versionSuffix;
|
inherit stdenv versionSuffix;
|
||||||
busybox-sandbox-shell = pkgs.busybox-sandbox-shell or pkgs.default-busybox-sandbox;
|
busybox-sandbox-shell = pkgs.busybox-sandbox-shell or pkgs.default-busybox-sandbox;
|
||||||
internalApiDocs = true;
|
internalApiDocs = true;
|
||||||
};
|
};
|
||||||
|
|
|
@ -143,7 +143,7 @@ def run_on_dir(author_info: AuthorInfoDB, d):
|
||||||
|
|
||||||
for category in CATEGORIES:
|
for category in CATEGORIES:
|
||||||
if entries[category]:
|
if entries[category]:
|
||||||
print('\n##', category)
|
print('\n#', category)
|
||||||
do_category(author_info, entries[category])
|
do_category(author_info, entries[category])
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
|
@ -39,7 +39,6 @@
|
||||||
pkg-config,
|
pkg-config,
|
||||||
python3,
|
python3,
|
||||||
rapidcheck,
|
rapidcheck,
|
||||||
skopeo,
|
|
||||||
sqlite,
|
sqlite,
|
||||||
toml11,
|
toml11,
|
||||||
util-linuxMinimal ? utillinuxMinimal,
|
util-linuxMinimal ? utillinuxMinimal,
|
||||||
|
@ -387,8 +386,6 @@ stdenv.mkDerivation (finalAttrs: {
|
||||||
passthru = {
|
passthru = {
|
||||||
inherit (__forDefaults) boehmgc-nix editline-lix build-release-notes;
|
inherit (__forDefaults) boehmgc-nix editline-lix build-release-notes;
|
||||||
|
|
||||||
inherit officialRelease;
|
|
||||||
|
|
||||||
# The collection of dependency logic for this derivation is complicated enough that
|
# The collection of dependency logic for this derivation is complicated enough that
|
||||||
# it's easier to parameterize the devShell off an already called package.nix.
|
# it's easier to parameterize the devShell off an already called package.nix.
|
||||||
mkDevShell =
|
mkDevShell =
|
||||||
|
@ -417,8 +414,6 @@ stdenv.mkDerivation (finalAttrs: {
|
||||||
p: [
|
p: [
|
||||||
p.yapf
|
p.yapf
|
||||||
p.python-frontmatter
|
p.python-frontmatter
|
||||||
p.requests
|
|
||||||
p.xdg-base-dirs
|
|
||||||
(p.toPythonModule xonsh-unwrapped)
|
(p.toPythonModule xonsh-unwrapped)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -455,8 +450,6 @@ stdenv.mkDerivation (finalAttrs: {
|
||||||
lib.optional (stdenv.cc.isClang && hostPlatform == buildPlatform) clang-tools_llvm
|
lib.optional (stdenv.cc.isClang && hostPlatform == buildPlatform) clang-tools_llvm
|
||||||
++ [
|
++ [
|
||||||
pythonEnv
|
pythonEnv
|
||||||
# docker image tool
|
|
||||||
skopeo
|
|
||||||
just
|
just
|
||||||
nixfmt
|
nixfmt
|
||||||
# Load-bearing order. Must come before clang-unwrapped below, but after clang_tools above.
|
# Load-bearing order. Must come before clang-unwrapped below, but after clang_tools above.
|
||||||
|
|
132
releng/README.md
132
releng/README.md
|
@ -1,132 +0,0 @@
|
||||||
# Release engineering
|
|
||||||
|
|
||||||
This directory contains the release engineering scripts for Lix.
|
|
||||||
|
|
||||||
## Release process
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
* FIXME: validation via misc tests in nixpkgs, among other things? What other
|
|
||||||
validation do we need before we can actually release?
|
|
||||||
* Have a release post ready to go as a PR on the website repo.
|
|
||||||
* No [release-blocker bugs][release-blockers].
|
|
||||||
|
|
||||||
[release-blockers]: https://git.lix.systems/lix-project/lix/issues?q=&type=all&sort=&labels=145&state=open&milestone=0&project=0&assignee=0&poster=0
|
|
||||||
|
|
||||||
### Process
|
|
||||||
|
|
||||||
The following process can be done either against the staging environment or the
|
|
||||||
live environment.
|
|
||||||
|
|
||||||
For staging, the buckets are `staging-releases`, `staging-cache`, etc.
|
|
||||||
|
|
||||||
FIXME: obtainment of signing key for signing cache paths?
|
|
||||||
|
|
||||||
First, we prepare the release. `python -m releng prepare` is used for this.
|
|
||||||
|
|
||||||
* Gather everything in `doc/manual/rl-next` and put it in
|
|
||||||
`doc/manual/src/release-notes/rl-MAJOR.md`.
|
|
||||||
|
|
||||||
Then we tag the release with `python -m releng tag`:
|
|
||||||
|
|
||||||
* Git HEAD is detached.
|
|
||||||
* `officialRelease = true` is set in `flake.nix`, this is committed, and a
|
|
||||||
release is tagged.
|
|
||||||
* The tag is merged back into the last branch (either `main` for new releases
|
|
||||||
or `release-MAJOR` for maintenance releases) with `git merge -s ours VERSION`
|
|
||||||
creating a history link but ignoring the tree of the release tag.
|
|
||||||
* Git HEAD is once again detached onto the release tag.
|
|
||||||
|
|
||||||
Then, we build the release artifacts with `python -m releng build`:
|
|
||||||
|
|
||||||
* Source tarball is generated with `git archive`, then checksummed.
|
|
||||||
* Manifest for `nix upgrade-nix` is produced and put in `s3://releases` at
|
|
||||||
`/manifest.nix` and `/lix/lix-VERSION`.
|
|
||||||
* Release is built: `hydraJobs.binaryTarball` jobs are built, and joined into a
|
|
||||||
derivation that depends on all of them and adds checksum files. This and the
|
|
||||||
sources go into `s3://releases/lix/lix-VERSION`.
|
|
||||||
|
|
||||||
At this point we have a `release/artifacts` and `release/manual` directory
|
|
||||||
which are ready to publish, and tags ready for publication. No keys are
|
|
||||||
required to do this part.
|
|
||||||
|
|
||||||
Next, we do the publication with `python -m releng upload`:
|
|
||||||
|
|
||||||
* Artifacts for this release are uploaded:
|
|
||||||
* s3://releases/manifest.nix, changing the default version of Lix for
|
|
||||||
`nix upgrade-nix`.
|
|
||||||
* s3://releases/lix/lix-VERSION/ gets the following contents
|
|
||||||
* Binary tarballs
|
|
||||||
* Docs: `manual/` (FIXME: should we actually do this? what about putting it
|
|
||||||
on docs.lix.systems? I think doing both is correct, since the Web site
|
|
||||||
should not be an archive of random old manuals)
|
|
||||||
* Docs as tarball in addition to web.
|
|
||||||
* Source tarball
|
|
||||||
* Docker image (FIXME: upload to forgejo registry and github registry [in the future][upload-docker])
|
|
||||||
* s3://docs/manual/lix/MAJOR
|
|
||||||
* s3://docs/manual/lix/stable
|
|
||||||
|
|
||||||
* The tag is uploaded to the remote repo.
|
|
||||||
* **Manually** build the installer using the scripts in the installer repo and upload.
|
|
||||||
|
|
||||||
FIXME: This currently requires a local Apple Macintosh® aarch64 computer, but
|
|
||||||
we could possibly automate doing it from the aarch64-darwin builder.
|
|
||||||
* **Manually** Push the main/release branch directly to gerrit.
|
|
||||||
* If this is a new major release, branch-off to `release-MAJOR` and push *that* branch
|
|
||||||
directly to gerrit as well (FIXME: special creds for doing this as a service
|
|
||||||
account so we don't need to have the gerrit perms to shoot ourselves in the
|
|
||||||
foot by default because pushing to main is bad?).
|
|
||||||
|
|
||||||
FIXME: automate branch-off to `release-*` branch.
|
|
||||||
* **Manually** (FIXME?) switch back to the release branch, which now has the
|
|
||||||
correct revision.
|
|
||||||
* Post!!
|
|
||||||
* Merge release blog post to [lix-website].
|
|
||||||
* Toot about it! https://chaos.social/@lix_project
|
|
||||||
* Tweet about it! https://twitter.com/lixproject
|
|
||||||
|
|
||||||
[lix-website]: https://git.lix.systems/lix-project/lix-website
|
|
||||||
|
|
||||||
[upload-docker]: https://git.lix.systems/lix-project/lix/issues/252
|
|
||||||
|
|
||||||
### Installer
|
|
||||||
|
|
||||||
The installer is cross-built to several systems from a Mac using
|
|
||||||
`build-all.xsh` and `upload-to-lix.xsh` in the installer repo (FIXME: currently
|
|
||||||
at least; maybe this should be moved here?) .
|
|
||||||
|
|
||||||
It installs a binary tarball (FIXME: [it should be taught to substitute from
|
|
||||||
cache instead][installer-substitute])
|
|
||||||
from some URL; this is the `hydraJobs.binaryTarball`. The default URLs differ
|
|
||||||
by architecture and are [configured here][tarball-urls].
|
|
||||||
|
|
||||||
[installer-substitute]: https://git.lix.systems/lix-project/lix-installer/issues/13
|
|
||||||
[tarball-urls]: https://git.lix.systems/lix-project/lix-installer/src/commit/693592ed10d421a885bec0a9dd45e87ab87eb90a/src/settings.rs#L14-L28
|
|
||||||
|
|
||||||
## Infrastructure summary
|
|
||||||
|
|
||||||
* releases.lix.systems (`s3://releases`):
|
|
||||||
* Each release gets a directory: https://releases.lix.systems/lix/lix-2.90-beta.1
|
|
||||||
* Binary tarballs: `nix-2.90.0-beta.1-x86_64-linux.tar.xz`, from `hydraJobs.binaryTarball`
|
|
||||||
* Manifest: `manifest.nix`, an attrset of the store paths by architecture.
|
|
||||||
* Manifest for `nix upgrade-nix` to the latest release at `/manifest.nix`.
|
|
||||||
* cache.lix.systems (`s3://cache`):
|
|
||||||
* Receives all artifacts for released versions of Lix; is a plain HTTP binary cache.
|
|
||||||
* install.lix.systems (`s3://install`):
|
|
||||||
```
|
|
||||||
~ » aws s3 ls s3://install/lix/
|
|
||||||
PRE lix-2.90-beta.0/
|
|
||||||
PRE lix-2.90-beta.1/
|
|
||||||
PRE lix-2.90.0pre20240411/
|
|
||||||
PRE lix-2.90.0pre20240412/
|
|
||||||
2024-05-05 18:59:11 6707344 lix-installer-aarch64-darwin
|
|
||||||
2024-05-05 18:59:16 7479768 lix-installer-aarch64-linux
|
|
||||||
2024-05-05 18:59:14 7982208 lix-installer-x86_64-darwin
|
|
||||||
2024-05-05 18:59:17 8978352 lix-installer-x86_64-linux
|
|
||||||
|
|
||||||
~ » aws s3 ls s3://install/lix/lix-2.90-beta.1/
|
|
||||||
2024-05-05 18:59:01 6707344 lix-installer-aarch64-darwin
|
|
||||||
2024-05-05 18:59:06 7479768 lix-installer-aarch64-linux
|
|
||||||
2024-05-05 18:59:03 7982208 lix-installer-x86_64-darwin
|
|
||||||
2024-05-05 18:59:07 8978352 lix-installer-x86_64-linux
|
|
||||||
```
|
|
|
@ -1,39 +0,0 @@
|
||||||
from xonsh.main import setup
|
|
||||||
setup()
|
|
||||||
del setup
|
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from . import environment
|
|
||||||
from . import create_release
|
|
||||||
from . import keys
|
|
||||||
from . import version
|
|
||||||
from . import cli
|
|
||||||
from . import docker
|
|
||||||
from . import docker_assemble
|
|
||||||
from . import gitutils
|
|
||||||
|
|
||||||
rootLogger = logging.getLogger()
|
|
||||||
rootLogger.setLevel(logging.DEBUG)
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
log.setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
fmt = logging.Formatter('{asctime} {levelname} {name}: {message}',
|
|
||||||
datefmt='%b %d %H:%M:%S',
|
|
||||||
style='{')
|
|
||||||
|
|
||||||
if not any(isinstance(h, logging.StreamHandler) for h in rootLogger.handlers):
|
|
||||||
hand = logging.StreamHandler()
|
|
||||||
hand.setFormatter(fmt)
|
|
||||||
rootLogger.addHandler(hand)
|
|
||||||
|
|
||||||
def reload():
|
|
||||||
import importlib
|
|
||||||
importlib.reload(environment)
|
|
||||||
importlib.reload(create_release)
|
|
||||||
importlib.reload(keys)
|
|
||||||
importlib.reload(version)
|
|
||||||
importlib.reload(cli)
|
|
||||||
importlib.reload(docker)
|
|
||||||
importlib.reload(docker_assemble)
|
|
||||||
importlib.reload(gitutils)
|
|
|
@ -1,3 +0,0 @@
|
||||||
from . import cli
|
|
||||||
|
|
||||||
cli.main()
|
|
112
releng/cli.py
112
releng/cli.py
|
@ -1,112 +0,0 @@
|
||||||
from . import create_release
|
|
||||||
from . import docker
|
|
||||||
from .environment import RelengEnvironment
|
|
||||||
from . import environment
|
|
||||||
import functools
|
|
||||||
import argparse
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def do_build(args):
|
|
||||||
if args.target == 'all':
|
|
||||||
create_release.build_artifacts(no_check_git=args.no_check_git)
|
|
||||||
elif args.target == 'manual':
|
|
||||||
eval_result = create_release.eval_jobs()
|
|
||||||
create_release.build_manual(eval_result)
|
|
||||||
else:
|
|
||||||
raise ValueError('invalid target, unreachable')
|
|
||||||
|
|
||||||
|
|
||||||
def do_tag(args):
|
|
||||||
create_release.do_tag_merge(force_tag=args.force_tag,
|
|
||||||
no_check_git=args.no_check_git)
|
|
||||||
|
|
||||||
|
|
||||||
def do_upload(env: RelengEnvironment, args):
|
|
||||||
create_release.setup_creds(env)
|
|
||||||
if args.target == 'all':
|
|
||||||
docker.check_all_logins(env)
|
|
||||||
create_release.upload_artifacts(env,
|
|
||||||
force_push_tag=args.force_push_tag,
|
|
||||||
noconfirm=args.noconfirm,
|
|
||||||
no_check_git=args.no_check_git)
|
|
||||||
elif args.target == 'manual':
|
|
||||||
create_release.upload_manual(env)
|
|
||||||
else:
|
|
||||||
raise ValueError('invalid target, unreachable')
|
|
||||||
|
|
||||||
|
|
||||||
def do_prepare(args):
|
|
||||||
create_release.prepare_release_notes()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
ap = argparse.ArgumentParser(description='*Lix ur release engineering*')
|
|
||||||
|
|
||||||
def fail(args):
|
|
||||||
ap.print_usage()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
ap.set_defaults(cmd=fail)
|
|
||||||
|
|
||||||
sps = ap.add_subparsers()
|
|
||||||
|
|
||||||
prepare = sps.add_parser(
|
|
||||||
'prepare',
|
|
||||||
help='Prepares for a release by moving the release notes over.')
|
|
||||||
prepare.set_defaults(cmd=do_prepare)
|
|
||||||
|
|
||||||
tag = sps.add_parser(
|
|
||||||
'tag',
|
|
||||||
help=
|
|
||||||
'Create the tag for the current release in .version and merge it back to the current branch, then switch to it'
|
|
||||||
)
|
|
||||||
tag.add_argument('--no-check-git',
|
|
||||||
action='store_true',
|
|
||||||
help="Don't check git state before tagging. For testing.")
|
|
||||||
tag.add_argument('--force-tag',
|
|
||||||
action='store_true',
|
|
||||||
help='Overwrite the existing tag. For testing.')
|
|
||||||
tag.set_defaults(cmd=do_tag)
|
|
||||||
|
|
||||||
build = sps.add_parser(
|
|
||||||
'build',
|
|
||||||
help=
|
|
||||||
'Build an artifacts/ directory with the things that would be released')
|
|
||||||
build.add_argument(
|
|
||||||
'--no-check-git',
|
|
||||||
action='store_true',
|
|
||||||
help="Don't check git state before building. For testing.")
|
|
||||||
build.add_argument('--target',
|
|
||||||
choices=['manual', 'all'],
|
|
||||||
help='Whether to build everything or just the manual')
|
|
||||||
build.set_defaults(cmd=do_build)
|
|
||||||
|
|
||||||
upload = sps.add_parser(
|
|
||||||
'upload', help='Upload artifacts to cache and releases bucket')
|
|
||||||
upload.add_argument(
|
|
||||||
'--no-check-git',
|
|
||||||
action='store_true',
|
|
||||||
help="Don't check git state before uploading. For testing.")
|
|
||||||
upload.add_argument('--force-push-tag',
|
|
||||||
action='store_true',
|
|
||||||
help='Force push the tag. For testing.')
|
|
||||||
upload.add_argument(
|
|
||||||
'--target',
|
|
||||||
choices=['manual', 'all'],
|
|
||||||
default='all',
|
|
||||||
help='Whether to upload a release or just the nightly/otherwise manual'
|
|
||||||
)
|
|
||||||
upload.add_argument(
|
|
||||||
'--noconfirm',
|
|
||||||
action='store_true',
|
|
||||||
help="Don't ask for confirmation. For testing/automation.")
|
|
||||||
upload.add_argument('--environment',
|
|
||||||
choices=list(environment.ENVIRONMENTS.keys()),
|
|
||||||
default='staging',
|
|
||||||
help='Environment to release to')
|
|
||||||
upload.set_defaults(cmd=lambda args: do_upload(
|
|
||||||
environment.ENVIRONMENTS[args.environment], args))
|
|
||||||
|
|
||||||
args = ap.parse_args()
|
|
||||||
args.cmd(args)
|
|
|
@ -1,317 +0,0 @@
|
||||||
import json
|
|
||||||
import subprocess
|
|
||||||
import itertools
|
|
||||||
import textwrap
|
|
||||||
from pathlib import Path
|
|
||||||
import tempfile
|
|
||||||
import hashlib
|
|
||||||
import datetime
|
|
||||||
from . import environment
|
|
||||||
from .environment import RelengEnvironment
|
|
||||||
from . import keys
|
|
||||||
from . import docker
|
|
||||||
from .version import VERSION, RELEASE_NAME, MAJOR
|
|
||||||
from .gitutils import verify_are_on_tag, git_preconditions
|
|
||||||
|
|
||||||
$RAISE_SUBPROC_ERROR = True
|
|
||||||
$XONSH_SHOW_TRACEBACK = True
|
|
||||||
|
|
||||||
GCROOTS_DIR = Path('./release/gcroots')
|
|
||||||
BUILT_GCROOTS_DIR = Path('./release/gcroots-build')
|
|
||||||
DRVS_TXT = Path('./release/drvs.txt')
|
|
||||||
ARTIFACTS = Path('./release/artifacts')
|
|
||||||
MANUAL = Path('./release/manual')
|
|
||||||
|
|
||||||
RELENG_MSG = "Release created with releng/create_release.xsh"
|
|
||||||
|
|
||||||
BUILD_CORES = 16
|
|
||||||
MAX_JOBS = 2
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
RELEASE_SYSTEMS = ["x86_64-linux"]
|
|
||||||
|
|
||||||
|
|
||||||
def setup_creds(env: RelengEnvironment):
|
|
||||||
key = keys.get_ephemeral_key(env)
|
|
||||||
$AWS_SECRET_ACCESS_KEY = key.secret_key
|
|
||||||
$AWS_ACCESS_KEY_ID = key.id
|
|
||||||
$AWS_DEFAULT_REGION = 'garage'
|
|
||||||
$AWS_ENDPOINT_URL = environment.S3_ENDPOINT
|
|
||||||
|
|
||||||
|
|
||||||
def official_release_commit_tag(force_tag=False):
|
|
||||||
print('[+] Setting officialRelease in flake.nix and tagging')
|
|
||||||
prev_branch = $(git symbolic-ref --short HEAD).strip()
|
|
||||||
|
|
||||||
git switch --detach
|
|
||||||
sed -i 's/officialRelease = false/officialRelease = true/' flake.nix
|
|
||||||
git add flake.nix
|
|
||||||
message = f'release: {VERSION} "{RELEASE_NAME}"\n\nRelease produced with releng/create_release.xsh'
|
|
||||||
git commit -m @(message)
|
|
||||||
git tag @(['-f'] if force_tag else []) -a -m @(message) @(VERSION)
|
|
||||||
|
|
||||||
return prev_branch
|
|
||||||
|
|
||||||
|
|
||||||
def merge_to_release(prev_branch):
|
|
||||||
git switch @(prev_branch)
|
|
||||||
# Create a merge back into the release branch so that git tools understand
|
|
||||||
# that the release branch contains the tag, without the release commit
|
|
||||||
# actually influencing the tree.
|
|
||||||
merge_msg = textwrap.dedent("""\
|
|
||||||
release: merge release {VERSION} back to mainline
|
|
||||||
|
|
||||||
This merge commit returns to the previous state prior to the release but leaves the tag in the branch history.
|
|
||||||
{RELENG_MSG}
|
|
||||||
""").format(VERSION=VERSION, RELENG_MSG=RELENG_MSG)
|
|
||||||
git merge -m @(merge_msg) -s ours @(VERSION)
|
|
||||||
|
|
||||||
|
|
||||||
def realise(paths: list[str]):
|
|
||||||
args = [
|
|
||||||
'--realise',
|
|
||||||
'--max-jobs',
|
|
||||||
MAX_JOBS,
|
|
||||||
'--cores',
|
|
||||||
BUILD_CORES,
|
|
||||||
'--log-format',
|
|
||||||
'bar-with-logs',
|
|
||||||
'--add-root',
|
|
||||||
BUILT_GCROOTS_DIR
|
|
||||||
]
|
|
||||||
nix-store @(args) @(paths)
|
|
||||||
|
|
||||||
|
|
||||||
def eval_jobs():
|
|
||||||
nej_output = $(nix-eval-jobs --workers 4 --gc-roots-dir @(GCROOTS_DIR) --force-recurse --flake '.#release-jobs')
|
|
||||||
return [x for x in (json.loads(s) for s in nej_output.strip().split('\n'))
|
|
||||||
if x['system'] in RELEASE_SYSTEMS
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
def upload_drv_paths_and_outputs(env: RelengEnvironment, paths: list[str]):
|
|
||||||
proc = subprocess.Popen([
|
|
||||||
'nix',
|
|
||||||
'copy',
|
|
||||||
'-v',
|
|
||||||
'--to',
|
|
||||||
env.cache_store_uri(),
|
|
||||||
'--stdin',
|
|
||||||
],
|
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
env=__xonsh__.env.detype(),
|
|
||||||
)
|
|
||||||
|
|
||||||
proc.stdin.write('\n'.join(itertools.chain(paths, x + '^*' for x in paths)).encode())
|
|
||||||
proc.stdin.close()
|
|
||||||
rv = proc.wait()
|
|
||||||
if rv != 0:
|
|
||||||
raise subprocess.CalledProcessError(rv, proc.args)
|
|
||||||
|
|
||||||
|
|
||||||
def make_manifest(eval_result):
|
|
||||||
manifest = {vs['system']: vs['outputs']['out'] for vs in eval_result}
|
|
||||||
def manifest_line(system, out):
|
|
||||||
return f' {system} = "{out}";'
|
|
||||||
|
|
||||||
manifest_text = textwrap.dedent("""\
|
|
||||||
# This file was generated by releng/create_release.xsh in Lix
|
|
||||||
{{
|
|
||||||
{lines}
|
|
||||||
}}
|
|
||||||
""").format(lines='\n'.join(manifest_line(s, p) for (s, p) in manifest.items()))
|
|
||||||
|
|
||||||
return manifest_text
|
|
||||||
|
|
||||||
|
|
||||||
def make_git_tarball(to: Path):
|
|
||||||
git archive --verbose --prefix=lix-@(VERSION)/ --format=tar.gz -o @(to) @(VERSION)
|
|
||||||
|
|
||||||
|
|
||||||
def confirm(prompt, expected):
|
|
||||||
resp = input(prompt)
|
|
||||||
|
|
||||||
if resp != expected:
|
|
||||||
raise ValueError('Unconfirmed')
|
|
||||||
|
|
||||||
|
|
||||||
def sha256_file(f: Path):
|
|
||||||
hasher = hashlib.sha256()
|
|
||||||
|
|
||||||
with open(f, 'rb') as h:
|
|
||||||
while data := h.read(1024 * 1024):
|
|
||||||
hasher.update(data)
|
|
||||||
|
|
||||||
return hasher.hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def make_artifacts_dir(eval_result, d: Path):
|
|
||||||
d.mkdir(exist_ok=True, parents=True)
|
|
||||||
version_dir = d / 'lix' / f'lix-{VERSION}'
|
|
||||||
version_dir.mkdir(exist_ok=True, parents=True)
|
|
||||||
|
|
||||||
tarballs_drv = next(p for p in eval_result if p['attr'] == 'tarballs')
|
|
||||||
cp --no-preserve=mode -r @(tarballs_drv['outputs']['out'])/* @(version_dir)
|
|
||||||
|
|
||||||
# FIXME: upgrade-nix searches for manifest.nix at root, which is rather annoying
|
|
||||||
with open(d / 'manifest.nix', 'w') as h:
|
|
||||||
h.write(make_manifest(eval_result))
|
|
||||||
|
|
||||||
with open(version_dir / 'manifest.nix', 'w') as h:
|
|
||||||
h.write(make_manifest(eval_result))
|
|
||||||
|
|
||||||
print('[+] Make sources tarball')
|
|
||||||
|
|
||||||
filename = f'lix-{VERSION}.tar.gz'
|
|
||||||
git_tarball = version_dir / filename
|
|
||||||
make_git_tarball(git_tarball)
|
|
||||||
|
|
||||||
file_hash = sha256_file(git_tarball)
|
|
||||||
|
|
||||||
print(f'Hash: {file_hash}')
|
|
||||||
with open(version_dir / f'{filename}.sha256', 'w') as h:
|
|
||||||
h.write(file_hash)
|
|
||||||
|
|
||||||
|
|
||||||
def prepare_release_notes():
|
|
||||||
print('[+] Preparing release notes')
|
|
||||||
RELEASE_NOTES_PATH = Path('doc/manual/rl-next')
|
|
||||||
|
|
||||||
if RELEASE_NOTES_PATH.isdir():
|
|
||||||
notes_body = subprocess.check_output(['build-release-notes', '--change-authors', 'doc/manual/change-authors.yml', 'doc/manual/rl-next']).decode()
|
|
||||||
else:
|
|
||||||
# I guess nobody put release notes on their changes?
|
|
||||||
print('[-] Warning: seemingly missing any release notes, not worrying about it')
|
|
||||||
notes_body = ''
|
|
||||||
|
|
||||||
rl_path = Path(f'doc/manual/src/release-notes/rl-{MAJOR}.md')
|
|
||||||
|
|
||||||
existing_rl = ''
|
|
||||||
try:
|
|
||||||
with open(rl_path, 'r') as fh:
|
|
||||||
existing_rl = fh.read()
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
date = datetime.datetime.now().strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
minor_header = f'# Lix {VERSION} ({date})'
|
|
||||||
|
|
||||||
header = f'# Lix {MAJOR} "{RELEASE_NAME}"'
|
|
||||||
if existing_rl.startswith(header):
|
|
||||||
# strip the header off for minor releases
|
|
||||||
lines = existing_rl.splitlines()
|
|
||||||
header = lines[0]
|
|
||||||
existing_rl = '\n'.join(lines[1:])
|
|
||||||
else:
|
|
||||||
header += f' ({date})\n\n'
|
|
||||||
|
|
||||||
header += '\n' + minor_header + '\n'
|
|
||||||
|
|
||||||
notes = header
|
|
||||||
notes += notes_body
|
|
||||||
notes += "\n\n"
|
|
||||||
notes += existing_rl
|
|
||||||
|
|
||||||
# make pre-commit happy about one newline
|
|
||||||
notes = notes.rstrip()
|
|
||||||
notes += "\n"
|
|
||||||
|
|
||||||
with open(rl_path, 'w') as fh:
|
|
||||||
fh.write(notes)
|
|
||||||
|
|
||||||
commit_msg = textwrap.dedent("""\
|
|
||||||
release: release notes for {VERSION}
|
|
||||||
|
|
||||||
{RELENG_MSG}
|
|
||||||
""").format(VERSION=VERSION, RELENG_MSG=RELENG_MSG)
|
|
||||||
|
|
||||||
git add @(rl_path)
|
|
||||||
git rm doc/manual/rl-next/*.md
|
|
||||||
|
|
||||||
git commit -m @(commit_msg)
|
|
||||||
|
|
||||||
|
|
||||||
def upload_artifacts(env: RelengEnvironment, noconfirm=False, no_check_git=False, force_push_tag=False):
|
|
||||||
if not no_check_git:
|
|
||||||
verify_are_on_tag()
|
|
||||||
git_preconditions()
|
|
||||||
assert 'AWS_SECRET_ACCESS_KEY' in __xonsh__.env
|
|
||||||
|
|
||||||
tree @(ARTIFACTS)
|
|
||||||
|
|
||||||
env_part = f'environment {env.name}'
|
|
||||||
not noconfirm and confirm(
|
|
||||||
f'Would you like to release {ARTIFACTS} as {VERSION} in {env.colour(env_part)}? Type "I want to release this to {env.name}" to confirm\n',
|
|
||||||
f'I want to release this to {env.name}'
|
|
||||||
)
|
|
||||||
|
|
||||||
docker_images = list((ARTIFACTS / f'lix/lix-{VERSION}').glob(f'lix-{VERSION}-docker-image-*.tar.gz'))
|
|
||||||
assert docker_images
|
|
||||||
|
|
||||||
print('[+] Upload to cache')
|
|
||||||
with open(DRVS_TXT) as fh:
|
|
||||||
upload_drv_paths_and_outputs(env, [x.strip() for x in fh.readlines() if x])
|
|
||||||
|
|
||||||
print('[+] Upload docker images')
|
|
||||||
for target in env.docker_targets:
|
|
||||||
docker.upload_docker_images(target, docker_images)
|
|
||||||
|
|
||||||
print('[+] Upload to release bucket')
|
|
||||||
aws s3 cp --recursive @(ARTIFACTS)/ @(env.releases_bucket)/
|
|
||||||
print('[+] Upload manual')
|
|
||||||
upload_manual(env)
|
|
||||||
|
|
||||||
print('[+] git push tag')
|
|
||||||
git push @(['-f'] if force_push_tag else []) @(env.git_repo) f'{VERSION}:refs/tags/{VERSION}'
|
|
||||||
|
|
||||||
|
|
||||||
def do_tag_merge(force_tag=False, no_check_git=False):
|
|
||||||
if not no_check_git:
|
|
||||||
git_preconditions()
|
|
||||||
prev_branch = official_release_commit_tag(force_tag=force_tag)
|
|
||||||
merge_to_release(prev_branch)
|
|
||||||
git switch --detach @(VERSION)
|
|
||||||
|
|
||||||
|
|
||||||
def build_manual(eval_result):
|
|
||||||
manual = next(x['outputs']['doc'] for x in eval_result if x['attr'] == 'build.x86_64-linux')
|
|
||||||
print('[+] Building manual')
|
|
||||||
realise([manual])
|
|
||||||
|
|
||||||
cp --no-preserve=mode -vr @(manual)/share/doc/nix @(MANUAL)
|
|
||||||
|
|
||||||
|
|
||||||
def upload_manual(env: RelengEnvironment):
|
|
||||||
stable = json.loads($(nix eval --json '.#nix.officialRelease'))
|
|
||||||
if stable:
|
|
||||||
version = MAJOR
|
|
||||||
else:
|
|
||||||
version = 'nightly'
|
|
||||||
|
|
||||||
print('[+] aws s3 sync manual')
|
|
||||||
aws s3 sync @(MANUAL)/ @(env.docs_bucket)/manual/lix/@(version)/
|
|
||||||
if stable:
|
|
||||||
aws s3 sync @(MANUAL)/ @(env.docs_bucket)/manual/lix/stable/
|
|
||||||
|
|
||||||
|
|
||||||
def build_artifacts(no_check_git=False):
|
|
||||||
rm -rf release/
|
|
||||||
if not no_check_git:
|
|
||||||
verify_are_on_tag()
|
|
||||||
git_preconditions()
|
|
||||||
|
|
||||||
print('[+] Evaluating')
|
|
||||||
eval_result = eval_jobs()
|
|
||||||
drv_paths = [x['drvPath'] for x in eval_result]
|
|
||||||
|
|
||||||
print('[+] Building')
|
|
||||||
realise(drv_paths)
|
|
||||||
build_manual(eval_result)
|
|
||||||
|
|
||||||
with open(DRVS_TXT, 'w') as fh:
|
|
||||||
# don't bother putting the release tarballs themselves because they are duplicate and huge
|
|
||||||
fh.write('\n'.join(x['drvPath'] for x in eval_result if x['attr'] != 'lix-release-tarballs'))
|
|
||||||
|
|
||||||
make_artifacts_dir(eval_result, ARTIFACTS)
|
|
||||||
print(f'[+] Done! See {ARTIFACTS}')
|
|
|
@ -1,74 +0,0 @@
|
||||||
import json
|
|
||||||
import logging
|
|
||||||
from pathlib import Path
|
|
||||||
import tempfile
|
|
||||||
|
|
||||||
import requests
|
|
||||||
|
|
||||||
from .environment import DockerTarget, RelengEnvironment
|
|
||||||
from .version import VERSION, MAJOR
|
|
||||||
from . import gitutils
|
|
||||||
from .docker_assemble import Registry, OCIIndex, OCIIndexItem
|
|
||||||
from . import docker_assemble
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
log.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
def check_all_logins(env: RelengEnvironment):
|
|
||||||
for target in env.docker_targets:
|
|
||||||
check_login(target)
|
|
||||||
|
|
||||||
def check_login(target: DockerTarget):
|
|
||||||
skopeo login @(target.registry_name())
|
|
||||||
|
|
||||||
def upload_docker_images(target: DockerTarget, paths: list[Path]):
|
|
||||||
if not paths: return
|
|
||||||
|
|
||||||
sess = requests.Session()
|
|
||||||
sess.headers['User-Agent'] = 'lix-releng'
|
|
||||||
|
|
||||||
tag_names = [DockerTarget.resolve(tag, version=VERSION, major=MAJOR) for tag in target.tags]
|
|
||||||
|
|
||||||
# latest only gets tagged for the current release branch of Lix
|
|
||||||
if not gitutils.is_maintenance_branch('HEAD'):
|
|
||||||
tag_names.append('latest')
|
|
||||||
|
|
||||||
meta = {}
|
|
||||||
|
|
||||||
reg = docker_assemble.Registry(sess)
|
|
||||||
manifests = []
|
|
||||||
|
|
||||||
with tempfile.TemporaryDirectory() as tmp:
|
|
||||||
tmp = Path(tmp)
|
|
||||||
|
|
||||||
for path in paths:
|
|
||||||
digest_file = tmp / (path.name + '.digest')
|
|
||||||
inspection = json.loads($(skopeo inspect docker-archive:@(path)))
|
|
||||||
|
|
||||||
docker_arch = inspection['Architecture']
|
|
||||||
docker_os = inspection['Os']
|
|
||||||
meta = inspection['Labels']
|
|
||||||
|
|
||||||
log.info('Pushing image %s for %s to %s', path, docker_arch, target.registry_path)
|
|
||||||
|
|
||||||
# insecure-policy: we don't have any signature policy, we are just uploading an image
|
|
||||||
# We upload to a junk tag, because otherwise it will upload to `latest`, which is undesirable
|
|
||||||
skopeo --insecure-policy copy --format oci --digestfile @(digest_file) docker-archive:@(path) docker://@(target.registry_path):temp
|
|
||||||
|
|
||||||
digest = digest_file.read_text().strip()
|
|
||||||
|
|
||||||
# skopeo doesn't give us the manifest size directly, so we just ask the registry
|
|
||||||
metadata = reg.image_info(target.registry_path, digest)
|
|
||||||
|
|
||||||
manifests.append(OCIIndexItem(metadata=metadata, architecture=docker_arch, os=docker_os))
|
|
||||||
# delete the temp tag, which we only have to create because of skopeo
|
|
||||||
# limitations anyhow (it seems to not have a way to say "don't tag it, find
|
|
||||||
# your checksum and put it there")
|
|
||||||
# FIXME: this is not possible because GitHub only has a proprietary API for it. amazing. 11/10.
|
|
||||||
# reg.delete_tag(target.registry_path, 'temp')
|
|
||||||
|
|
||||||
log.info('Pushed images to %r, building a bigger and more menacing manifest from %r with metadata %r', target, manifests, meta)
|
|
||||||
# send the multiarch manifest to each tag
|
|
||||||
index = OCIIndex(manifests=manifests, annotations=meta)
|
|
||||||
for tag in tag_names:
|
|
||||||
reg.upload_index(target.registry_path, tag, index)
|
|
|
@ -1,399 +0,0 @@
|
||||||
from typing import Any, Literal, Optional
|
|
||||||
import re
|
|
||||||
from pathlib import Path
|
|
||||||
import json
|
|
||||||
import dataclasses
|
|
||||||
import time
|
|
||||||
from urllib.parse import unquote
|
|
||||||
import urllib.request
|
|
||||||
import logging
|
|
||||||
|
|
||||||
import requests.auth
|
|
||||||
import requests
|
|
||||||
import xdg_base_dirs
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
|
||||||
log.setLevel(logging.INFO)
|
|
||||||
|
|
||||||
DEBUG_REQUESTS = False
|
|
||||||
if DEBUG_REQUESTS:
|
|
||||||
urllib3_logger = logging.getLogger('requests.packages.urllib3')
|
|
||||||
urllib3_logger.setLevel(logging.DEBUG)
|
|
||||||
urllib3_logger.propagate = True
|
|
||||||
|
|
||||||
# So, there is a bunch of confusing stuff happening in this file. The gist of why it's Like This is:
|
|
||||||
#
|
|
||||||
# nix2container does not concern itself with tags (reasonably enough):
|
|
||||||
# https://github.com/nlewo/nix2container/issues/59
|
|
||||||
#
|
|
||||||
# This is fine. But then we noticed: docker images don't play nice if you have
|
|
||||||
# multiple architectures you want to abstract over if you don't do special
|
|
||||||
# things. Those special things are images with manifests containing multiple
|
|
||||||
# images.
|
|
||||||
#
|
|
||||||
# Docker has a data model vaguely analogous to git: you have higher level
|
|
||||||
# objects referring to a bunch of content-addressed blobs.
|
|
||||||
#
|
|
||||||
# A multiarch image is more or less just a manifest that refers to more
|
|
||||||
# manifests; in OCI it is an Index.
|
|
||||||
#
|
|
||||||
# See the API spec here: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#definitions
|
|
||||||
# And the Index spec here: https://github.com/opencontainers/image-spec/blob/v1.0.1/image-index.md
|
|
||||||
#
|
|
||||||
# skopeo doesn't *know* how to make multiarch *manifests*:
|
|
||||||
# https://github.com/containers/skopeo/issues/1136
|
|
||||||
#
|
|
||||||
# There is a tool called manifest-tool that is supposed to do this
|
|
||||||
# (https://github.com/estesp/manifest-tool) but it doesn't support putting in
|
|
||||||
# annotations on the outer image, and I *really* didn't want to write golang to
|
|
||||||
# fix that. Thus, a little bit of homebrew containers code.
|
|
||||||
#
|
|
||||||
# Essentially what we are doing in here is splatting a bunch of images into the
|
|
||||||
# registry without tagging them (except as "temp", due to podman issues), then
|
|
||||||
# simply sending a new composite manifest ourselves.
|
|
||||||
|
|
||||||
DockerArchitecture = Literal['amd64'] | Literal['arm64']
|
|
||||||
MANIFEST_MIME = 'application/vnd.oci.image.manifest.v1+json'
|
|
||||||
INDEX_MIME = 'application/vnd.oci.image.index.v1+json'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True, order=True)
|
|
||||||
class ImageMetadata:
|
|
||||||
size: int
|
|
||||||
digest: str
|
|
||||||
"""sha256:SOMEHEX"""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True, order=True)
|
|
||||||
class OCIIndexItem:
|
|
||||||
"""Information about an untagged uploaded image."""
|
|
||||||
|
|
||||||
metadata: ImageMetadata
|
|
||||||
|
|
||||||
architecture: DockerArchitecture
|
|
||||||
|
|
||||||
os: str = 'linux'
|
|
||||||
|
|
||||||
def serialize(self):
|
|
||||||
return {
|
|
||||||
'mediaType': MANIFEST_MIME,
|
|
||||||
'size': self.metadata.size,
|
|
||||||
'digest': self.metadata.digest,
|
|
||||||
'platform': {
|
|
||||||
'architecture': self.architecture,
|
|
||||||
'os': self.os,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass(frozen=True)
|
|
||||||
class OCIIndex:
|
|
||||||
manifests: list[OCIIndexItem]
|
|
||||||
|
|
||||||
annotations: dict[str, str]
|
|
||||||
|
|
||||||
def serialize(self):
|
|
||||||
return {
|
|
||||||
'schemaVersion': 2,
|
|
||||||
'manifests': [item.serialize() for item in sorted(self.manifests)],
|
|
||||||
'annotations': self.annotations
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def docker_architecture_from_nix_system(system: str) -> DockerArchitecture:
|
|
||||||
MAP = {
|
|
||||||
'x86_64-linux': 'amd64',
|
|
||||||
'aarch64-linux': 'arm64',
|
|
||||||
}
|
|
||||||
return MAP[system] # type: ignore
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class TaggingOperation:
|
|
||||||
manifest: OCIIndex
|
|
||||||
tags: list[str]
|
|
||||||
"""Tags this image is uploaded under"""
|
|
||||||
|
|
||||||
|
|
||||||
runtime_dir = xdg_base_dirs.xdg_runtime_dir()
|
|
||||||
config_dir = xdg_base_dirs.xdg_config_home()
|
|
||||||
|
|
||||||
AUTH_FILES = ([runtime_dir / 'containers/auth.json'] if runtime_dir else []) + \
|
|
||||||
[config_dir / 'containers/auth.json', Path.home() / '.docker/config.json']
|
|
||||||
|
|
||||||
|
|
||||||
# Copied from Werkzeug https://github.com/pallets/werkzeug/blob/62e3ea45846d06576199a2f8470be7fe44c867c1/src/werkzeug/http.py#L300-L325
|
|
||||||
def parse_list_header(value: str) -> list[str]:
|
|
||||||
"""Parse a header value that consists of a list of comma separated items according
|
|
||||||
to `RFC 9110 <https://httpwg.org/specs/rfc9110.html#abnf.extension>`__.
|
|
||||||
|
|
||||||
This extends :func:`urllib.request.parse_http_list` to remove surrounding quotes
|
|
||||||
from values.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
parse_list_header('token, "quoted value"')
|
|
||||||
['token', 'quoted value']
|
|
||||||
|
|
||||||
This is the reverse of :func:`dump_header`.
|
|
||||||
|
|
||||||
:param value: The header value to parse.
|
|
||||||
"""
|
|
||||||
result = []
|
|
||||||
|
|
||||||
for item in urllib.request.parse_http_list(value):
|
|
||||||
if len(item) >= 2 and item[0] == item[-1] == '"':
|
|
||||||
item = item[1:-1]
|
|
||||||
|
|
||||||
result.append(item)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# https://www.rfc-editor.org/rfc/rfc2231#section-4
|
|
||||||
_charset_value_re = re.compile(
|
|
||||||
r"""
|
|
||||||
([\w!#$%&*+\-.^`|~]*)' # charset part, could be empty
|
|
||||||
[\w!#$%&*+\-.^`|~]*' # don't care about language part, usually empty
|
|
||||||
([\w!#$%&'*+\-.^`|~]+) # one or more token chars with percent encoding
|
|
||||||
""",
|
|
||||||
re.ASCII | re.VERBOSE,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
# Copied from: https://github.com/pallets/werkzeug/blob/62e3ea45846d06576199a2f8470be7fe44c867c1/src/werkzeug/http.py#L327-L394
|
|
||||||
def parse_dict_header(value: str) -> dict[str, str | None]:
|
|
||||||
"""Parse a list header using :func:`parse_list_header`, then parse each item as a
|
|
||||||
``key=value`` pair.
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
parse_dict_header('a=b, c="d, e", f')
|
|
||||||
{"a": "b", "c": "d, e", "f": None}
|
|
||||||
|
|
||||||
This is the reverse of :func:`dump_header`.
|
|
||||||
|
|
||||||
If a key does not have a value, it is ``None``.
|
|
||||||
|
|
||||||
This handles charsets for values as described in
|
|
||||||
`RFC 2231 <https://www.rfc-editor.org/rfc/rfc2231#section-3>`__. Only ASCII, UTF-8,
|
|
||||||
and ISO-8859-1 charsets are accepted, otherwise the value remains quoted.
|
|
||||||
|
|
||||||
:param value: The header value to parse.
|
|
||||||
|
|
||||||
.. versionchanged:: 3.0
|
|
||||||
Passing bytes is not supported.
|
|
||||||
|
|
||||||
.. versionchanged:: 3.0
|
|
||||||
The ``cls`` argument is removed.
|
|
||||||
|
|
||||||
.. versionchanged:: 2.3
|
|
||||||
Added support for ``key*=charset''value`` encoded items.
|
|
||||||
|
|
||||||
.. versionchanged:: 0.9
|
|
||||||
The ``cls`` argument was added.
|
|
||||||
"""
|
|
||||||
result: dict[str, str | None] = {}
|
|
||||||
|
|
||||||
for item in parse_list_header(value):
|
|
||||||
key, has_value, value = item.partition("=")
|
|
||||||
key = key.strip()
|
|
||||||
|
|
||||||
if not has_value:
|
|
||||||
result[key] = None
|
|
||||||
continue
|
|
||||||
|
|
||||||
value = value.strip()
|
|
||||||
encoding: str | None = None
|
|
||||||
|
|
||||||
if key[-1] == "*":
|
|
||||||
# key*=charset''value becomes key=value, where value is percent encoded
|
|
||||||
# adapted from parse_options_header, without the continuation handling
|
|
||||||
key = key[:-1]
|
|
||||||
match = _charset_value_re.match(value)
|
|
||||||
|
|
||||||
if match:
|
|
||||||
# If there is a charset marker in the value, split it off.
|
|
||||||
encoding, value = match.groups()
|
|
||||||
assert encoding
|
|
||||||
encoding = encoding.lower()
|
|
||||||
|
|
||||||
# A safe list of encodings. Modern clients should only send ASCII or UTF-8.
|
|
||||||
# This list will not be extended further. An invalid encoding will leave the
|
|
||||||
# value quoted.
|
|
||||||
if encoding in {"ascii", "us-ascii", "utf-8", "iso-8859-1"}:
|
|
||||||
# invalid bytes are replaced during unquoting
|
|
||||||
value = unquote(value, encoding=encoding)
|
|
||||||
|
|
||||||
if len(value) >= 2 and value[0] == value[-1] == '"':
|
|
||||||
value = value[1:-1]
|
|
||||||
|
|
||||||
result[key] = value
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def parse_www_authenticate(www_authenticate):
|
|
||||||
scheme, _, rest = www_authenticate.partition(' ')
|
|
||||||
scheme = scheme.lower()
|
|
||||||
rest = rest.strip()
|
|
||||||
|
|
||||||
parsed = parse_dict_header(rest.rstrip('='))
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
|
|
||||||
class AuthState:
|
|
||||||
|
|
||||||
def __init__(self, auth_files: list[Path] = AUTH_FILES):
|
|
||||||
self.auth_map: dict[str, str] = {}
|
|
||||||
for f in auth_files:
|
|
||||||
self.auth_map.update(AuthState.load_auth_file(f))
|
|
||||||
self.token_cache: dict[str, str] = {}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def load_auth_file(path: Path) -> dict[str, str]:
|
|
||||||
if path.exists():
|
|
||||||
with path.open() as fh:
|
|
||||||
try:
|
|
||||||
json_obj = json.load(fh)
|
|
||||||
return {k: v['auth'] for k, v in json_obj['auths'].items()}
|
|
||||||
except (json.JSONDecodeError, KeyError) as e:
|
|
||||||
log.exception('JSON decode error in %s', path, exc_info=e)
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def get_token(self, hostname: str) -> Optional[str]:
|
|
||||||
return self.token_cache.get(hostname)
|
|
||||||
|
|
||||||
def obtain_token(self, session: requests.Session, token_endpoint: str,
|
|
||||||
scope: str, service: str, image_path: str) -> str:
|
|
||||||
authority, _, _ = image_path.partition('/')
|
|
||||||
if tok := self.get_token(authority):
|
|
||||||
return tok
|
|
||||||
|
|
||||||
creds = self.find_credential_for(image_path)
|
|
||||||
if not creds:
|
|
||||||
raise ValueError('No credentials available for ' + image_path)
|
|
||||||
|
|
||||||
resp = session.get(token_endpoint,
|
|
||||||
params={
|
|
||||||
'client_id': 'lix-releng',
|
|
||||||
'scope': scope,
|
|
||||||
'service': service,
|
|
||||||
},
|
|
||||||
headers={
|
|
||||||
'Authorization': 'Basic ' + creds
|
|
||||||
}).json()
|
|
||||||
token = resp['token']
|
|
||||||
self.token_cache[service] = token
|
|
||||||
return token
|
|
||||||
|
|
||||||
def find_credential_for(self, image_path: str):
|
|
||||||
trails = image_path.split('/')
|
|
||||||
for i in range(len(trails)):
|
|
||||||
prefix = '/'.join(trails[:len(trails) - i])
|
|
||||||
if prefix in self.auth_map:
|
|
||||||
return self.auth_map[prefix]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class RegistryAuthenticator(requests.auth.AuthBase):
|
|
||||||
"""Authenticates to an OCI compliant registry"""
|
|
||||||
|
|
||||||
def __init__(self, auth_state: AuthState, session: requests.Session,
|
|
||||||
image: str):
|
|
||||||
self.auth_map: dict[str, str] = {}
|
|
||||||
self.image = image
|
|
||||||
self.session = session
|
|
||||||
self.auth_state = auth_state
|
|
||||||
|
|
||||||
def response_hook(self, r: requests.Response,
|
|
||||||
**kwargs: Any) -> requests.Response:
|
|
||||||
if r.status_code == 401:
|
|
||||||
www_authenticate = r.headers.get('www-authenticate', '').lower()
|
|
||||||
parsed = parse_www_authenticate(www_authenticate)
|
|
||||||
assert parsed
|
|
||||||
|
|
||||||
tok = self.auth_state.obtain_token(
|
|
||||||
self.session,
|
|
||||||
parsed['realm'], # type: ignore
|
|
||||||
parsed['scope'], # type: ignore
|
|
||||||
parsed['service'], # type: ignore
|
|
||||||
self.image)
|
|
||||||
|
|
||||||
new_req = r.request.copy()
|
|
||||||
new_req.headers['Authorization'] = 'Bearer ' + tok
|
|
||||||
|
|
||||||
return self.session.send(new_req)
|
|
||||||
else:
|
|
||||||
return r
|
|
||||||
|
|
||||||
def __call__(self,
|
|
||||||
r: requests.PreparedRequest) -> requests.PreparedRequest:
|
|
||||||
authority, _, _ = self.image.partition('/')
|
|
||||||
auth_may = self.auth_state.get_token(authority)
|
|
||||||
|
|
||||||
if auth_may:
|
|
||||||
r.headers['Authorization'] = 'Bearer ' + auth_may
|
|
||||||
|
|
||||||
r.register_hook('response', self.response_hook)
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
class Registry:
|
|
||||||
|
|
||||||
def __init__(self, session: requests.Session):
|
|
||||||
self.auth_state = AuthState()
|
|
||||||
self.session = session
|
|
||||||
|
|
||||||
def image_info(self, image_path: str, manifest_id: str) -> ImageMetadata:
|
|
||||||
authority, _, path = image_path.partition('/')
|
|
||||||
resp = self.session.head(
|
|
||||||
f'https://{authority}/v2/{path}/manifests/{manifest_id}',
|
|
||||||
headers={'Accept': MANIFEST_MIME},
|
|
||||||
auth=RegistryAuthenticator(self.auth_state, self.session,
|
|
||||||
image_path))
|
|
||||||
resp.raise_for_status()
|
|
||||||
return ImageMetadata(int(resp.headers['content-length']),
|
|
||||||
resp.headers['docker-content-digest'])
|
|
||||||
|
|
||||||
def delete_tag(self, image_path: str, tag: str):
|
|
||||||
authority, _, path = image_path.partition('/')
|
|
||||||
resp = self.session.delete(
|
|
||||||
f'https://{authority}/v2/{path}/manifests/{tag}',
|
|
||||||
headers={'Content-Type': INDEX_MIME},
|
|
||||||
auth=RegistryAuthenticator(self.auth_state, self.session,
|
|
||||||
image_path))
|
|
||||||
resp.raise_for_status()
|
|
||||||
|
|
||||||
def _upload_index(self, image_path: str, tag: str, index: OCIIndex):
|
|
||||||
authority, _, path = image_path.partition('/')
|
|
||||||
body = json.dumps(index.serialize(),
|
|
||||||
separators=(',', ':'),
|
|
||||||
sort_keys=True)
|
|
||||||
|
|
||||||
resp = self.session.put(
|
|
||||||
f'https://{authority}/v2/{path}/manifests/{tag}',
|
|
||||||
data=body,
|
|
||||||
headers={'Content-Type': INDEX_MIME},
|
|
||||||
auth=RegistryAuthenticator(self.auth_state, self.session,
|
|
||||||
image_path))
|
|
||||||
resp.raise_for_status()
|
|
||||||
|
|
||||||
return resp.headers['Location']
|
|
||||||
|
|
||||||
def upload_index(self,
|
|
||||||
image_path: str,
|
|
||||||
tag: str,
|
|
||||||
index: OCIIndex,
|
|
||||||
retries=20,
|
|
||||||
retry_delay=1):
|
|
||||||
# eventual consistency lmao
|
|
||||||
for _ in range(retries):
|
|
||||||
try:
|
|
||||||
return self._upload_index(image_path, tag, index)
|
|
||||||
except requests.HTTPError as e:
|
|
||||||
if e.response.status_code != 404:
|
|
||||||
raise
|
|
||||||
|
|
||||||
time.sleep(retry_delay)
|
|
|
@ -1,134 +0,0 @@
|
||||||
from typing import Callable
|
|
||||||
import urllib.parse
|
|
||||||
import re
|
|
||||||
import functools
|
|
||||||
import subprocess
|
|
||||||
import dataclasses
|
|
||||||
|
|
||||||
S3_HOST = 's3.lix.systems'
|
|
||||||
S3_ENDPOINT = 'https://s3.lix.systems'
|
|
||||||
|
|
||||||
DEFAULT_STORE_URI_BITS = {
|
|
||||||
'region': 'garage',
|
|
||||||
'endpoint': 's3.lix.systems',
|
|
||||||
'want-mass-query': 'true',
|
|
||||||
'write-nar-listing': 'true',
|
|
||||||
'ls-compression': 'zstd',
|
|
||||||
'narinfo-compression': 'zstd',
|
|
||||||
'compression': 'zstd',
|
|
||||||
'parallel-compression': 'true',
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class DockerTarget:
|
|
||||||
registry_path: str
|
|
||||||
"""Registry path without the tag, e.g. ghcr.io/lix-project/lix"""
|
|
||||||
|
|
||||||
tags: list[str]
|
|
||||||
"""List of tags this image should take. There must be at least one."""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve(item: str, version: str, major: str) -> str:
|
|
||||||
"""
|
|
||||||
Applies templates:
|
|
||||||
- version: the Lix version e.g. 2.90.0
|
|
||||||
- major: the major Lix version e.g. 2.90
|
|
||||||
"""
|
|
||||||
return item.format(version=version, major=major)
|
|
||||||
|
|
||||||
def registry_name(self) -> str:
|
|
||||||
[a, _, _] = self.registry_path.partition('/')
|
|
||||||
return a
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class RelengEnvironment:
|
|
||||||
name: str
|
|
||||||
colour: Callable[[str], str]
|
|
||||||
|
|
||||||
cache_store_overlay: dict[str, str]
|
|
||||||
cache_bucket: str
|
|
||||||
releases_bucket: str
|
|
||||||
docs_bucket: str
|
|
||||||
git_repo: str
|
|
||||||
|
|
||||||
docker_targets: list[DockerTarget]
|
|
||||||
|
|
||||||
def cache_store_uri(self):
|
|
||||||
qs = DEFAULT_STORE_URI_BITS.copy()
|
|
||||||
qs.update(self.cache_store_overlay)
|
|
||||||
return self.cache_bucket + "?" + urllib.parse.urlencode(qs)
|
|
||||||
|
|
||||||
|
|
||||||
SGR = '\x1b['
|
|
||||||
RED = '31;1m'
|
|
||||||
GREEN = '32;1m'
|
|
||||||
RESET = '0m'
|
|
||||||
|
|
||||||
|
|
||||||
def sgr(colour: str, text: str) -> str:
|
|
||||||
return f'{SGR}{colour}{text}{SGR}{RESET}'
|
|
||||||
|
|
||||||
|
|
||||||
STAGING = RelengEnvironment(
|
|
||||||
name='staging',
|
|
||||||
colour=functools.partial(sgr, GREEN),
|
|
||||||
docs_bucket='s3://staging-docs',
|
|
||||||
cache_bucket='s3://staging-cache',
|
|
||||||
cache_store_overlay={'secret-key': 'staging.key'},
|
|
||||||
releases_bucket='s3://staging-releases',
|
|
||||||
git_repo='ssh://git@git.lix.systems/lix-project/lix-releng-staging',
|
|
||||||
docker_targets=[
|
|
||||||
# latest will be auto tagged if appropriate
|
|
||||||
DockerTarget('git.lix.systems/lix-project/lix-releng-staging',
|
|
||||||
tags=['{version}', '{major}']),
|
|
||||||
DockerTarget('ghcr.io/lix-project/lix-releng-staging',
|
|
||||||
tags=['{version}', '{major}']),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
GERRIT_REMOTE_RE = re.compile(r'^ssh://(\w+@)?gerrit.lix.systems:2022/lix$')
|
|
||||||
|
|
||||||
|
|
||||||
def guess_gerrit_remote():
|
|
||||||
"""
|
|
||||||
Deals with people having unknown gerrit username.
|
|
||||||
"""
|
|
||||||
out = [
|
|
||||||
x.split()[1] for x in subprocess.check_output(
|
|
||||||
['git', 'remote', '-v']).decode().splitlines()
|
|
||||||
]
|
|
||||||
return next(x for x in out if GERRIT_REMOTE_RE.match(x))
|
|
||||||
|
|
||||||
|
|
||||||
PROD = RelengEnvironment(
|
|
||||||
name='production',
|
|
||||||
colour=functools.partial(sgr, RED),
|
|
||||||
docs_bucket='s3://docs',
|
|
||||||
cache_bucket='s3://cache',
|
|
||||||
# FIXME: we should decrypt this with age into a tempdir in the future, but
|
|
||||||
# the issue is how to deal with the recipients file. For now, we should
|
|
||||||
# just delete it after doing a release.
|
|
||||||
cache_store_overlay={'secret-key': 'prod.key'},
|
|
||||||
releases_bucket='s3://releases',
|
|
||||||
git_repo=guess_gerrit_remote(),
|
|
||||||
docker_targets=[
|
|
||||||
# latest will be auto tagged if appropriate
|
|
||||||
DockerTarget('git.lix.systems/lix-project/lix',
|
|
||||||
tags=['{version}', '{major}']),
|
|
||||||
DockerTarget('ghcr.io/lix-project/lix', tags=['{version}', '{major}']),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
ENVIRONMENTS = {
|
|
||||||
'staging': STAGING,
|
|
||||||
'production': PROD,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
|
||||||
class S3Credentials:
|
|
||||||
name: str
|
|
||||||
id: str
|
|
||||||
secret_key: str
|
|
|
@ -1,37 +0,0 @@
|
||||||
import subprocess
|
|
||||||
import json
|
|
||||||
|
|
||||||
|
|
||||||
def version_compare(v1: str, v2: str):
|
|
||||||
return json.loads($(nix-instantiate --eval --json --argstr v1 @(v1) --argstr v2 @(v2) --expr '{v1, v2}: builtins.compareVersions v1 v2'))
|
|
||||||
|
|
||||||
|
|
||||||
def latest_tag_on_branch(branch: str) -> str:
|
|
||||||
return $(git describe --abbrev=0 @(branch) e>/dev/null).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def is_maintenance_branch(branch: str) -> bool:
|
|
||||||
try:
|
|
||||||
main_tag = latest_tag_on_branch('main')
|
|
||||||
current_tag = latest_tag_on_branch(branch)
|
|
||||||
|
|
||||||
return version_compare(current_tag, main_tag) < 0
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
# This is the case before Lix releases 2.90, since main *has* no
|
|
||||||
# release tag on it.
|
|
||||||
# FIXME: delete this case after 2.91
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def verify_are_on_tag():
|
|
||||||
current_tag = $(git describe --tag).strip()
|
|
||||||
assert current_tag == VERSION
|
|
||||||
|
|
||||||
|
|
||||||
def git_preconditions():
|
|
||||||
# verify there is nothing in index ready to stage
|
|
||||||
proc = !(git diff-index --quiet --cached HEAD --)
|
|
||||||
assert proc.rtn == 0
|
|
||||||
# verify there is nothing *stageable* and tracked
|
|
||||||
proc = !(git diff-files --quiet)
|
|
||||||
assert proc.rtn == 0
|
|
|
@ -1,19 +0,0 @@
|
||||||
import subprocess
|
|
||||||
import json
|
|
||||||
from . import environment
|
|
||||||
|
|
||||||
|
|
||||||
def get_ephemeral_key(
|
|
||||||
env: environment.RelengEnvironment) -> environment.S3Credentials:
|
|
||||||
output = subprocess.check_output([
|
|
||||||
'ssh', '-l', 'root', environment.S3_HOST, 'garage-ephemeral-key',
|
|
||||||
'new', '--name', f'releng-{env.name}', '--read', '--write',
|
|
||||||
'--age-secs', '3600',
|
|
||||||
env.releases_bucket.removeprefix('s3://'),
|
|
||||||
env.cache_bucket.removeprefix('s3://'),
|
|
||||||
env.docs_bucket.removeprefix('s3://'),
|
|
||||||
])
|
|
||||||
d = json.loads(output.decode())
|
|
||||||
return environment.S3Credentials(name=d['name'],
|
|
||||||
id=d['id'],
|
|
||||||
secret_key=d['secret_key'])
|
|
|
@ -1,57 +0,0 @@
|
||||||
{ hydraJobs, pkgs }:
|
|
||||||
let
|
|
||||||
inherit (pkgs) lib;
|
|
||||||
lix = hydraJobs.build.x86_64-linux;
|
|
||||||
|
|
||||||
systems = [ "x86_64-linux" ];
|
|
||||||
dockerSystems = [ "x86_64-linux" ];
|
|
||||||
|
|
||||||
doTarball =
|
|
||||||
{
|
|
||||||
target,
|
|
||||||
targetName,
|
|
||||||
rename ? null,
|
|
||||||
}:
|
|
||||||
''
|
|
||||||
echo "doing: ${target}"
|
|
||||||
# expand wildcard
|
|
||||||
filename=$(echo ${target}/${targetName})
|
|
||||||
basename="$(basename $filename)"
|
|
||||||
|
|
||||||
echo $filename $basename
|
|
||||||
cp -v "$filename" "$out"
|
|
||||||
${lib.optionalString (rename != null) ''
|
|
||||||
mv "$out/$basename" "$out/${rename}"
|
|
||||||
basename="${rename}"
|
|
||||||
''}
|
|
||||||
sha256sum --binary $filename | cut -f1 -d' ' > $out/$basename.sha256
|
|
||||||
'';
|
|
||||||
|
|
||||||
targets =
|
|
||||||
builtins.map (system: {
|
|
||||||
target = hydraJobs.binaryTarball.${system};
|
|
||||||
targetName = "*.tar.xz";
|
|
||||||
}) systems
|
|
||||||
++ builtins.map (system: {
|
|
||||||
target = hydraJobs.dockerImage.${system}.tarball;
|
|
||||||
targetName = "image.tar.gz";
|
|
||||||
rename = "lix-${lix.version}-docker-image-${system}.tar.gz";
|
|
||||||
}) dockerSystems;
|
|
||||||
|
|
||||||
manualTar = pkgs.runCommand "lix-manual-tarball" { } ''
|
|
||||||
mkdir -p $out
|
|
||||||
cp -r ${lix.doc}/share/doc/nix/manual lix-${lix.version}-manual
|
|
||||||
tar -cvzf "$out/lix-${lix.version}-manual.tar.gz" lix-${lix.version}-manual
|
|
||||||
'';
|
|
||||||
|
|
||||||
tarballs = pkgs.runCommand "lix-release-tarballs" { } ''
|
|
||||||
mkdir -p $out
|
|
||||||
${lib.concatMapStringsSep "\n" doTarball targets}
|
|
||||||
cp ${manualTar}/*.tar.gz $out
|
|
||||||
cp -r ${lix.doc}/share/doc/nix/manual $out
|
|
||||||
'';
|
|
||||||
in
|
|
||||||
{
|
|
||||||
inherit (hydraJobs) build;
|
|
||||||
inherit tarballs;
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
import json
|
|
||||||
|
|
||||||
version_json = json.load(open('version.json'))
|
|
||||||
VERSION = version_json['version']
|
|
||||||
MAJOR = '.'.join(VERSION.split('.')[:2])
|
|
||||||
RELEASE_NAME = version_json['release_name']
|
|
|
@ -146,8 +146,7 @@ fi
|
||||||
isDaemonNewer () {
|
isDaemonNewer () {
|
||||||
[[ -n "${NIX_DAEMON_PACKAGE:-}" ]] || return 0
|
[[ -n "${NIX_DAEMON_PACKAGE:-}" ]] || return 0
|
||||||
local requiredVersion="$1"
|
local requiredVersion="$1"
|
||||||
local versionOutput=$($NIX_DAEMON_PACKAGE/bin/nix daemon --version)
|
local daemonVersion=$($NIX_DAEMON_PACKAGE/bin/nix daemon --version | cut -d' ' -f3)
|
||||||
local daemonVersion=${versionOutput##* }
|
|
||||||
[[ $(nix eval --expr "builtins.compareVersions ''$daemonVersion'' ''$requiredVersion''") -ge 0 ]]
|
[[ $(nix eval --expr "builtins.compareVersions ''$daemonVersion'' ''$requiredVersion''") -ge 0 ]]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,6 @@ in {
|
||||||
{ config, pkgs, ... }:
|
{ config, pkgs, ... }:
|
||||||
{ services.openssh.enable = true;
|
{ services.openssh.enable = true;
|
||||||
services.openssh.settings.PermitRootLogin = "yes";
|
services.openssh.settings.PermitRootLogin = "yes";
|
||||||
users.users.root.hashedPasswordFile = lib.mkForce null;
|
|
||||||
users.users.root.password = "foobar";
|
users.users.root.password = "foobar";
|
||||||
virtualisation.writableStore = true;
|
virtualisation.writableStore = true;
|
||||||
virtualisation.additionalPaths = [ pkgB pkgC ];
|
virtualisation.additionalPaths = [ pkgB pkgC ];
|
||||||
|
|
Loading…
Reference in a new issue