diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a98c56b..348c264 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -26,6 +26,14 @@ jobs: - name: Install Nix prerequisites if: ${{ matrix.test == 'nix' }} run: nix-env -i nix-eval-jobs -f '' + - name: Setup Nix cache + if: ${{ matrix.test == 'nix' }} + uses: ryanccn/attic-action@v0 + with: + endpoint: https://attic.brainhive.nl/ + cache: steiger + token: ${{ secrets.ATTIC_TOKEN }} + inputs-from: . - name: Install Bazel prerequisites if: ${{ matrix.test == 'bazel' }} run: | diff --git a/.gitignore b/.gitignore index f717ddd..3906c5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -/target -/result +target +result .direnv diff --git a/README.md b/README.md index 3951e81..7831f82 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,6 @@ Requirements }; config.Cmd = ["/bin/hello"]; - compressor = "none"; }; }; diff --git a/flake.lock b/flake.lock index 73115d3..77f4cc5 100644 --- a/flake.lock +++ b/flake.lock @@ -15,34 +15,33 @@ "type": "github" } }, - "nixpkgs": { + "nix-filter": { "locked": { - "lastModified": 1770380644, - "narHash": "sha256-P7dWMHRUWG5m4G+06jDyThXO7kwSk46C1kgjEWcybkE=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "ae67888ff7ef9dff69b3cf0cc0fbfbcd3a722abe", + "lastModified": 1757882181, + "narHash": "sha256-+cCxYIh2UNalTz364p+QYmWHs0P+6wDhiWR4jDIKQIU=", + "owner": "numtide", + "repo": "nix-filter", + "rev": "59c44d1909c72441144b93cf0f054be7fe764de5", "type": "github" }, "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", + "owner": "numtide", + "repo": "nix-filter", "type": "github" } }, - "nixpkgs-ocitools": { + "nixpkgs": { "locked": { - "lastModified": 1770565630, - "narHash": "sha256-+0dj3MSv+Pxf6e+89tObqGSAGWcmZHPFGw8Hi6DsTco=", - "owner": "brainhivenl", + "lastModified": 1770380644, + "narHash": "sha256-P7dWMHRUWG5m4G+06jDyThXO7kwSk46C1kgjEWcybkE=", + "owner": "NixOS", "repo": "nixpkgs", - "rev": "6144e69fa0fd51c96201bc64632526dee386ddf4", + "rev": "ae67888ff7ef9dff69b3cf0cc0fbfbcd3a722abe", "type": "github" }, "original": { - "owner": "brainhivenl", - "ref": "oci/refactor", + "owner": "NixOS", + "ref": "nixpkgs-unstable", "repo": "nixpkgs", "type": "github" } @@ -50,8 +49,8 @@ "root": { "inputs": { "crane": "crane", + "nix-filter": "nix-filter", "nixpkgs": "nixpkgs", - "nixpkgs-ocitools": "nixpkgs-ocitools", "rust-overlay": "rust-overlay" } }, diff --git a/flake.nix b/flake.nix index 96380cd..35fdc49 100644 --- a/flake.nix +++ b/flake.nix @@ -1,8 +1,7 @@ { inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; - nixpkgs-ocitools.url = "github:brainhivenl/nixpkgs/oci/refactor"; - + nix-filter.url = "github:numtide/nix-filter"; crane.url = "github:ipetkov/crane"; rust-overlay = { url = "github:oxalica/rust-overlay"; @@ -12,11 +11,13 @@ outputs = { self, + nix-filter, nixpkgs, crane, ... - } @ inputs: let + }: let inherit (nixpkgs) lib; + filter = nix-filter.lib; forEachSystem = fun: lib.genAttrs (lib.systems.flakeExposed) ( @@ -35,13 +36,21 @@ packages = forEachSystem ( pkgs: let craneLib = crane.mkLib pkgs; + src = filter { + root = ./.; + include = [ + ./Cargo.toml + ./Cargo.lock + ./src + ]; + }; commonArgs = { - src = craneLib.cleanCargoSource ./.; + inherit src; strictDeps = true; buildInputs = lib.optionals pkgs.stdenv.isDarwin [pkgs.libiconv]; }; - in { - default = craneLib.buildPackage ( + + steiger = craneLib.buildPackage ( commonArgs // { cargoArtifacts = craneLib.buildDepsOnly commonArgs; @@ -53,30 +62,49 @@ NIX_EVAL_JOBS_BINARY = lib.getExe pkgs.nix-eval-jobs; } ); + in { + default = steiger; + ociTools = pkgs.callPackage ./oci-tools {}; + + image = let + ociTools = self.packages.${pkgs.stdenv.hostPlatform.system}.ociTools; + in + ociTools.buildImage { + name = "test-image"; + layers = [ + (pkgs.writeClosure [pkgs.dockerTools.caCertificates pkgs.hello]) + ]; + pathsToLink = ["/bin" "/etc"]; + }; } ); checks = forEachSystem (pkgs: { - inherit (self.packages.${pkgs.system}) default; + inherit (self.packages.${pkgs.stdenv.hostPlatform.system}) default; }); devShells = forEachSystem ( - pkgs: let - craneLib = crane.mkLib pkgs; - in { - default = craneLib.devShell { + pkgs: { + default = (crane.mkLib pkgs).devShell { packages = [ pkgs.rust-analyzer pkgs.nix-eval-jobs ]; }; + steiger = pkgs.mkShell { + packages = [self.packages.${pkgs.stdenv.hostPlatform.system}.default]; + }; } ); - overlays.ociTools = final: prev: let - pkgs = import inputs.nixpkgs-ocitools {inherit (final) system;}; - in { - inherit (pkgs) ociTools; + overlays = { + default = final: prev: { + steiger = self.packages.${final.stdenv.hostPlatform.system}.default; + ociTools = self.packages.${final.stdenv.hostPlatform.system}.ociTools; + }; + ociTools = final: prev: { + ociTools = self.packages.${final.stdenv.hostPlatform.system}.ociTools; + }; }; }; } diff --git a/oci-tools.nix b/oci-tools.nix new file mode 100644 index 0000000..f4fe6a7 --- /dev/null +++ b/oci-tools.nix @@ -0,0 +1,64 @@ +{ + lib, + stdenvNoCC, + runCommand, + writeClosure, + closureInfo, + steiger, +}: let + defaultArch = let + inherit (stdenvNoCC.targetPlatform) system; + in ( + if lib.hasPrefix "aarch64-" system + then "arm64" + else if lib.hasPrefix "x86_64-" system + then "amd64" + else throw "unsupported system: ${system}" + ); + + joinLines = lib.concatStringsSep "\n"; + + # split derivation into 2 seperate closures separating the main package and it's dependencies + splitDeps = drv: let + fullClosure = closureInfo {rootPaths = [drv];}; + depsPaths = runCommand "deps-paths" {} '' + grep -v "${drv}" ${fullClosure}/store-paths > $out + ''; + drvPaths = runCommand "app-paths" {} '' + echo "${drv}" > $out + ''; + in + map (closure: {inherit closure;}) [depsPaths drvPaths]; + + # writeClosure wrapper which allows direct closure passthrough + writeLayerClosure = layer: + if layer ? closure + then layer.closure + else writeClosure layer; + + buildImage = { + name, + tag ? "latest", + layers, + pathsToLink ? null, + config ? {}, + os ? "linux", + arch ? defaultArch, + }: + runCommand name + {nativeBuildInputs = [steiger];} + # sh + '' + steiger nix-to-oci \ + ${joinLines (map (closure: "--closure=${closure} \\") (map writeLayerClosure layers))} + ${joinLines (map (path: "--link-path=${path} \\") pathsToLink)} + --out-path $out \ + --name='${name}' \ + --tag='${tag}' \ + --os='${os}' \ + --arch='${arch}' \ + --config='${builtins.toJSON config}' + ''; +in { + inherit splitDeps buildImage; +} diff --git a/oci-tools/Cargo.lock b/oci-tools/Cargo.lock new file mode 100644 index 0000000..2e2fe80 --- /dev/null +++ b/oci-tools/Cargo.lock @@ -0,0 +1,477 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "argh" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f384d96bfd3c0b3c41f24dae69ee9602c091d64fc432225cf5295b5abbe0036" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938e5f66269c1f168035e29ed3fb437b084e476465e9314a0328f4005d7be599" +dependencies = [ + "argh_shared", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5127f8a5bc1cfb0faf1f6248491452b8a5b6901068d8da2d47cbb285986ae683" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "const_format" +version = "0.2.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getset" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "nix2oci" +version = "0.1.0" +dependencies = [ + "anyhow", + "argh", + "hex", + "oci-spec", + "serde_json", + "sha2", +] + +[[package]] +name = "oci-spec" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8445a2631507cec628a15fdd6154b54a3ab3f20ed4fe9d73a3b8b7a4e1ba03a" +dependencies = [ + "const_format", + "derive_builder", + "getset", + "regex", + "serde", + "serde_json", + "strum", + "strum_macros", + "thiserror", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/oci-tools/Cargo.toml b/oci-tools/Cargo.toml new file mode 100644 index 0000000..70de425 --- /dev/null +++ b/oci-tools/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "nix2oci" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "nix2oci" +path = "main.rs" + +[dependencies] +hex = { version = "0.4.3" } +sha2 = { version = "0.10.9" } +anyhow = { version = "1.0.102" } +serde_json = { version = "1.0.149" } +argh = { version = "0.1.14", default-features = false } +oci-spec = { version = "0.9.0", default-features = false, features = ["image"] } diff --git a/oci-tools/default.nix b/oci-tools/default.nix new file mode 100644 index 0000000..3c57ee2 --- /dev/null +++ b/oci-tools/default.nix @@ -0,0 +1,64 @@ +{ + lib, + stdenvNoCC, + runCommand, + writeClosure, + closureInfo, + callPackage, +}: let + nix2oci = callPackage ./package.nix {}; + + defaultArch = let + inherit (stdenvNoCC.targetPlatform) system; + in ( + if lib.hasPrefix "aarch64-" system + then "arm64" + else if lib.hasPrefix "x86_64-" system + then "amd64" + else throw "unsupported system: ${system}" + ); + + # split derivation into 2 seperate closures separating the main package and it's dependencies + splitDeps = drv: let + fullClosure = closureInfo {rootPaths = [drv];}; + depsPaths = runCommand "deps-paths" {} '' + grep -v "${drv}" ${fullClosure}/store-paths > $out + ''; + drvPaths = runCommand "app-paths" {} '' + echo "${drv}" > $out + ''; + in + map (closure: {inherit closure;}) [depsPaths drvPaths]; + + buildImage = { + name, + tag ? "latest", + layers, + pathsToLink ? [], + config ? {}, + os ? "linux", + arch ? defaultArch, + }: let + writeLayerClosure = layer: + if layer ? closure + then layer.closure + else writeClosure layer; + + closureArgs = map (closure: "--closure ${closure}") (map writeLayerClosure layers); + linkPathArgs = map (path: "--link-path ${path}") pathsToLink; + joinArgs = lib.concatStringsSep " "; + in + runCommand name {nativeBuildInputs = [nix2oci];} + # sh + '' + nix2oci ${joinArgs closureArgs} ${joinArgs linkPathArgs} \ + --name '${name}' \ + --tag '${tag}' \ + --out-path $out \ + --os '${os}' \ + --arch '${arch}' \ + --config '${builtins.toJSON config}' + ''; +in { + inherit splitDeps buildImage; +} diff --git a/oci-tools/main.rs b/oci-tools/main.rs new file mode 100644 index 0000000..c8841ec --- /dev/null +++ b/oci-tools/main.rs @@ -0,0 +1,290 @@ +use std::borrow::Cow; +use std::fs; +use std::io; +use std::os::unix; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::str::FromStr; + +use anyhow::{Result, anyhow}; +use argh::FromArgs; +use oci_spec::image::{ + Arch, Config, DescriptorBuilder, Digest, ImageConfigurationBuilder, ImageIndexBuilder, + ImageManifestBuilder, MediaType, Os, RootFsBuilder, +}; +use serde_json::json; +use sha2::Sha256; +use sha2::digest::{Digest as _, Output}; + +#[derive(Debug, FromArgs)] +pub struct Opts { + #[argh(option)] + name: String, + #[argh(option, long = "closure")] + closures: Vec, + #[argh(option, long = "link-path")] + paths_to_link: Vec, + #[argh(option)] + out_path: PathBuf, + #[argh(option)] + tag: Option, + #[argh(option)] + config: String, + #[argh(option)] + os: String, + #[argh(option)] + arch: String, +} + +fn fmt_sha256(sha256: Output) -> String { + format!("sha256:{}", hex::encode(sha256)) +} + +trait ToDigest { + fn to_digest(&self) -> Result; +} + +impl ToDigest for Output { + fn to_digest(&self) -> Result { + let id = fmt_sha256(*self); + Ok(Digest::from_str(&id).unwrap()) + } +} + +fn write_blob(bytes: &[u8], blob_dir: &Path) -> Result<(Output, usize)> { + let mut hasher = Sha256::default(); + hasher.update(bytes); + let sha256 = hasher.finalize(); + + let blob_path = blob_dir.join(hex::encode(sha256)); + fs::write(blob_path, bytes)?; + + Ok((sha256, bytes.len())) +} + +fn run_tar( + blob_dir: &Path, + configure: impl FnOnce(&mut Command) -> &mut Command, +) -> Result<(Output, usize)> { + let temp_file_path = blob_dir.join("temp"); + let temp_file = fs::File::create_new(&temp_file_path)?; + + let mut cmd = Command::new("tar"); + cmd.stdout(temp_file) + .arg("--create") + .arg("--hard-dereference") + .arg("--sort=name") + .arg("--owner=0") + .arg("--group=0") + .arg("--numeric-owner"); + + configure(&mut cmd); + + let output = cmd.output()?; + if !output.status.success() { + let _ = fs::remove_file(&temp_file_path); + let stderr = String::from_utf8_lossy(&output.stderr).into_owned(); + return Err(anyhow!("tar failed: {stderr}")); + } + + let content = fs::read(&temp_file_path)?; + let result = write_blob(&content, blob_dir)?; + fs::remove_file(temp_file_path)?; + + Ok(result) +} + +fn write_layer_from_paths(paths: &[PathBuf], blob_dir: &Path) -> Result<(Output, usize)> { + run_tar(blob_dir, |cmd| cmd.args(paths)) +} + +fn write_layer_from_dir(path: &Path, blob_dir: &Path) -> Result<(Output, usize)> { + run_tar(blob_dir, |cmd| { + cmd.arg(format!("--directory={}", path.to_string_lossy())) + .arg(".") + }) +} + +fn copy_dir_all(src: &Path, dst: &Path) -> io::Result<()> { + fs::create_dir_all(dst)?; + for entry in fs::read_dir(src)? { + let entry = entry?; + if entry.file_type()?.is_dir() { + copy_dir_all(&entry.path(), &dst.join(entry.file_name()))?; + } else { + fs::copy(entry.path(), dst.join(entry.file_name()))?; + } + } + Ok(()) +} + +/// Recursively merge `src` into `dst` via symlinks, handling existing entries. +fn symlink_all(src: &Path, dst: &Path) -> io::Result<()> { + let src_meta = fs::symlink_metadata(src)?; + + if !src_meta.is_dir() { + prepare_dst(dst)?; + let src = if src_meta.is_symlink() { + Cow::Owned(fs::read_link(src)?) + } else { + Cow::Borrowed(src) + }; + unix::fs::symlink(src, dst)?; + return Ok(()); + } + + if let Ok(dst_meta) = dst.symlink_metadata() { + if !dst_meta.is_dir() { + fs::remove_file(dst)?; + } + } + fs::create_dir_all(dst)?; + + for entry in fs::read_dir(src)? { + let entry = entry?; + symlink_all(&entry.path(), &dst.join(entry.file_name()))?; + } + Ok(()) +} + +fn prepare_dst(dst: &Path) -> io::Result<()> { + if let Some(parent) = dst.parent() { + fs::create_dir_all(parent)?; + } + if let Ok(meta) = dst.symlink_metadata() { + if meta.is_dir() { + fs::remove_dir_all(dst)?; + } else { + fs::remove_file(dst)?; + } + } + Ok(()) +} + +pub fn main() -> Result<()> { + let opts: Opts = argh::from_env(); + let architecture = Arch::from(opts.arch.as_str()); + let os = Os::from(opts.os.as_str()); + + let temp_dir = Path::new(concat!("/tmp/", env!("CARGO_PKG_NAME"))); + let blob_dir = temp_dir.join("blobs/sha256"); + fs::create_dir_all(&blob_dir)?; + + let mut layer_meta = vec![]; + let mut all_drv_paths = vec![]; + + for closure_path in &opts.closures { + let closure_content = fs::read_to_string(closure_path)?; + let paths: Vec = closure_content + .lines() + .map(PathBuf::from) + .filter(|path| !all_drv_paths.contains(path)) + .collect(); + + if !paths.is_empty() { + let meta = write_layer_from_paths(&paths, &blob_dir)?; + layer_meta.push(meta); + all_drv_paths.extend(paths); + } + } + + if !opts.paths_to_link.is_empty() { + let layer_path = temp_dir.join("link-layer"); + fs::create_dir_all(&layer_path)?; + + for drv_path in &all_drv_paths { + for path_to_link in &opts.paths_to_link { + let path_to_link = path_to_link.strip_prefix("/").unwrap_or(path_to_link); + let src = drv_path.join(path_to_link); + let dst = layer_path.join(path_to_link); + if src.symlink_metadata().is_ok() { + symlink_all(&src, &dst)?; + } + } + } + + let meta = write_layer_from_dir(&layer_path, &blob_dir)?; + layer_meta.push(meta); + fs::remove_dir_all(&layer_path)?; + } + + let user_config = serde_json::from_str::(&opts.config)?; + let config = ImageConfigurationBuilder::default() + .architecture(architecture) + .os(os) + .config(user_config) + .rootfs( + RootFsBuilder::default() + .typ("layers") + .diff_ids( + layer_meta + .iter() + .map(|(sha256, _)| fmt_sha256(*sha256)) + .collect::>(), + ) + .build()?, + ) + .build()?; + + let config_bytes = serde_json::to_vec(&config)?; + let (config_hash, config_size) = write_blob(&config_bytes, &blob_dir)?; + + let manifest = ImageManifestBuilder::default() + .schema_version(2u32) + .media_type(MediaType::ImageManifest) + .config( + DescriptorBuilder::default() + .media_type(MediaType::ImageConfig) + .digest(config_hash.to_digest()?) + .size(config_size as u64) + .build()?, + ) + .layers( + layer_meta + .iter() + .map(|(sha256, size)| { + Ok(DescriptorBuilder::default() + .media_type(MediaType::ImageLayer) + .digest(sha256.to_digest()?) + .size(*size as u64) + .build()?) + }) + .collect::>>()?, + ) + .build()?; + + let manifest_bytes = serde_json::to_vec(&manifest)?; + let (manifest_hash, manifest_size) = write_blob(&manifest_bytes, &blob_dir)?; + + let index = ImageIndexBuilder::default() + .schema_version(2u32) + .media_type(MediaType::ImageIndex) + .manifests(vec![ + DescriptorBuilder::default() + .media_type(MediaType::ImageManifest) + .digest(manifest_hash.to_digest()?) + .size(manifest_size as u64) + .annotations([( + "org.opencontainers.image.ref.name".to_string(), + format!( + "{}:{}", + &opts.name, + opts.tag.as_ref().map(String::as_str).unwrap_or("latest") + ), + )]) + .build()?, + ]) + .build()?; + + let index_bytes = serde_json::to_vec(&index)?; + fs::write(temp_dir.join("index.json"), &index_bytes)?; + + let image_layout = json!({ "imageLayoutVersion": "1.0.0" }); + let image_layout_bytes = serde_json::to_vec(&image_layout)?; + fs::write(temp_dir.join("oci-layout"), &image_layout_bytes)?; + + copy_dir_all(&temp_dir, &opts.out_path)?; + fs::remove_dir_all(&temp_dir)?; + + Ok(()) +} diff --git a/oci-tools/package.nix b/oci-tools/package.nix new file mode 100644 index 0000000..ac33358 --- /dev/null +++ b/oci-tools/package.nix @@ -0,0 +1,13 @@ +{rustPlatform}: +rustPlatform.buildRustPackage (finalAttrs: { + pname = "nix2oci"; + version = "0.1.0"; + + src = ./.; + cargoLock = { + lockFile = ./Cargo.lock; + }; + + doCheck = false; + meta.mainProgram = "nix2oci"; +})