Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 21 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,48 +73,38 @@ or
nix build .#custom-bundle-squashfs
```

To build the full bundle as a gzip-compressed tar OCI image:
To build the full bundle as an OCI image:

```
nix build .#bundle-oci
```

The result is an OCI image layout directory with the reference `bundle`, suitable
for copying with an OCI-layout-aware tool such as
`skopeo copy oci:./result:bundle docker://REGISTRY/REPOSITORY:TAG`.
The output also provides a copy helper that keeps the exact image in its Nix
closure and preserves the manifest digest:
The result is an OCI image layout directory with the reference `bundle`. Copy
it with the helper, which keeps the image in its Nix closure and preserves its
manifest digest:

```
nix run .#bundle-oci.copyTo -- docker://REGISTRY/REPOSITORY:TAG --authfile ./auth.json --digestfile ./digest
```

The destination can use any Skopeo-supported transport, including `oci:`.
Additional arguments are passed to `skopeo copy`. The helper trusts the locally
built source (`--insecure-policy`); registry TLS verification remains enabled.
For a clean Git source, the manifest records the full flake revision, source URL
The destination can use any Skopeo-supported transport. Additional arguments
are passed to `skopeo copy`. The helper trusts the locally built source
(`--insecure-policy`); registry TLS verification remains enabled. For a clean
Git source, the manifest records the full flake revision, source URL
`https://github.com/replit/nixmodules`, and output name `bundle-oci` in OCI
annotations. The manifest creation annotation and config `created` field use
the flake's last-modified timestamp in UTC, never the build's wall clock.
Local sources without a clean revision omit the revision annotation; sources
without a last-modified timestamp omit creation metadata. Publish from a
revision-pinned Git flake when complete provenance is required.

It contains one `application/vnd.oci.image.layer.v1.tar+gzip` layer. Gzip is
transport compression: the consumer decompresses it when unpacking the image.
The manifest describes the compressed blob's digest and size; the config's
diffID is the digest of the uncompressed tar stream. Tar entry order,
timestamps, ownership, extended headers and gzip headers are deterministic.

The filesystem is generated directly from the same pinned full bundle used by
`disk-script`: its complete store closure and `/etc/nixmodules` metadata, with
unchanged store paths, file contents, modes and symlink targets. All guest
UIDs/GIDs are 11000, matching the legacy disk builders. Existing disk and
per-module OCI builders are unchanged. There is no disk conversion or dependency
update. The tar archive preserves hardlinks and extended attributes. A consumer
that converts the tar to EROFS must leave filesystem compression disabled for
files to remain DAX-eligible. This uses more unpacked disk space than compressed
EROFS; it does not automatically share all guest memory.
annotations. Creation metadata uses the flake's last-modified timestamp in UTC.
Local sources without revision or timestamp metadata omit those annotations.

The image contains one `application/vnd.oci.image.layer.v1.erofs+zstd` layer.
The layer descriptor's digest and size cover the zstd-compressed data; its
`dev.replit.erofs.uncompressed-size` annotation records the raw EROFS image
size. The config diffID is the SHA-256 digest of that raw image. The EROFS
filesystem has internal compression and inline data disabled, with the complete
bundle closure and `/etc/nixmodules` metadata.

This changes the existing `bundle-oci` format and is not backward-compatible
with consumers expecting a tar+gzip layer. Consumers must support this OCI
layer media type and EROFS; deploy that support before publishing images.

A small fixture exercises the producer without building the full bundle:

Expand Down
21 changes: 21 additions & 0 deletions pkgs/bundle-oci/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
set -euo pipefail

bundle="$1"
store_paths="$2"
output="$3"
architecture="$4"
metadata="$5"
layout_script="$6"

mkdir -p root/nix/store root/etc/nixmodules "$output/blobs/sha256"
cp -a --reflink=auto "$bundle/etc/nixmodules/." root/etc/nixmodules/
while IFS= read -r path; do
cp -a --reflink=auto "$path" root/nix/store/
done < "$store_paths"
chmod 755 root root/nix root/nix/store root/etc root/etc/nixmodules

env -u SOURCE_DATE_EPOCH mkfs.erofs -U 00000000-0000-0000-0000-000000000001 -T 1 --all-time \
--force-uid=11000 --force-gid=11000 -E noinline_data raw.erofs root
zstd -15 --long=27 -T8 --check --no-progress --quiet raw.erofs -o layer.erofs.zst
python3 "$layout_script" raw.erofs layer.erofs.zst "$output" "$architecture" "$metadata"
26 changes: 19 additions & 7 deletions pkgs/bundle-oci/check.nix
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,15 @@
let
dependency = pkgs.runCommand "bundle-oci-dependency" { } ''
mkdir -p "$out"
printf 'dependency\n' > "$out/data"
head -c 131072 /dev/zero >> "$out/data"
printf 'fixture dependency\n' > "$out/data"
ln "$out/data" "$out/hardlink"
'';
module = pkgs.runCommand "bundle-oci-module" { } ''
mkdir -p "$out/bin"
printf '%s\n' '${dependency}' > "$out/module.json"
printf '#!/bin/sh\nexit 0\n' > "$out/bin/tool"
head -c 131072 /dev/zero >> "$out/bin/tool"
chmod 755 "$out/bin/tool"
chmod 751 "$out/bin/tool"
ln "$out/module.json" "$out/module-hardlink"
ln -s ../module.json "$out/bin/relative"
ln -s '${dependency}/data' "$out/absolute"
'';
Expand All @@ -28,10 +27,15 @@ let
in
pkgs.runCommand "bundle-oci-check"
{
nativeBuildInputs = [ pkgs.gnutar pkgs.gzip pkgs.python3 ];
nativeBuildInputs = [
pkgs.erofs-utils
pkgs.python3
pkgs.skopeo
pkgs.zstd
];
}
''
python3 ${./check.py} ${image} ${bundle} ${closure}/store-paths ${dependency} ${./layout.py} ${./layer.sh}
python3 ${./check.py} ${image} ${bundle} ${closure}/store-paths ${dependency} ${module} ${./layout.py}
${image.copyTo}/bin/copy-nixmodules-bundle-oci "oci:$PWD/copied:bundle" \
--authfile ${pkgs.writeText "empty-auth.json" ''{"auths":{}}''} \
--digestfile "$PWD/copied.digest"
Expand All @@ -44,7 +48,15 @@ pkgs.runCommand "bundle-oci-check"
assert digest_file.read_text().strip() == descriptor["digest"]
for blob in (original / "blobs/sha256").iterdir():
assert blob.read_bytes() == (copied / "blobs/sha256" / blob.name).read_bytes()
print("copyTo preserved manifest digest and every OCI blob")
copied_manifest = json.loads(
(copied / "blobs/sha256" / copied_descriptor["digest"].split(":")[1]).read_text()
)
assert copied_manifest["layers"][0]["mediaType"] == "application/vnd.oci.image.layer.v1.erofs+zstd"
original_manifest = json.loads(
(original / "blobs/sha256" / descriptor["digest"].split(":")[1]).read_text()
)
assert copied_manifest["layers"][0]["annotations"] == original_manifest["layers"][0]["annotations"]
print("copyTo preserved the custom layer media type, manifest digest and every OCI blob")
PY
touch "$out"
''
Loading
Loading