diff --git a/.bazelrc b/.bazelrc index 117762c5..5a5ec282 100644 --- a/.bazelrc +++ b/.bazelrc @@ -18,8 +18,8 @@ build --workspace_status_command=$(pwd)/workspace_status.sh # To update these lines, execute # `bazel run @rules_bazel_integration_test//tools:update_deleted_packages` # docs: https://bazel.build/reference/command-line-reference#flag--deleted_packages -build --deleted_packages=e2e/workspace,e2e/workspace/bazel_builtin,e2e/workspace/c-sources,e2e/workspace/canonical-name-module,e2e/workspace/canonical-name-module/other,e2e/workspace/cc-dependencies,e2e/workspace/cc-dependencies/shared-library,e2e/workspace/cc-dependencies/static-library,e2e/workspace/cc-dependencies/static-library-cdeps,e2e/workspace/configure-mode,e2e/workspace/configure-target,e2e/workspace/configure-threaded,e2e/workspace/configure-use_cc_common_link,e2e/workspace/configure-use_cc_common_link/shared-library,e2e/workspace/configure-use_cc_common_link/static-library,e2e/workspace/configure-version,e2e/workspace/data-dependencies,e2e/workspace/embed-file,e2e/workspace/env-attr,e2e/workspace/import-name-attr,e2e/workspace/import-names-attr,e2e/workspace/include-dependencies,e2e/workspace/include-dependencies/zig-include,e2e/workspace/include-dependencies/zig-include-define,e2e/workspace/include-dependencies/zig-include-isystem,e2e/workspace/include-dependencies/zig-std-include,e2e/workspace/link-dependencies,e2e/workspace/link-dependencies/shared-library,e2e/workspace/link-dependencies/static-library,e2e/workspace/linker-script,e2e/workspace/linkopts-attr,e2e/workspace/location-expansion,e2e/workspace/multiple-sources-and-packages-test,e2e/workspace/multiple-sources-binary,e2e/workspace/root-module-from-single-dependency,e2e/workspace/runfiles-library,e2e/workspace/runfiles-library/dependency,e2e/workspace/runfiles-library/dependency/transitive-dependency,e2e/workspace/simple-binary,e2e/workspace/simple-cmake-library,e2e/workspace/simple-library,e2e/workspace/simple-shared-library,e2e/workspace/simple-test,e2e/workspace/test-runner-attr,e2e/workspace/third_party/arocc,e2e/workspace/third_party/translate-c,e2e/workspace/toolchain-glibc-version,e2e/workspace/transitive-zig-modules-binary,e2e/workspace/transitive-zig-modules-binary/hello-world,e2e/workspace/transitive-zig-modules-binary/hello-world/data,e2e/workspace/transitive-zig-modules-binary/hello-world/data/hello,e2e/workspace/transitive-zig-modules-binary/hello-world/data/world,e2e/workspace/transitive-zig-modules-binary/hello-world/io,e2e/workspace/translate-c/transitive-cc-library-zig-binary,e2e/workspace/zig-docs,e2e/workspace/zig-header,e2e/workspace/zig-module-binary,e2e/workspace/zig-module-binary/data,e2e/workspace/zig-module-binary/io,zig/tests/integration_tests/minimal,zig/tests/integration_tests/mirrors,zig/tests/integration_tests/workspace,zig/tests/integration_tests/workspace/custom_interpreter,zig/tests/integration_tests/workspace/env-attr,zig/tests/integration_tests/workspace/runfiles -query --deleted_packages=e2e/workspace,e2e/workspace/bazel_builtin,e2e/workspace/c-sources,e2e/workspace/canonical-name-module,e2e/workspace/canonical-name-module/other,e2e/workspace/cc-dependencies,e2e/workspace/cc-dependencies/shared-library,e2e/workspace/cc-dependencies/static-library,e2e/workspace/cc-dependencies/static-library-cdeps,e2e/workspace/configure-mode,e2e/workspace/configure-target,e2e/workspace/configure-threaded,e2e/workspace/configure-use_cc_common_link,e2e/workspace/configure-use_cc_common_link/shared-library,e2e/workspace/configure-use_cc_common_link/static-library,e2e/workspace/configure-version,e2e/workspace/data-dependencies,e2e/workspace/embed-file,e2e/workspace/env-attr,e2e/workspace/import-name-attr,e2e/workspace/import-names-attr,e2e/workspace/include-dependencies,e2e/workspace/include-dependencies/zig-include,e2e/workspace/include-dependencies/zig-include-define,e2e/workspace/include-dependencies/zig-include-isystem,e2e/workspace/include-dependencies/zig-std-include,e2e/workspace/link-dependencies,e2e/workspace/link-dependencies/shared-library,e2e/workspace/link-dependencies/static-library,e2e/workspace/linker-script,e2e/workspace/linkopts-attr,e2e/workspace/location-expansion,e2e/workspace/multiple-sources-and-packages-test,e2e/workspace/multiple-sources-binary,e2e/workspace/root-module-from-single-dependency,e2e/workspace/runfiles-library,e2e/workspace/runfiles-library/dependency,e2e/workspace/runfiles-library/dependency/transitive-dependency,e2e/workspace/simple-binary,e2e/workspace/simple-cmake-library,e2e/workspace/simple-library,e2e/workspace/simple-shared-library,e2e/workspace/simple-test,e2e/workspace/test-runner-attr,e2e/workspace/third_party/arocc,e2e/workspace/third_party/translate-c,e2e/workspace/toolchain-glibc-version,e2e/workspace/transitive-zig-modules-binary,e2e/workspace/transitive-zig-modules-binary/hello-world,e2e/workspace/transitive-zig-modules-binary/hello-world/data,e2e/workspace/transitive-zig-modules-binary/hello-world/data/hello,e2e/workspace/transitive-zig-modules-binary/hello-world/data/world,e2e/workspace/transitive-zig-modules-binary/hello-world/io,e2e/workspace/translate-c/transitive-cc-library-zig-binary,e2e/workspace/zig-docs,e2e/workspace/zig-header,e2e/workspace/zig-module-binary,e2e/workspace/zig-module-binary/data,e2e/workspace/zig-module-binary/io,zig/tests/integration_tests/minimal,zig/tests/integration_tests/mirrors,zig/tests/integration_tests/workspace,zig/tests/integration_tests/workspace/custom_interpreter,zig/tests/integration_tests/workspace/env-attr,zig/tests/integration_tests/workspace/runfiles +build --deleted_packages=e2e/workspace,e2e/workspace/bazel_builtin,e2e/workspace/c-sources,e2e/workspace/canonical-name-module,e2e/workspace/canonical-name-module/other,e2e/workspace/cc-dependencies,e2e/workspace/cc-dependencies/shared-library,e2e/workspace/cc-dependencies/static-library,e2e/workspace/cc-dependencies/static-library-cdeps,e2e/workspace/configure-mode,e2e/workspace/configure-target,e2e/workspace/configure-threaded,e2e/workspace/configure-use_cc_common_link,e2e/workspace/configure-use_cc_common_link/shared-library,e2e/workspace/configure-use_cc_common_link/static-library,e2e/workspace/configure-version,e2e/workspace/data-dependencies,e2e/workspace/embed-file,e2e/workspace/env-attr,e2e/workspace/import-name-attr,e2e/workspace/import-names-attr,e2e/workspace/include-dependencies,e2e/workspace/include-dependencies/zig-include,e2e/workspace/include-dependencies/zig-include-define,e2e/workspace/include-dependencies/zig-include-isystem,e2e/workspace/include-dependencies/zig-std-include,e2e/workspace/link-dependencies,e2e/workspace/link-dependencies/shared-library,e2e/workspace/link-dependencies/static-library,e2e/workspace/linker-script,e2e/workspace/linkopts-attr,e2e/workspace/location-expansion,e2e/workspace/multiple-sources-and-packages-test,e2e/workspace/multiple-sources-binary,e2e/workspace/root-module-from-single-dependency,e2e/workspace/runfiles-library,e2e/workspace/runfiles-library/dependency,e2e/workspace/runfiles-library/dependency/transitive-dependency,e2e/workspace/simple-binary,e2e/workspace/simple-cmake-library,e2e/workspace/simple-library,e2e/workspace/simple-shared-library,e2e/workspace/simple-test,e2e/workspace/test-runner-attr,e2e/workspace/third_party/arocc,e2e/workspace/third_party/translate-c,e2e/workspace/toolchain-glibc-version,e2e/workspace/transitive-zig-modules-binary,e2e/workspace/transitive-zig-modules-binary/hello-world,e2e/workspace/transitive-zig-modules-binary/hello-world/data,e2e/workspace/transitive-zig-modules-binary/hello-world/data/hello,e2e/workspace/transitive-zig-modules-binary/hello-world/data/world,e2e/workspace/transitive-zig-modules-binary/hello-world/io,e2e/workspace/translate-c/transitive-cc-library-zig-binary,e2e/workspace/zig-docs,e2e/workspace/zig-header,e2e/workspace/zig-module-binary,e2e/workspace/zig-module-binary/data,e2e/workspace/zig-module-binary/io,e2e/workspace/zig-package-import/app,e2e/workspace/zig-package-import/greet,zig/tests/integration_tests/minimal,zig/tests/integration_tests/mirrors,zig/tests/integration_tests/packages,zig/tests/integration_tests/packages/child,zig/tests/integration_tests/packages/path_deps/greeter,zig/tests/integration_tests/packages/path_deps/message,zig/tests/integration_tests/packages/tools,zig/tests/integration_tests/workspace,zig/tests/integration_tests/workspace/custom_interpreter,zig/tests/integration_tests/workspace/env-attr,zig/tests/integration_tests/workspace/runfiles +query --deleted_packages=e2e/workspace,e2e/workspace/bazel_builtin,e2e/workspace/c-sources,e2e/workspace/canonical-name-module,e2e/workspace/canonical-name-module/other,e2e/workspace/cc-dependencies,e2e/workspace/cc-dependencies/shared-library,e2e/workspace/cc-dependencies/static-library,e2e/workspace/cc-dependencies/static-library-cdeps,e2e/workspace/configure-mode,e2e/workspace/configure-target,e2e/workspace/configure-threaded,e2e/workspace/configure-use_cc_common_link,e2e/workspace/configure-use_cc_common_link/shared-library,e2e/workspace/configure-use_cc_common_link/static-library,e2e/workspace/configure-version,e2e/workspace/data-dependencies,e2e/workspace/embed-file,e2e/workspace/env-attr,e2e/workspace/import-name-attr,e2e/workspace/import-names-attr,e2e/workspace/include-dependencies,e2e/workspace/include-dependencies/zig-include,e2e/workspace/include-dependencies/zig-include-define,e2e/workspace/include-dependencies/zig-include-isystem,e2e/workspace/include-dependencies/zig-std-include,e2e/workspace/link-dependencies,e2e/workspace/link-dependencies/shared-library,e2e/workspace/link-dependencies/static-library,e2e/workspace/linker-script,e2e/workspace/linkopts-attr,e2e/workspace/location-expansion,e2e/workspace/multiple-sources-and-packages-test,e2e/workspace/multiple-sources-binary,e2e/workspace/root-module-from-single-dependency,e2e/workspace/runfiles-library,e2e/workspace/runfiles-library/dependency,e2e/workspace/runfiles-library/dependency/transitive-dependency,e2e/workspace/simple-binary,e2e/workspace/simple-cmake-library,e2e/workspace/simple-library,e2e/workspace/simple-shared-library,e2e/workspace/simple-test,e2e/workspace/test-runner-attr,e2e/workspace/third_party/arocc,e2e/workspace/third_party/translate-c,e2e/workspace/toolchain-glibc-version,e2e/workspace/transitive-zig-modules-binary,e2e/workspace/transitive-zig-modules-binary/hello-world,e2e/workspace/transitive-zig-modules-binary/hello-world/data,e2e/workspace/transitive-zig-modules-binary/hello-world/data/hello,e2e/workspace/transitive-zig-modules-binary/hello-world/data/world,e2e/workspace/transitive-zig-modules-binary/hello-world/io,e2e/workspace/translate-c/transitive-cc-library-zig-binary,e2e/workspace/zig-docs,e2e/workspace/zig-header,e2e/workspace/zig-module-binary,e2e/workspace/zig-module-binary/data,e2e/workspace/zig-module-binary/io,e2e/workspace/zig-package-import/app,e2e/workspace/zig-package-import/greet,zig/tests/integration_tests/minimal,zig/tests/integration_tests/mirrors,zig/tests/integration_tests/packages,zig/tests/integration_tests/packages/child,zig/tests/integration_tests/packages/path_deps/greeter,zig/tests/integration_tests/packages/path_deps/message,zig/tests/integration_tests/packages/tools,zig/tests/integration_tests/workspace,zig/tests/integration_tests/workspace/custom_interpreter,zig/tests/integration_tests/workspace/env-attr,zig/tests/integration_tests/workspace/runfiles # Load any settings specific to the current user. # Place settings that should affect the integration tests into `.bazelrc.ic.user`. diff --git a/MODULE.bazel b/MODULE.bazel index ae9e5680..938de727 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -35,6 +35,10 @@ register_toolchains("@rules_zig//zig/target:all") register_toolchains("@zig_toolchains//:all") +zig_host_toolchain = use_repo_rule("//zig/private/repo:zig_host_toolchain.bzl", "zig_host_toolchain") + +zig_host_toolchain(name = "rules_zig_host_toolchain") + zls = use_extension("//zig/zls:extensions.bzl", "zls") zls.index(file = "//zig/zls/private:versions.json") use_repo(zls, "zls_toolchains") diff --git a/e2e/workspace/.bazelrc b/e2e/workspace/.bazelrc index 02f4efd0..541ebaf9 100644 --- a/e2e/workspace/.bazelrc +++ b/e2e/workspace/.bazelrc @@ -5,3 +5,7 @@ try-import %workspace%/.bazelrc.user # docs: https://bazel.build/reference/command-line-reference#flag--workspace_status_command build --workspace_status_command=$(pwd)/workspace_status.sh + +# The importer requires the reworked Zig build system (configurer + `std` APIs), +# see https://codeberg.org/ziglang/zig/pulls/35428. +common --repo_env=RULES_ZIG_HOST_SDK=0.17.0-dev.813+2153f8143 diff --git a/e2e/workspace/MODULE.bazel b/e2e/workspace/MODULE.bazel index 3573cbc3..f610eb0b 100644 --- a/e2e/workspace/MODULE.bazel +++ b/e2e/workspace/MODULE.bazel @@ -15,10 +15,24 @@ zig = use_extension( "zig", dev_dependency = True, ) -zig.toolchain(zig_version = "0.16.0") +zig.index(file = "extra-versions.json") +zig.toolchain( + default = True, + zig_version = "0.16.0", +) zig.toolchain(zig_version = "0.15.2") +zig.toolchain(zig_version = "0.17.0-dev.813+2153f8143") use_repo(zig, "zig_toolchains") +zig_packages = use_extension( + "@rules_zig//zig:packages.bzl", + "zig_packages", + dev_dependency = True, +) +zig_packages.from_file(build_zig_zon = "//zig-package-import/app:build.zig.zon") +zig_packages.from_file(build_zig_zon = "//zig-package-import/greet:build.zig.zon") +use_repo(zig_packages, "zig_deps") + http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") http_archive( diff --git a/e2e/workspace/extra-versions.json b/e2e/workspace/extra-versions.json new file mode 100644 index 00000000..8929cf19 --- /dev/null +++ b/e2e/workspace/extra-versions.json @@ -0,0 +1,148 @@ +{ + "master": { + "version": "0.17.0-dev.813+2153f8143", + "date": "2026-06-07", + "docs": "https://ziglang.org/documentation/master/", + "stdDocs": "https://ziglang.org/documentation/master/std/", + "src": { + "tarball": "https://ziglang.org/builds/zig-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "bcb2ec46a2353620f2d90fcda1b046895ac7ba16b6b9febc9a0f9bd48f568c31", + "size": "22688996" + }, + "bootstrap": { + "tarball": "https://ziglang.org/builds/zig-bootstrap-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "d93ea88bcd934b24695de5e445f3640d8e04afc544f88dbed6fef1d91c4c5292", + "size": "56681092" + }, + "x86_64-macos": { + "tarball": "https://ziglang.org/builds/zig-x86_64-macos-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "3938c46ae4bca3c13f423b09503e3ef00bb4b7ef12b8bc1e5122ede366057a5b", + "size": "59323140" + }, + "aarch64-macos": { + "tarball": "https://ziglang.org/builds/zig-aarch64-macos-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "36673d2513afa4a96c86780648ba504beedd7f0451389091cf9d53e38d5b4840", + "size": "53999760" + }, + "x86_64-linux": { + "tarball": "https://ziglang.org/builds/zig-x86_64-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "b0d46ffc4587b9e8dd0b524ee5bc4da1e67f28bba55e7c534cec64af2f2d7a74", + "size": "57296196" + }, + "aarch64-linux": { + "tarball": "https://ziglang.org/builds/zig-aarch64-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "aa67b418d50bdde3043cfe765016d5387a2333b514ada2c57f24baae4005c331", + "size": "52949940" + }, + "arm-linux": { + "tarball": "https://ziglang.org/builds/zig-arm-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "7e365014a7520ca405b18d8690d802592199ce178d9a3bbfa526b1417edcfaa5", + "size": "53772136" + }, + "riscv64-linux": { + "tarball": "https://ziglang.org/builds/zig-riscv64-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "e2282b0784722eb3e41a41dcdea257618bc02c6b10fdd462c5f8e514b09628da", + "size": "57219904" + }, + "powerpc64le-linux": { + "tarball": "https://ziglang.org/builds/zig-powerpc64le-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "4b4ffe65aa052e1399319f4d80b5d117e6902d514d52d7bca5dfce66153474ec", + "size": "57144644" + }, + "x86-linux": { + "tarball": "https://ziglang.org/builds/zig-x86-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "cb14dab396b988221e02c4736680a0e166048d5592fafc4ba2fde362d3bc78b3", + "size": "59897932" + }, + "loongarch64-linux": { + "tarball": "https://ziglang.org/builds/zig-loongarch64-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "9a078bfac64a0d51752de87eb31348d33035451043f9a03546bf57d05c070a43", + "size": "54315620" + }, + "s390x-linux": { + "tarball": "https://ziglang.org/builds/zig-s390x-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "c631cf79d8a405edf9d941c621131ac262d4468566cdbf1442087f764df46eb7", + "size": "57127508" + }, + "x86_64-windows": { + "tarball": "https://ziglang.org/builds/zig-x86_64-windows-0.17.0-dev.813+2153f8143.zip", + "shasum": "2a8f1a34402076ab7931e4535bd379b20c83fc263d1387cb3f70cb2e397f9ebe", + "size": "101062660" + }, + "aarch64-windows": { + "tarball": "https://ziglang.org/builds/zig-aarch64-windows-0.17.0-dev.813+2153f8143.zip", + "shasum": "c335026c4b666a995ac2f4d5481f74f7f9a455dd7ab3620b3e04779d3f6055a8", + "size": "96847518" + }, + "x86-windows": { + "tarball": "https://ziglang.org/builds/zig-x86-windows-0.17.0-dev.813+2153f8143.zip", + "shasum": "f341544a7263651616c7ce292e23a257c85f892ac72c0eeea9a7f0c6e93351da", + "size": "102769164" + }, + "aarch64-freebsd": { + "tarball": "https://ziglang.org/builds/zig-aarch64-freebsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "aa0da8ed92903cb7e32b18bccd4d6d5770192105749af6053ca5faafe6afdfbe", + "size": "52893704" + }, + "arm-freebsd": { + "tarball": "https://ziglang.org/builds/zig-arm-freebsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "60b75fa5e895e9ef01b9a745495bd1ed48078a7a8bebd546278bda82be27dc1d", + "size": "54464888" + }, + "powerpc64le-freebsd": { + "tarball": "https://ziglang.org/builds/zig-powerpc64le-freebsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "22c735fb40f8d0ad7342660a6a8a405615f27b66432dd5021efca1267b51191c", + "size": "57177396" + }, + "riscv64-freebsd": { + "tarball": "https://ziglang.org/builds/zig-riscv64-freebsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "0876f2330308279466a6d86630d3510f0498d94e11238c5653bee7532e6f078d", + "size": "57367100" + }, + "x86_64-freebsd": { + "tarball": "https://ziglang.org/builds/zig-x86_64-freebsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "a75220898fe9f403d24e9712ef56fa7cf622ddb96be21f2d2b7f01535a4f26a9", + "size": "57464308" + }, + "aarch64-netbsd": { + "tarball": "https://ziglang.org/builds/zig-aarch64-netbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "c004ad712df83a2eed7a0724fd582a08bb09eaee262996c4e6b82680a22d7b6f", + "size": "52847860" + }, + "arm-netbsd": { + "tarball": "https://ziglang.org/builds/zig-arm-netbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "611fdc70b82e6bf88053a72efba73a788962ef0bd9008336e0202bb43eb67967", + "size": "55526764" + }, + "x86-netbsd": { + "tarball": "https://ziglang.org/builds/zig-x86-netbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "1296c141a8603a0911547e60532c63638cf5dd859ab4acf2ead33a925d64776b", + "size": "60476932" + }, + "x86_64-netbsd": { + "tarball": "https://ziglang.org/builds/zig-x86_64-netbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "bd88fad25ef1251d74a8051c30b886256c8b0f0292b4209b1013e925b54cd314", + "size": "57360376" + }, + "aarch64-openbsd": { + "tarball": "https://ziglang.org/builds/zig-aarch64-openbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "7730b3b2bc98317c154fda750a74790a3e6e79526a868a9bc04d63a9bb71c79e", + "size": "53347332" + }, + "arm-openbsd": { + "tarball": "https://ziglang.org/builds/zig-arm-openbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "d067209109995e5311c2f196f75fec59124d55aa140b5727c9b2eb386f46cf6a", + "size": "53971816" + }, + "riscv64-openbsd": { + "tarball": "https://ziglang.org/builds/zig-riscv64-openbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "32a6df5e4e0a3647ac4354b0a7f103a6bab9d0c74225ec5c0e0d9aea0f4d5d74", + "size": "57655868" + }, + "x86_64-openbsd": { + "tarball": "https://ziglang.org/builds/zig-x86_64-openbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "fb173bd2caff90b7483a513f7759181de0b4f60d87698f89bc7caf8150a00e0a", + "size": "58620540" + } + } +} diff --git a/e2e/workspace/zig-package-import/.gitignore b/e2e/workspace/zig-package-import/.gitignore new file mode 100644 index 00000000..9411c9d1 --- /dev/null +++ b/e2e/workspace/zig-package-import/.gitignore @@ -0,0 +1,3 @@ +zig-pkg/ +zig-out/ +.zig-cache/ diff --git a/e2e/workspace/zig-package-import/app/BUILD.bazel b/e2e/workspace/zig-package-import/app/BUILD.bazel new file mode 100644 index 00000000..2c154212 --- /dev/null +++ b/e2e/workspace/zig-package-import/app/BUILD.bazel @@ -0,0 +1,39 @@ +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("@bazel_skylib//rules:diff_test.bzl", "diff_test") +load("@rules_zig//zig:defs.bzl", "zig_binary") +load("@zig_deps//:defs.bzl", "zig_deps") + +zig_binary( + name = "app", + srcs = glob( + ["src/**/*.zig"], + exclude = ["src/main.zig"], + ), + main = "src/main.zig", + # `main.zig` and its dependencies (`clap`, `cbor`, `ezi_gex`) require Zig 0.16+. + target_compatible_with = select({ + "@zig_toolchains//:0.15.2": ["@platforms//:incompatible"], + "//conditions:default": [], + }), + deps = zig_deps(), +) + +genrule( + name = "output", + outs = ["output.actual"], + cmd = "$(execpath :app) > $(OUTS)", + tools = [":app"], +) + +diff_test( + name = "output_test", + size = "small", + file1 = ":output.expected", + file2 = ":output.actual", +) + +build_test( + name = "build_test", + size = "small", + targets = [":app"], +) diff --git a/e2e/workspace/zig-package-import/app/build.zig b/e2e/workspace/zig-package-import/app/build.zig new file mode 100644 index 00000000..7ae012e3 --- /dev/null +++ b/e2e/workspace/zig-package-import/app/build.zig @@ -0,0 +1,27 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const clap = b.dependency("clap", .{ + .target = target, + .optimize = optimize, + }); + const greet = b.dependency("greet", .{ + .target = target, + .optimize = optimize, + }); + + const exe = b.addExecutable(.{ + .name = "app", + .root_module = b.createModule(.{ + .root_source_file = b.path("src/main.zig"), + .target = target, + .optimize = optimize, + }), + }); + exe.root_module.addImport("clap", clap.module("clap")); + exe.root_module.addImport("greet", greet.module("greet")); + b.installArtifact(exe); +} diff --git a/e2e/workspace/zig-package-import/app/build.zig.zon b/e2e/workspace/zig-package-import/app/build.zig.zon new file mode 100644 index 00000000..ad5c5db5 --- /dev/null +++ b/e2e/workspace/zig-package-import/app/build.zig.zon @@ -0,0 +1,29 @@ +.{ + .name = .app, + .version = "0.1.0", + .fingerprint = 0xc96e70cfecd7faf1, + .minimum_zig_version = "0.16.0", + .dependencies = .{ + .clap = .{ + .url = "https://github.com/Hejsil/zig-clap/archive/8d97efa1ee1e575443c7888d5c38e1c3fc145cf5.tar.gz", + .hash = "clap-0.12.0-oBajB7foAQDqlSwaSG5g0yq7xGbQARUsBk5T64gAOqP5", + }, + .greet = .{ + .path = "../greet", + }, + .cbor = .{ + // Intentionally a git+https URL to test that fetch mechanism. + .url = "git+https://github.com/neurocyte/cbor.git#0d9cab516a1508c832c685183aeac0365b9a224a", + .hash = "cbor-1.3.0-RcQE_Mx9AQBfuyJs1eAURJfqdG_5vTJ-VqYCNgpdXrHJ", + }, + .ezi_gex = .{ + .url = "git+https://github.com/shaik-abdul-thouhid/ezi-gex.git#ee198a599ecd55baa108395788979c0eb40675f5", + .hash = "ezi_gex-0.5.0-dev-fTQAPBMbFwDJ7MdWAT2OIXD1RdXWbO6gWcc1hzqbGD80", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/e2e/workspace/zig-package-import/app/output.expected b/e2e/workspace/zig-package-import/app/output.expected new file mode 100644 index 00000000..c8fc619c --- /dev/null +++ b/e2e/workspace/zig-package-import/app/output.expected @@ -0,0 +1,3 @@ +Hello world! +cbor: 13 bytes +regex match: bob@example diff --git a/e2e/workspace/zig-package-import/app/src/main.zig b/e2e/workspace/zig-package-import/app/src/main.zig new file mode 100644 index 00000000..b200a70e --- /dev/null +++ b/e2e/workspace/zig-package-import/app/src/main.zig @@ -0,0 +1,44 @@ +const std = @import("std"); +const clap = @import("clap"); +const greet = @import("greet"); +const cbor = @import("cbor"); +const gex = @import("ezi_gex"); + +pub fn main(init: std.process.Init) !void { + var diag = clap.Diagnostic{}; + var res = clap.parse(clap.Help, &greet.params, clap.parsers.default, init.minimal.args, .{ + .diagnostic = &diag, + .allocator = init.gpa, + }) catch |err| { + try diag.reportToFile(init.io, .stderr(), err); + return err; + }; + defer res.deinit(); + + if (res.args.help != 0) + return clap.helpToFile(init.io, .stderr(), clap.Help, &greet.params, .{}); + + const message = try greet.greeting(init.gpa, res); + defer init.gpa.free(message); + + // CBOR-encode the greeting using the git+https `cbor` dependency. + var cbor_buf: [256]u8 = undefined; + const encoded = cbor.fmt(&cbor_buf, message); + + // Match a pattern with the multi-module `ezi_gex` regex engine. + var gex_diag: gex.Diagnostic = .{}; + var re = try gex.compileRuntime(init.gpa, "(?\\w+)@(?\\w+)", &gex_diag, .{}); + defer re.deinit(); + var scratch = try @TypeOf(re).Scratch.init(init.gpa, &re.program); + defer scratch.deinit(init.gpa); + const haystack = "contact: bob@example"; + const match = if (re.find(&scratch, haystack)) |m| m.slice(haystack) else "(none)"; + + var stdout_buf: [512]u8 = undefined; + var stdout_writer = std.Io.File.stdout().writer(init.io, &stdout_buf); + const stdout = &stdout_writer.interface; + try stdout.print("{s}\n", .{message}); + try stdout.print("cbor: {d} bytes\n", .{encoded.len}); + try stdout.print("regex match: {s}\n", .{match}); + try stdout.flush(); +} diff --git a/e2e/workspace/zig-package-import/greet/BUILD.bazel b/e2e/workspace/zig-package-import/greet/BUILD.bazel new file mode 100644 index 00000000..80ffc29f --- /dev/null +++ b/e2e/workspace/zig-package-import/greet/BUILD.bazel @@ -0,0 +1,21 @@ +load("@rules_zig//zig:defs.bzl", "zig_library") +load("@zig_deps//:defs.bzl", "zig_dep") + +# A local path dependency must expose a module named after the dependency so +# that dependents can resolve it through the hub. +zig_library( + name = "greet", + srcs = glob( + ["src/**/*.zig"], + exclude = ["src/greet.zig"], + ), + import_name = "greet", + main = "src/greet.zig", + # Depends on `clap`, which uses the Zig 0.16+. + target_compatible_with = select({ + "@zig_toolchains//:0.15.2": ["@platforms//:incompatible"], + "//conditions:default": [], + }), + visibility = ["//visibility:public"], + deps = [zig_dep("clap")], +) diff --git a/e2e/workspace/zig-package-import/greet/build.zig b/e2e/workspace/zig-package-import/greet/build.zig new file mode 100644 index 00000000..9349f46b --- /dev/null +++ b/e2e/workspace/zig-package-import/greet/build.zig @@ -0,0 +1,18 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const clap = b.dependency("clap", .{ + .target = target, + .optimize = optimize, + }); + + const mod = b.addModule("greet", .{ + .root_source_file = b.path("src/greet.zig"), + .target = target, + .optimize = optimize, + }); + mod.addImport("clap", clap.module("clap")); +} diff --git a/e2e/workspace/zig-package-import/greet/build.zig.zon b/e2e/workspace/zig-package-import/greet/build.zig.zon new file mode 100644 index 00000000..403fb19e --- /dev/null +++ b/e2e/workspace/zig-package-import/greet/build.zig.zon @@ -0,0 +1,17 @@ +.{ + .name = .greet, + .version = "0.1.0", + .fingerprint = 0x2df8175bd2f91638, + .minimum_zig_version = "0.16.0", + .dependencies = .{ + .clap = .{ + .url = "https://github.com/Hejsil/zig-clap/archive/8d97efa1ee1e575443c7888d5c38e1c3fc145cf5.tar.gz", + .hash = "clap-0.12.0-oBajB7foAQDqlSwaSG5g0yq7xGbQARUsBk5T64gAOqP5", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/e2e/workspace/zig-package-import/greet/src/greet.zig b/e2e/workspace/zig-package-import/greet/src/greet.zig new file mode 100644 index 00000000..d9a928f7 --- /dev/null +++ b/e2e/workspace/zig-package-import/greet/src/greet.zig @@ -0,0 +1,14 @@ +const std = @import("std"); +const clap = @import("clap"); + +// contrived example to introduce a transitive clap dependency +pub const params = clap.parseParamsComptime( + \\-h, --help Display this help and exit. + \\-n, --name Name to greet (defaults to "world"). + \\ +); + +pub fn greeting(allocator: std.mem.Allocator, res: anytype) ![]u8 { + const name = res.args.name orelse "world"; + return std.fmt.allocPrint(allocator, "Hello {s}!", .{name}); +} diff --git a/zig/BUILD.bazel b/zig/BUILD.bazel index 6e77cd79..fa2695db 100644 --- a/zig/BUILD.bazel +++ b/zig/BUILD.bazel @@ -6,6 +6,7 @@ exports_files( [ "defs.bzl", "extensions.bzl", + "packages.bzl", "toolchain.bzl", ], visibility = ["//docs:__pkg__"], @@ -75,6 +76,13 @@ bzl_library( ], ) +bzl_library( + name = "packages", + srcs = ["packages.bzl"], + visibility = ["//visibility:public"], + deps = ["//zig/private/bzlmod:zig_packages"], +) + # Execute `bazel run //util:update_filegroups` to update this target. filegroup( name = "all_files", @@ -82,6 +90,7 @@ filegroup( ":BUILD.bazel", ":defs.bzl", ":extensions.bzl", + ":packages.bzl", ":toolchain.bzl", "//zig/config:all_files", "//zig/lib:all_files", diff --git a/zig/packages.bzl b/zig/packages.bzl new file mode 100644 index 00000000..b4053f4b --- /dev/null +++ b/zig/packages.bzl @@ -0,0 +1,5 @@ +"""Extension for importing Zig package dependencies.""" + +load("//zig/private/bzlmod:zig_packages.bzl", _zig_packages = "zig_packages") + +zig_packages = _zig_packages diff --git a/zig/private/BUILD.bazel b/zig/private/BUILD.bazel index 36ee7fce..2b0cfd78 100644 --- a/zig/private/BUILD.bazel +++ b/zig/private/BUILD.bazel @@ -175,6 +175,8 @@ filegroup( ":BUILD.bazel", ":cc_helper.bzl", ":cc_linkopts.bzl", + ":configurer.zig", + ":package_prefix.zig", ":platforms.bzl", ":resolved_target_toolchain.bzl", ":resolved_toolchain.bzl", @@ -192,6 +194,7 @@ filegroup( ":zig_test.bzl", ":zig_toolchain.bzl", ":zig_toolchain_header.bzl", + ":zon2json.zig", "//zig/private/bzlmod:all_files", "//zig/private/common:all_files", "//zig/private/providers:all_files", diff --git a/zig/private/bzlmod/BUILD.bazel b/zig/private/bzlmod/BUILD.bazel index ef90ceb9..251c209d 100644 --- a/zig/private/bzlmod/BUILD.bazel +++ b/zig/private/bzlmod/BUILD.bazel @@ -11,6 +11,17 @@ bzl_library( ], ) +bzl_library( + name = "zig_packages", + srcs = ["zig_packages.bzl"], + visibility = ["//zig:__subpackages__"], + deps = [ + "//zig/private/repo:zig_deps_hub", + "//zig/private/repo:zig_package", + "@rules_zig_host_toolchain//:toolchain", + ], +) + bzl_library( name = "cc_common_link", srcs = ["cc_common_link.bzl"], @@ -24,6 +35,7 @@ filegroup( ":BUILD.bazel", ":cc_common_link.bzl", ":zig.bzl", + ":zig_packages.bzl", ], visibility = ["//zig/private:__pkg__"], ) diff --git a/zig/private/bzlmod/zig.bzl b/zig/private/bzlmod/zig.bzl index 3aa4853c..dbaaf792 100644 --- a/zig/private/bzlmod/zig.bzl +++ b/zig/private/bzlmod/zig.bzl @@ -302,6 +302,7 @@ def _toolchain_extension(module_ctx): toolchain_names = [] toolchain_labels = [] toolchain_zig_versions = [] + toolchain_exec_platforms = [] toolchain_exec_lengths = [] toolchain_exec_constraints = [] toolchain_target_compatible_lengths = [] @@ -332,6 +333,7 @@ def _toolchain_extension(module_ctx): toolchain_names.append(name) toolchain_labels.append("@{}//:zig_toolchain".format(repo_name)) toolchain_zig_versions.append(zig_version) + toolchain_exec_platforms.append(platform) toolchain_exec_lengths.append(len(compatible_with)) toolchain_exec_constraints.extend(compatible_with) toolchain_target_compatible_lengths.append(len(variant.extra_target_compatible_with)) @@ -344,6 +346,7 @@ def _toolchain_extension(module_ctx): names = toolchain_names, labels = toolchain_labels, zig_versions = toolchain_zig_versions, + exec_platforms = toolchain_exec_platforms, exec_lengths = toolchain_exec_lengths, exec_constraints = toolchain_exec_constraints, target_compatible_lengths = toolchain_target_compatible_lengths, diff --git a/zig/private/bzlmod/zig_packages.bzl b/zig/private/bzlmod/zig_packages.bzl new file mode 100644 index 00000000..5ecc88d7 --- /dev/null +++ b/zig/private/bzlmod/zig_packages.bzl @@ -0,0 +1,475 @@ +"""Implementation of the `zig_packages` module extension.""" + +load("@rules_zig_host_toolchain//:toolchain.bzl", "zig_cache", "zig_path") +load("//zig/private/repo:zig_deps_hub.bzl", "zig_deps_hub") +load("//zig/private/repo:zig_package.bzl", "zig_package") + +from_file = tag_class( + attrs = { + "build_zig_zon": attr.label( + doc = "A `build.zig.zon` manifest to resolve Zig dependencies for.", + mandatory = True, + allow_single_file = True, + ), + }, +) + +system_library = tag_class( + attrs = { + "name": attr.string( + doc = "The name of the system library as passed to `linkSystemLibrary` in a package's `build.zig`.", + mandatory = True, + ), + "lib": attr.label( + doc = "A `cc_library` that provides the named system library.", + mandatory = True, + ), + }, +) + +system_integration = tag_class( + attrs = { + "name": attr.string( + doc = "The name of an optional system integration (`systemIntegrationOption`) to enable.", + mandatory = True, + ), + }, +) + +config = tag_class( + attrs = { + "name": attr.string(mandatory = True, doc = "Module-local name of this configuration cell."), + "optimize": attr.string(doc = "Zig optimize mode: debug | release_safe | release_small | release_fast."), + "select_on": attr.string_list(doc = "Extra Bazel condition labels ANDed into this cell's `select()` branch."), + "zig_flags": attr.string_list(doc = "Extra `-D` build options (NAME=VALUE) passed to the configurer for this cell."), + }, +) + +configure = tag_class( + attrs = { + "configs": attr.string_list(mandatory = True, doc = "Ordered config names that apply."), + "fallback": attr.string(mandatory = True, doc = "The config name used for the `//conditions:default` branch."), + "package": attr.string(doc = "If set, override the matrix for the named package only; otherwise global."), + "version": attr.string(doc = "Disambiguate `package` by version."), + }, +) + +_OPTIMIZE_MODES = { + "debug": "Debug", + "release_safe": "ReleaseSafe", + "release_small": "ReleaseSmall", + "release_fast": "ReleaseFast", +} + +def resolve_cell(tag): + """Resolve a `config` tag into a build-configuration matrix cell. + + `optimize` expands to `//zig/config/mode:mode` and `-Doptimize=Mode`; + `select_on` is appended verbatim and `zig_flags` become `-DNAME=VALUE`. + + Args: + tag: a `config` tag. + + Returns: + `(error, cell)`, `cell`: `struct(name, select_on, zig_options)`, where + `zig_options` is a list of `-DNAME=VALUE` Zig build option flags. + """ + select_on = [] + zig_options = [] + + if tag.optimize: + mode = _OPTIMIZE_MODES.get(tag.optimize) + if mode == None: + return ("config '{}' has unknown optimize mode '{}'".format(tag.name, tag.optimize), None) + select_on.append("@rules_zig//zig/config/mode:" + tag.optimize) + zig_options.append("-Doptimize=" + mode) + + select_on.extend(tag.select_on) + + for flag in tag.zig_flags: + if "=" not in flag: + return ("config '{}' flag '{}' is not NAME=VALUE".format(tag.name, flag), None) + zig_options.append("-D" + flag) + + return (None, struct(name = tag.name, select_on = select_on, zig_options = zig_options)) + +def check_cells(package_name, cells): + """Validate and deduplicate a package's matrix cells. + + Args: + package_name: the package the cells belong to, for error messages. + cells: the package's cells, each a + `struct(name, select_on, zig_options, config_setting)`. + + Returns: + (error, cells), the fallback cells followed by the deduplicated + non-fallback cells. + """ + fallbacks = [cell for cell in cells if cell.config_setting == ""] + nonfallback = [cell for cell in cells if cell.config_setting != ""] + + deduped = [] + by_conditions = {} + for cell in nonfallback: + if not cell.select_on: + return ("package '{}' config '{}' has no select conditions".format(package_name, cell.name), None) + + key = tuple(sorted(cell.select_on)) + existing = by_conditions.get(key) + if existing != None: + if existing.zig_options != cell.zig_options: + return ("package '{}' configs '{}' and '{}' share conditions but differ in build options".format( + package_name, + existing.name, + cell.name, + ), None) + continue + by_conditions[key] = cell + deduped.append(cell) + + for outer in deduped: + for inner in deduped: + if outer.name == inner.name: + continue + if len(outer.select_on) < len(inner.select_on) and all([c in inner.select_on for c in outer.select_on]): + return ("package '{}' config '{}' conditions are a subset of '{}'; they would match ambiguously".format( + package_name, + outer.name, + inner.name, + ), None) + + return (None, fallbacks + deduped) + +def package_name_version(key): + """Split a URL package's hash key into its name and version. + + Args: + key: a URL package's Zig hash key, `--`. + `version` may contain `-` (e.g. `0.5.0-dev`). + + Returns: + (name, version). + """ + name, _, rest = key.partition("-") + return name, rest.rpartition("-")[0] + +def package_cells(key, cells_by_name, global_configure, per_package_configure): + """Resolve the matrix cells a URL package is configured under. + + Args: + key: the package's Zig hash key. + cells_by_name: map from config name to its `struct(name, select_on, zig_options)`. + global_configure: the global `configure` (with `configs`/`fallback`), or None. + per_package_configure: map from `(name, version)` to a `configure`. + + Returns: + (error, cells): cells is the ordered list of `struct(name, select_on, + zig_options, config_setting)`, or None if the package is configured + once at the host default. + """ + name, version = package_name_version(key) + + selected = per_package_configure.get((name, version)) + if selected == None: + selected = per_package_configure.get((name, "")) + if selected == None: + selected = global_configure + if selected == None: + return (None, None) + + if selected.fallback not in selected.configs: + return ("package '{}' configure fallback '{}' is not among its configs".format(name, selected.fallback), None) + + cells = [] + seen = {} + for config_name in selected.configs: + if config_name in seen: + return ("package '{}' configure lists config '{}' more than once".format(name, config_name), None) + seen[config_name] = True + cell = cells_by_name.get(config_name) + if cell == None: + return ("package '{}' configure references unknown config '{}'".format(name, config_name), None) + cells.append(struct( + name = cell.name, + select_on = cell.select_on, + zig_options = cell.zig_options, + config_setting = "" if config_name == selected.fallback else cell.name, + )) + + return check_cells(name, cells) + +def collect_configs(modules): + """Collect the build-configuration matrix from the root module's tags. + + Configurations from non-root modules are ignored (their names are returned + so the caller can warn). + + Args: + modules: sequence of bazel_module, the extension's modules with + `tags.config` and `tags.configure`. + + Returns: + (error, result), where result is `struct(cells_by_name, global_configure, + per_package_configure, ignored)`. + """ + ignored = [] + cells_by_name = {} + global_configure = None + per_package_configure = {} + + for mod in modules: + if not mod.is_root: + if mod.tags.config or mod.tags.configure: + ignored.append(mod.name) + continue + + for tag in mod.tags.config: + error, cell = resolve_cell(tag) + if error != None: + return (error, None) + existing = cells_by_name.get(tag.name) + if existing != None and existing != cell: + return ("conflicting config tags named '{}'".format(tag.name), None) + cells_by_name[tag.name] = cell + + for tag in mod.tags.configure: + if tag.package: + key = (tag.package, tag.version) + if key in per_package_configure: + return ("multiple configure tags for package '{}'".format(tag.package), None) + per_package_configure[key] = tag + elif global_configure != None: + return ("at most one global configure tag is allowed", None) + else: + global_configure = tag + + return (None, struct( + cells_by_name = cells_by_name, + global_configure = global_configure, + per_package_configure = per_package_configure, + ignored = ignored, + )) + +def _resolve_graph(module_ctx, zig, zon2json, cache, pkg_dir, manifests): + result = module_ctx.execute( + [zig, "run", "--cache-dir", cache, "--global-cache-dir", cache, zon2json, "--", zig, cache, str(pkg_dir)] + + [str(manifest) for manifest in manifests], + ) + if result.return_code != 0: + fail("Failed to resolve the Zig dependency graph:\n{}".format(result.stderr)) + return json.decode(result.stdout) + +def _portable_key(key, pkg_dir, manifest_labels): + """Map a path dependency's absolute directory to a portable key. + + Absolute paths are local to a single resolution and must not be persisted. + Path dependencies inside the package directory become package-relative keys; + those in module source become the label of their provided manifest. + """ + if key.startswith(pkg_dir + "/"): + return key[len(pkg_dir) + 1:] + + manifest = key + "/build.zig.zon" + if manifest in manifest_labels: + return manifest_labels[manifest] + + fail("Zig path dependency at '{}' has no provided manifest; add its `build.zig.zon` as a `from_file` tag.".format(key)) + +def _localize_paths(graph, pkg_dir, manifest_labels): + remap = { + key: _portable_key(key, pkg_dir, manifest_labels) + for key, package in graph["packages"].items() + if package["path"] != None + } + + packages = {} + for key, package in graph["packages"].items(): + package["deps"] = {name: remap.get(child, child) for name, child in package["deps"].items()} + if package["path"] != None: + package["path"] = remap[key] + packages[remap.get(key, key)] = package + graph["packages"] = packages + + for root in graph["roots"]: + root["deps"] = {name: remap.get(child, child) for name, child in root["deps"].items()} + + return graph + +def _edges(graph, key): + return [[name, child] for name, child in graph["packages"][key]["deps"].items()] + +def _deps_data(graph, key, reachable): + # Every transitively reachable package must be configurable: a URL dependency + # resolves to its sibling spoke (`path` is None); a sub-tree path dependency + # is configured in-tree (`path` is its location relative to this package). + # Sub-tree dependencies keep their own `deps`, including any reached through + # them, so their `build.zig` (e.g. `b.dependency`) and modules resolve. + packages = {} + for dep in reachable: + package = graph["packages"][dep] + if package["url"] != None: + path = None + elif dep.startswith(key + "/"): + path = dep[len(key) + 1:] + else: + fail("Zig package '{}' depends on out-of-tree path dependency '{}', which is unsupported.".format(key, dep)) + packages[dep] = {"deps": _edges(graph, dep), "path": path} + return { + "root_deps": _edges(graph, key), + "packages": packages, + } + +def _zig_packages_impl(module_ctx): + zig = zig_path(module_ctx) + zon2json = module_ctx.path(Label("//zig/private:zon2json.zig")) + cache = zig_cache(module_ctx) + pkg_dir = module_ctx.path("pkg") + + manifests = [] + manifest_labels = {} + root_tags = [] + root_dev = False + root_nondev = False + for mod in module_ctx.modules: + for tag in mod.tags.from_file: + manifest = module_ctx.path(tag.build_zig_zon) + + module_ctx.watch(manifest) + manifests.append(manifest) + manifest_labels[str(manifest)] = str(tag.build_zig_zon) + root_tags.append(tag.build_zig_zon) + if mod.is_root: + if module_ctx.is_dev_dependency(tag): + root_dev = True + else: + root_nondev = True + + system_libraries = {} + for mod in module_ctx.modules: + for tag in mod.tags.system_library: + existing = system_libraries.get(tag.name) + if existing != None and existing != tag.lib: + fail("Conflicting `system_library` annotations for '{}': {} and {}.".format(tag.name, existing, tag.lib)) + system_libraries[tag.name] = tag.lib + + system_integrations = {} + for mod in module_ctx.modules: + for tag in mod.tags.system_integration: + system_integrations[tag.name] = True + + system_integrations = system_integrations.keys() + + error, matrix = collect_configs(module_ctx.modules) + if error != None: + fail("Invalid Zig package configuration matrix: {}.".format(error)) + for name in matrix.ignored: + # buildifier: disable=print + print("ignoring config/configure tags from non-root module '{}'; set dev_dependency to True to avoid this warning".format(name)) + + graph = _resolve_graph(module_ctx, zig, zon2json, cache, pkg_dir, manifests) + graph = _localize_paths(graph, str(pkg_dir), manifest_labels) + + # `graph["packages"]` is topologically ordered, so each dependency's reachable + # set is already known by the time we reach a package: accumulate in one pass. + # Reachability follows every edge (URL and sub-tree path), so URL spokes reached + # through a sub-tree dependency are configured too. + reachable = {} + config_groups = {} + for key, package in graph["packages"].items(): + reached = {} + for _name, child in graph["packages"][key]["deps"].items(): + reached[child] = True + for dep in reachable[child]: + reached[dep] = True + reachable[key] = reached + if package["url"] == None: + continue + + error, cells = package_cells(key, matrix.cells_by_name, matrix.global_configure, matrix.per_package_configure) + if error != None: + fail("Invalid Zig package configuration matrix: {}.".format(error)) + + configs = {} + if cells != None: + config_settings = {} + for cell in cells: + if cell.config_setting != "": + config_settings[cell.name] = "@zig_deps//config:cfg_" + cell.name + config_groups[cell.name] = {"name": cell.name, "select_on": cell.select_on} + configs = { + "configs": json.encode([ + {"name": cell.name, "zig_options": cell.zig_options, "config_setting": cell.config_setting} + for cell in cells + ]), + "config_settings": config_settings, + } + + spokes = [dep for dep in reached if graph["packages"][dep]["url"] != None] + zig_package( + name = key, + url = package["url"], + zig_hash = key, + deps = json.encode(_deps_data(graph, key, reached)), + dep_build_files = {dep: "@{}//:build.zig".format(dep) for dep in spokes}, + system_libraries = system_libraries, + system_integrations = system_integrations, + **configs + ) + + manifests, targets = _hub_data(graph, root_tags) + zig_deps_hub( + name = "zig_deps", + manifests = json.encode(manifests), + packages = targets, + config_groups = json.encode([config_groups[name] for name in sorted(config_groups)]), + ) + + direct = ["zig_deps"] if root_nondev else [] + dev = ["zig_deps"] if root_dev and not root_nondev else [] + return module_ctx.extension_metadata( + root_module_direct_deps = direct, + root_module_direct_dev_deps = dev, + ) + +def _hub_data(graph, root_tags): + """Build the hub manifests and the dependency target map. + + A URL dependency named `name` resolves to the module of the same name in its + spoke. A local path dependency resolves, by convention, to a target of the + same name in the dependency manifest's own package, which the user provides. + Each dependency records whether it is a URL spoke, so the hub can expose its + other modules by name (`zig_dep(name, module = ...)`). + """ + tag_by_key = {str(tag): tag for tag in root_tags} + manifests = [] + targets = {} + for root, label in zip(graph["roots"], root_tags): + deps = {} + for name, key in root["deps"].items(): + url = graph["packages"][key]["url"] != None + if url: + target = "@{}//:{}".format(key, name) + targets[target] = target + else: + module = tag_by_key[key].same_package_label(name) + target = str(module) + targets[target] = module + deps[name] = {"target": target, "url": url} + manifests.append({ + "repo": label.repo_name, + "package": label.package, + "scope": str(label.same_package_label("__subpackages__")), + "deps": deps, + }) + return manifests, targets + +zig_packages = module_extension( + implementation = _zig_packages_impl, + tag_classes = { + "from_file": from_file, + "system_library": system_library, + "system_integration": system_integration, + "config": config, + "configure": configure, + }, +) diff --git a/zig/private/configurer.zig b/zig/private/configurer.zig new file mode 100644 index 00000000..e281001f --- /dev/null +++ b/zig/private/configurer.zig @@ -0,0 +1,376 @@ +//! Configure a Zig package's `build.zig` and emit its public module graph as +//! JSON, for translation into Bazel `zig_library` targets. +//! +//! Usage: configurer --zig --build-root [--system-integration NAME ...] [--zig-option -DNAME=VALUE ...] +//! +//! Modeled on `lib/compiler/configurer.zig`: it sets up a `std.Build`, runs the +//! package's `build` function, then walks the module import graph (seeded by the +//! modules registered via `b.addModule`) instead of serializing the build graph. +//! Following imports transitively reaches the modules of in-tree sub-tree path +//! dependencies, whose `build.zig` runs in a sub-builder. The package's +//! `build.zig` is provided as the `pkg` module and its dependency table as the +//! `deps` module, both wired in at compile time. +//! +//! The emitted JSON has the shape: +//! +//! {"modules": [{"name": ..., "package": , "root_source": ..., +//! "generated_source": ..., "link_libc": true, "link_libcpp": true, +//! "csrcs": [...], "include_dirs": [...], "system_libs": [...], "unsupported": [...], // each present only when set/non-empty +//! "imports": [{"name": ..., "module": ..., "package": }]}]} +//! +//! A module's `package` is the Zig hash (or sub-tree key) of the package that +//! owns it, or the empty string for the root package being configured. An +//! import's `package` identifies the owner of the imported module likewise. An +//! import's `name` is the name the importer uses (`@import(name)`), while its +//! `module` is the imported module's own registered name; the two differ when a +//! module is imported under an alias. +//! +//! A module whose root source is produced by `b.addOptions()` has no file in the +//! package tree; for these the `generated_source` field carries the step's +//! accumulated source so the importer can materialize it as a static file. +//! +//! The `link_libc`/`link_libcpp` fields are emitted (as `true`) only when the +//! module links the C / C++ standard library, e.g. via +//! `b.addModule(..., .{ .link_libc = true })` or `module.linkSystemLibrary("c", +//! .{})`; they are omitted otherwise. +//! +//! The `csrcs` field lists the module's vendored C sources, each with its +//! per-file `flags` and an optional `language`. The `include_dirs` field lists +//! the module's own include directories, each tagged by `kind` (`path`, +//! `path_system`, or `path_after`). The `system_libs` field lists the names of +//! non-libc system libraries the module links (`linkSystemLibrary`); the +//! importer requires each to be mapped to a `cc_library` via a +//! `zig_packages.system_library` annotation. Each `--system-integration NAME` +//! argument pre-enables the named optional system integration before the build +//! runs, so the package's `systemIntegrationOption(NAME)` returns true and its +//! guarded `linkSystemLibrary` calls run (surfacing as `system_libs` entries). +//! The `unsupported` field lists human-readable descriptions of C or link +//! constructs the importer cannot represent (assembly, prebuilt objects, +//! generated config headers, linked compile steps, ...) and causes failure if +//! present. + +const std = @import("std"); +const Io = std.Io; +const Build = std.Build; +const LazyPath = Build.LazyPath; +const Allocator = std.mem.Allocator; +const mem = std.mem; +const process = std.process; + +pub const root = @import("pkg"); +pub const dependencies = @import("deps"); + +pub fn main(init: process.Init) !void { + const arena = init.arena.allocator(); + const io = init.io; + + const args = try init.minimal.args.toSlice(arena); + + var zig_exe: ?[]const u8 = null; + var build_root_sub_path: ?[]const u8 = null; + var system_integrations: std.ArrayList([]const u8) = .empty; + var zig_options: std.ArrayList([]const u8) = .empty; + var i: usize = 1; + while (i < args.len) : (i += 1) { + if (mem.eql(u8, args[i], "--zig")) { + zig_exe = nextArg(args, &i); + } else if (mem.eql(u8, args[i], "--build-root")) { + build_root_sub_path = nextArg(args, &i); + } else if (mem.eql(u8, args[i], "--system-integration")) { + try system_integrations.append(arena, nextArg(args, &i)); + } else if (mem.eql(u8, args[i], "--zig-option")) { + try zig_options.append(arena, nextArg(args, &i)); + } else { + fatal("unrecognized argument: {s}", .{args[i]}); + } + } + + const zig = zig_exe orelse fatal("missing --zig", .{}); + const build_root_path = build_root_sub_path orelse fatal("missing --build-root", .{}); + + var graph: Build.Graph = .{ + .io = io, + .arena = arena, + .environ_map = try init.minimal.environ.createMap(arena), + .host = .{ + .query = .{}, + .result = try std.zig.system.resolveTargetQuery(io, .{}), + }, + .generated_files = .empty, + .zig_exe = zig, + .wip_configuration = .init(arena), + }; + // Seed the configuration string table so its reserved sentinels resolve: + // `.empty` must intern at offset 0 and `.root` at offset 1, before any other + // string. + const empty_string = try graph.wip_configuration.addString(""); + const root_string = try graph.wip_configuration.addString("root"); + std.debug.assert(empty_string == .empty); + std.debug.assert(root_string == .root); + + for (system_integrations.items) |name| { + try graph.system_integration_options.put(arena, name, .user_enabled); + } + + const build_root: Build.Cache.Path = .{ + .root_dir = .{ + .handle = try Io.Dir.cwd().openDir(io, build_root_path, .{}), + .path = build_root_path, + }, + }; + + const builder = try Build.create(&graph, build_root, dependencies.root_deps); + + for (zig_options.items) |option| { + const setting = if (mem.startsWith(u8, option, "-D")) option[2..] else fatal("--zig-option requires -DNAME=VALUE, got '{s}'", .{option}); + const eq = mem.indexOfScalar(u8, setting, '=') orelse + fatal("--zig-option requires -DNAME=VALUE, got '{s}'", .{option}); + if (try builder.addUserInputOption(setting[0..eq], setting[eq + 1 ..])) + fatal("invalid --zig-option '{s}'", .{option}); + } + + try builder.runBuild(root); + + if (builder.validateUserInputDidItFail()) + fatal("a --zig-option was not recognized by the package's build.zig", .{}); + + try emit(arena, io, builder); +} + +const ModuleSet = std.AutoArrayHashMapUnmanaged(*Build.Module, void); + +/// Collect `module` and every module it transitively imports, deduplicated by +/// identity. Imports cross into sub-builders, so this reaches the modules of +/// in-tree sub-tree path dependencies in addition to the root package's own. +fn collect(arena: Allocator, modules: *ModuleSet, module: *Build.Module) !void { + const gop = try modules.getOrPut(arena, module); + if (gop.found_existing) return; + for (module.import_table.values()) |imported| try collect(arena, modules, imported); +} + +/// The name a module is registered under in its owning package. Anonymous +/// modules (created via `b.createModule` rather than `b.addModule`) have no +/// registered name, so synthesize a stable one from the module's position in the +/// deduplicated set; its generated target and every import edge referencing it go +/// through this function and so agree. +fn moduleName(arena: Allocator, modules: *const ModuleSet, module: *Build.Module) ![]const u8 { + var it = module.owner.modules.iterator(); + while (it.next()) |entry| { + if (entry.value_ptr.* == module) return entry.key_ptr.*; + } + return std.fmt.allocPrint(arena, "__anon_{d}", .{modules.getIndex(module).?}); +} + +fn emit(arena: Allocator, io: Io, builder: *Build) !void { + var modules: ModuleSet = .empty; + for (builder.modules.values()) |module| try collect(arena, &modules, module); + + var stdout_buffer: [4096]u8 = undefined; + var stdout = std.Io.File.stdout().writerStreaming(io, &stdout_buffer); + const writer = &stdout.interface; + + var json: std.json.Stringify = .{ .writer = writer }; + try json.beginObject(); + try json.objectField("modules"); + try json.beginArray(); + for (modules.keys()) |module| { + try json.beginObject(); + try json.objectField("name"); + try json.write(try moduleName(arena, &modules, module)); + try json.objectField("package"); + try json.write(module.owner.pkg_hash); + try json.objectField("root_source"); + try json.write(lazyPathString(module.root_source_file)); + if (generatedOptionsSource(builder, module.root_source_file)) |source| { + try json.objectField("generated_source"); + try json.write(source); + } + if (module.link_libc == true) { + try json.objectField("link_libc"); + try json.write(true); + } + if (module.link_libcpp == true) { + try json.objectField("link_libcpp"); + try json.write(true); + } + try emitC(arena, &json, module); + try json.objectField("imports"); + try json.beginArray(); + for (module.import_table.keys(), module.import_table.values()) |import_name, imported| { + try json.beginObject(); + try json.objectField("name"); + try json.write(import_name); + try json.objectField("module"); + try json.write(try moduleName(arena, &modules, imported)); + try json.objectField("package"); + try json.write(imported.owner.pkg_hash); + try json.endObject(); + } + try json.endArray(); + try json.endObject(); + } + try json.endArray(); + try json.endObject(); + try writer.writeByte('\n'); + try writer.flush(); +} + +fn lazyPathString(lazy_path: ?LazyPath) ?[]const u8 { + const path = lazy_path orelse return null; + return switch (path) { + .src_path => |src| src.sub_path, + .cwd_relative => |rel| rel, + .relative => |rel| rel.sub_path, + else => null, + }; +} + +/// If `lazy_path` is the generated output of a `b.addOptions()` step, return its +/// accumulated source. The options source is a self-contained set of `pub const` +/// declarations fully determined once `build` has run, so it can be materialized +/// as a static file instead of being produced by a build step — which the +/// importer cannot run. Other generated paths return null (and the module is +/// dropped, as it has no static source). +fn generatedOptionsSource(builder: *Build, lazy_path: ?LazyPath) ?[]const u8 { + const generated = switch (lazy_path orelse return null) { + .generated => |generated| generated, + else => return null, + }; + if (generated.up != 0 or generated.sub_path.len != 0) return null; + const step = builder.graph.generated_files.items[@intFromEnum(generated.index)]; + if (step.tag != .options) return null; + const options: *Build.Step.Options = @fieldParentPtr("step", step); + return options.contents.items; +} + +const CSource = struct { + path: []const u8, + flags: []const []const u8, + language: ?[]const u8, +}; + +const IncludeDir = struct { + kind: []const u8, + path: []const u8, +}; + +fn joinPath(arena: Allocator, base: []const u8, sub: []const u8) ![]const u8 { + if (base.len == 0) return sub; + return std.fmt.allocPrint(arena, "{s}/{s}", .{ base, sub }); +} + +fn languageName(language: ?Build.Module.CSourceLanguage) ?[]const u8 { + return if (language) |l| @tagName(l) else null; +} + +fn appendIncludeDir( + arena: Allocator, + include_dirs: *std.ArrayList(IncludeDir), + unsupported: *std.ArrayList([]const u8), + kind: []const u8, + lazy_path: LazyPath, +) !void { + if (lazyPathString(lazy_path)) |path| { + try include_dirs.append(arena, .{ .kind = kind, .path = path }); + } else { + try unsupported.append(arena, "an include path with a generated or out-of-package location"); + } +} + +fn emitC(arena: Allocator, json: *std.json.Stringify, module: *Build.Module) !void { + var csrcs: std.ArrayList(CSource) = .empty; + var include_dirs: std.ArrayList(IncludeDir) = .empty; + var system_libs: std.ArrayList([]const u8) = .empty; + var unsupported: std.ArrayList([]const u8) = .empty; + + for (module.link_objects.items) |link_object| switch (link_object) { + .c_source_file => |c| { + if (lazyPathString(c.file)) |path| { + try csrcs.append(arena, .{ .path = path, .flags = c.flags, .language = languageName(c.language) }); + } else { + try unsupported.append(arena, "a C source file with a generated or out-of-package path"); + } + }, + .c_source_files => |c| { + if (lazyPathString(c.root)) |root_path| { + for (c.files) |file| { + try csrcs.append(arena, .{ .path = try joinPath(arena, root_path, file), .flags = c.flags, .language = languageName(c.language) }); + } + } else { + try unsupported.append(arena, "C source files with a generated or out-of-package root"); + } + }, + .system_lib => |lib| try system_libs.append(arena, lib.name), + .static_path => try unsupported.append(arena, "a precompiled object or static library (`addObjectFile`)"), + .assembly_file => try unsupported.append(arena, "an assembly source file"), + .win32_resource_file => try unsupported.append(arena, "a Win32 resource file"), + .other_step => try unsupported.append(arena, "a linked compile step (`linkLibrary`/`addObject`)"), + }; + + for (module.include_dirs.items) |include_dir| switch (include_dir) { + .path => |lp| try appendIncludeDir(arena, &include_dirs, &unsupported, "path", lp), + .path_system => |lp| try appendIncludeDir(arena, &include_dirs, &unsupported, "path_system", lp), + .path_after => |lp| try appendIncludeDir(arena, &include_dirs, &unsupported, "path_after", lp), + .embed_path => try unsupported.append(arena, "an embed include path (`addEmbedPath`)"), + .framework_path, .framework_path_system => try unsupported.append(arena, "a framework include path"), + .config_header_step => try unsupported.append(arena, "a generated config header (`addConfigHeader`)"), + .other_step => try unsupported.append(arena, "an include path from a linked compile step"), + }; + + if (csrcs.items.len > 0) { + try json.objectField("csrcs"); + try json.beginArray(); + for (csrcs.items) |c| { + try json.beginObject(); + try json.objectField("path"); + try json.write(c.path); + try json.objectField("flags"); + try json.beginArray(); + for (c.flags) |flag| try json.write(flag); + try json.endArray(); + try json.objectField("language"); + try json.write(c.language); + try json.endObject(); + } + try json.endArray(); + } + + if (include_dirs.items.len > 0) { + try json.objectField("include_dirs"); + try json.beginArray(); + for (include_dirs.items) |inc| { + try json.beginObject(); + try json.objectField("kind"); + try json.write(inc.kind); + try json.objectField("path"); + try json.write(inc.path); + try json.endObject(); + } + try json.endArray(); + } + + if (system_libs.items.len > 0) { + try json.objectField("system_libs"); + try json.beginArray(); + for (system_libs.items) |name| try json.write(name); + try json.endArray(); + } + + if (unsupported.items.len > 0) { + try json.objectField("unsupported"); + try json.beginArray(); + for (unsupported.items) |u| try json.write(u); + try json.endArray(); + } +} + +fn nextArg(args: []const [:0]const u8, i: *usize) []const u8 { + i.* += 1; + if (i.* >= args.len) fatal("'{s}' requires a value", .{args[i.* - 1]}); + return args[i.*]; +} + +fn fatal(comptime format: []const u8, args: anytype) noreturn { + std.debug.print("configurer: " ++ format ++ "\n", args); + std.process.exit(1); +} diff --git a/zig/private/package_prefix.zig b/zig/private/package_prefix.zig new file mode 100644 index 00000000..135b6f55 --- /dev/null +++ b/zig/private/package_prefix.zig @@ -0,0 +1,57 @@ +//! Print the directory prefix of the shallowest `build.zig.zon` in a Zig +//! package archive (a gzip-compressed tarball), for use as an extraction +//! `strip_prefix`. +//! +//! Usage: package_prefix + +const std = @import("std"); +const flate = std.compress.flate; +const tar = std.tar; + +pub fn main(init: std.process.Init) !void { + const allocator = init.arena.allocator(); + const io = init.io; + + const args = try init.minimal.args.toSlice(allocator); + if (args.len != 2) fatal("usage: package_prefix ", .{}); + + var file = try std.Io.Dir.cwd().openFile(io, args[1], .{}); + defer file.close(io); + + var read_buffer: [64 * 1024]u8 = undefined; + var file_reader = file.reader(io, &read_buffer); + + var window: [flate.max_window_len]u8 = undefined; + var decompress: flate.Decompress = .init(&file_reader.interface, .gzip, &window); + + var name_buffer: [std.fs.max_path_bytes]u8 = undefined; + var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined; + var iterator: tar.Iterator = .init(&decompress.reader, .{ + .file_name_buffer = &name_buffer, + .link_name_buffer = &link_name_buffer, + }); + + var prefix: ?[]const u8 = null; + while (try iterator.next()) |entry| { + if (entry.kind == .directory) continue; + if (!std.mem.eql(u8, std.fs.path.basename(entry.name), "build.zig.zon")) continue; + + const dir = std.fs.path.dirname(entry.name) orelse ""; + if (prefix == null or dir.len < prefix.?.len) { + prefix = try allocator.dupe(u8, dir); + } + } + + const result = prefix orelse fatal("no `build.zig.zon` found in '{s}'", .{args[1]}); + + var stdout_buffer: [std.fs.max_path_bytes]u8 = undefined; + var stdout = std.Io.File.stdout().writerStreaming(io, &stdout_buffer); + try stdout.interface.writeAll(result); + try stdout.interface.writeByte('\n'); + try stdout.interface.flush(); +} + +fn fatal(comptime format: []const u8, args: anytype) noreturn { + std.debug.print("package_prefix: " ++ format ++ "\n", args); + std.process.exit(1); +} diff --git a/zig/private/repo/BUILD.bazel b/zig/private/repo/BUILD.bazel index 59521193..bfe75ecf 100644 --- a/zig/private/repo/BUILD.bazel +++ b/zig/private/repo/BUILD.bazel @@ -20,12 +20,36 @@ bzl_library( ], ) +bzl_library( + name = "zig_host_toolchain", + srcs = ["zig_host_toolchain.bzl"], + visibility = ["//zig:__subpackages__"], + deps = ["@zig_toolchains//private:toolchains"], +) + +bzl_library( + name = "zig_package", + srcs = ["zig_package.bzl"], + visibility = ["//zig:__subpackages__"], + deps = ["@rules_zig_host_toolchain//:toolchain"], +) + +bzl_library( + name = "zig_deps_hub", + srcs = ["zig_deps_hub.bzl"], + visibility = ["//zig:__subpackages__"], + deps = ["@bazel_skylib//lib:paths"], +) + # Execute `bazel run //util:update_filegroups` to update this target. filegroup( name = "all_files", srcs = [ ":BUILD.bazel", ":toolchains_repo.bzl", + ":zig_deps_hub.bzl", + ":zig_host_toolchain.bzl", + ":zig_package.bzl", ":zig_repository.bzl", ], visibility = ["//zig/private:__pkg__"], diff --git a/zig/private/repo/toolchains_repo.bzl b/zig/private/repo/toolchains_repo.bzl index c404950e..fc0d340e 100644 --- a/zig/private/repo/toolchains_repo.bzl +++ b/zig/private/repo/toolchains_repo.bzl @@ -32,6 +32,7 @@ ATTRS = { "names": attr.string_list(doc = "The name suffixes to assign to the generated toolchain targets. Will be pre-fixed with a counter for ordering."), "labels": attr.string_list(doc = "The labels to the toolchain implementation targets."), "zig_versions": attr.string_list(doc = "The Zig SDK versions of the corresponding toolchain targets."), + "exec_platforms": attr.string_list(doc = "The execution platform of each toolchain target, e.g. `x86_64-linux`."), "exec_lengths": attr.int_list(doc = "The length of the slice of the `exec_constraints` attribute that corresponds to each toolchain target."), "exec_constraints": attr.string_list(doc = "All toolchain execution platform constraints concatenated to a single list."), "target_compatible_lengths": attr.int_list(doc = "The length of the slice of the `target_compatible_constraints` attribute that corresponds to each toolchain target."), @@ -67,7 +68,7 @@ def _toolchains_repo_impl(repository_ctx): len_expected = len(repository_ctx.attr.names) len_equal = all([ len_expected == len(getattr(repository_ctx.attr, attr)) - for attr in ["labels", "zig_versions", "exec_lengths", "target_compatible_lengths", "target_settings_lengths"] + for attr in ["labels", "zig_versions", "exec_platforms", "exec_lengths", "target_compatible_lengths", "target_settings_lengths"] ]) if not len_equal: fail("Lengths of the attributes `names`, `labels`, `zig_versions`, `exec_lengths`, `target_compatible_lengths`, `target_settings_lengths` must match.") @@ -105,6 +106,14 @@ def _toolchains_repo_impl(repository_ctx): load("@bazel_skylib//lib:selects.bzl", "selects") load("@bazel_skylib//rules:common_settings.bzl", "string_flag") +""" + + toolchains_content = """\ +# Generated by toolchains_repo.bzl +# +# This file is not part of the public API, please do not rely on its contents. + +ZIG_TOOLCHAINS = [ """ unique_zig_versions = { @@ -177,6 +186,7 @@ selects.config_setting_group( repository_ctx.attr.names, repository_ctx.attr.labels, repository_ctx.attr.zig_versions, + repository_ctx.attr.exec_platforms, repository_ctx.attr.exec_lengths, repository_ctx.attr.target_compatible_lengths, repository_ctx.attr.target_settings_lengths, @@ -184,7 +194,7 @@ selects.config_setting_group( exec_offset = 0 target_compatible_offset = 0 target_settings_offset = 0 - for counter, (name, label, zig_version, exec_len, target_compatible_len, target_settings_len) in enumerate(zipped): + for counter, (name, label, zig_version, exec_platform, exec_len, target_compatible_len, target_settings_len) in enumerate(zipped): compatible_with = repository_ctx.attr.exec_constraints[exec_offset:exec_offset + exec_len] exec_offset += exec_len target_compatible_with = repository_ctx.attr.target_compatible_constraints[target_compatible_offset:target_compatible_offset + target_compatible_len] @@ -211,9 +221,32 @@ toolchain( label = label, ) + zig_exe = "zig.exe" if "windows" in exec_platform else "zig" + toolchains_content += """\ + struct(version = "{version}", exec_platform = "{exec_platform}", zig = Label("{zig}")), +""".format( + version = zig_version, + exec_platform = exec_platform, + zig = label.replace("//:zig_toolchain", "//:" + zig_exe), + ) + # Base BUILD file for this repository repository_ctx.file("BUILD.bazel", build_content) + repository_ctx.file("private/BUILD.bazel", """\ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +bzl_library( + name = "toolchains", + srcs = ["toolchains.bzl"], + visibility = ["//visibility:public"], +) +""") + toolchains_content += """\ +] +""" + repository_ctx.file("private/toolchains.bzl", toolchains_content) + toolchains_repo = repository_rule( _toolchains_repo_impl, doc = DOC, diff --git a/zig/private/repo/zig_deps_hub.bzl b/zig/private/repo/zig_deps_hub.bzl new file mode 100644 index 00000000..df1995d0 --- /dev/null +++ b/zig/private/repo/zig_deps_hub.bzl @@ -0,0 +1,162 @@ +"""Implementation of the `zig_deps` hub repository rule.""" + +load("@bazel_skylib//lib:paths.bzl", "paths") + +DOC = """\ +The `@zig_deps` hub repository. + +Exposes each manifest's direct Zig dependencies by name, grouped under the +manifest's own Bazel package and scoped to that package's subtree. Generated by +the `zig_packages` module extension. +""" + +ATTRS = { + "manifests": attr.string( + mandatory = True, + doc = "JSON-encoded list of `{repo, package, scope, deps}` manifest entries.", + ), + "packages": attr.string_keyed_label_dict( + mandatory = True, + doc = "Map from each package key to its `files` target.", + ), + "config_groups": attr.string( + default = "[]", + doc = "JSON list of `{name, select_on}`, the non-fallback configuration cells across all packages.", + ), +} + +_ALIAS = """\ +alias( + name = "{name}", + actual = "{actual}", + visibility = ["{scope}"], +) +""" + +_CONFIG_GROUP = """\ +selects.config_setting_group( + name = "cfg_{name}", + match_all = {match_all}, + visibility = ["//visibility:public"], +) +""" + +def render_config_groups(config_groups): + """Render the matrix's config_setting_groups as the hub's `config/BUILD.bazel`. + + Each cell becomes a `cfg_` group matching all of its conditions, which + the spokes `select()` on. + + Args: + config_groups: list of `{name, select_on}` (the non-fallback cells). + + Returns: + the `config/BUILD.bazel` text, or "" when there are no groups. + """ + if not config_groups: + return "" + + chunks = ["load(\"@bazel_skylib//lib:selects.bzl\", \"selects\")"] + for group in config_groups: + chunks.append(_CONFIG_GROUP.format(name = group["name"], match_all = json.encode(group["select_on"]))) + return "\n".join(chunks) + +# The generated accessors reference each spoke by its canonical (`@@`) repo name, +# which is resolved at module-extension time and has no apparent-name alias here. +# buildifier: disable=canonical-repository +_DEFS = '''\ +"""Accessors for the Zig package dependencies of each `from_file` manifest.""" + +load("@bazel_skylib//lib:paths.bzl", "paths") + +_MANIFESTS = json.decode("""%MANIFESTS%""") + +def zig_dep(name, module = None): + """Return the label of dependency `name` of the enclosing Zig manifest. + + Resolves to the dependency's module of the same name. Pass `module` to select + a differently named module exposed by the same (URL) dependency. + """ + path, deps = _manifest() + if name not in deps: + fail("Zig manifest '%s' declares no dependency '%s'; available: %s" % (path, name, sorted(deps))) + if module == None or module == name: + return _label(path, name) + dep = deps[name] + if not dep["select"]: + fail("Zig dependency '%s' is a path dependency; cannot select module '%s'" % (name, module)) + return Label("@@" + dep["spoke"] + "//:" + module) + +def zig_deps(): + """Return the labels of every dependency of the enclosing Zig manifest.""" + path, deps = _manifest() + return [_label(path, name) for name in deps] + +def _manifest(): + repo = native.repo_name() + package = native.package_name() + manifests = _MANIFESTS.get(repo, {}) + manifest = _enclosing(manifests, package) + if manifest == None: + fail("no Zig `from_file` manifest covers package '%s'" % package) + parts = [part for part in [repo, manifest] if part] + path = paths.join(*parts) if parts else "" + return path, manifests[manifest] + +def _label(path, name): + return Label("//" + path + ":" + name) + +def _enclosing(manifests, package): + candidate = package + for _ in range(len(package) + 1): + if candidate in manifests: + return candidate + if not candidate: + break + candidate = candidate.rpartition("/")[0] + return None +''' + +def _hub_path(manifest): + # `paths.join` keeps empty trailing segments (e.g. `repo/`), so drop empties + # first: the main module has no repo prefix and root manifests no package. + parts = [part for part in [manifest["repo"], manifest["package"]] if part] + return paths.join(*parts) if parts else "" + +def _zig_deps_hub_impl(repository_ctx): + packages = repository_ctx.attr.packages + manifests = json.decode(repository_ctx.attr.manifests) + + builds = {} + registry = {} + for manifest in manifests: + aliases = [] + deps = {} + for name, dep in manifest["deps"].items(): + label = packages[dep["target"]] + aliases.append(_ALIAS.format(name = name, actual = str(label), scope = manifest["scope"])) + + # URL dependencies expose further modules by name, addressed through + # their spoke's canonical repository; path dependencies do not. + deps[name] = {"spoke": label.repo_name if dep["url"] else "", "select": dep["url"]} + builds[_hub_path(manifest)] = "\n".join(aliases) + registry.setdefault(manifest["repo"], {})[manifest["package"]] = deps + + if "" not in builds: + builds[""] = "" + + for path, content in builds.items(): + build_file = path + "/BUILD.bazel" if path else "BUILD.bazel" + repository_ctx.file(build_file, content) + + repository_ctx.file("defs.bzl", _DEFS.replace("%MANIFESTS%", json.encode(registry))) + + config_build = render_config_groups(json.decode(repository_ctx.attr.config_groups)) + if config_build: + repository_ctx.file("config/BUILD.bazel", config_build) + +zig_deps_hub = repository_rule( + _zig_deps_hub_impl, + attrs = ATTRS, + doc = DOC, +) diff --git a/zig/private/repo/zig_host_toolchain.bzl b/zig/private/repo/zig_host_toolchain.bzl new file mode 100644 index 00000000..82969315 --- /dev/null +++ b/zig/private/repo/zig_host_toolchain.bzl @@ -0,0 +1,98 @@ +"""Implementation of the `zig_host_toolchain` repository rule.""" + +load("@zig_toolchains//private:toolchains.bzl", "ZIG_TOOLCHAINS") + +_VERSION_ENV = "RULES_ZIG_HOST_SDK" + +DOC = """\ +Expose a registered Zig SDK for the host platform. + +By default selects the first registered Zig SDK version, i.e. the default +toolchain version, or the latest if no default is set. Set the +`{env}` environment variable to a registered version to override. + +Generates a `toolchain.bzl` file with a `zig_path(ctx)` function that resolves +the host platform's Zig binary from a repository_ctx or module_ctx. +""".format(env = _VERSION_ENV) + +def _detect_host_platform(repository_ctx): + os_name = repository_ctx.os.name + arch = repository_ctx.os.arch + + if "linux" in os_name: + os = "linux" + elif "mac" in os_name or "darwin" in os_name: + os = "macos" + elif "windows" in os_name: + os = "windows" + else: + fail("Unsupported host operating system: {}".format(os_name)) + + if arch in ["amd64", "x86_64", "x64"]: + cpu = "x86_64" + elif arch in ["aarch64", "arm64"]: + cpu = "aarch64" + else: + fail("Unsupported host CPU architecture: {}".format(arch)) + + return "{}-{}".format(cpu, os) + +def _select_zig(repository_ctx, platform): + candidates = [ + toolchain + for toolchain in ZIG_TOOLCHAINS + if toolchain.exec_platform == platform + ] + if not candidates: + fail("No registered Zig SDK supports the host platform '{}'.".format(platform)) + + requested = repository_ctx.os.environ.get(_VERSION_ENV) + if not requested: + return candidates[0].zig + + for toolchain in candidates: + if toolchain.version == requested: + return toolchain.zig + + fail("{} requested Zig SDK version '{}', which is not registered for host platform '{}'. Registered versions: {}.".format( + _VERSION_ENV, + requested, + platform, + [toolchain.version for toolchain in candidates], + )) + +def _zig_host_toolchain_impl(repository_ctx): + platform = _detect_host_platform(repository_ctx) + zig = _select_zig(repository_ctx, platform) + + repository_ctx.file("BUILD.bazel", """\ +# Generated by zig_host_toolchain.bzl + +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +bzl_library( + name = "toolchain", + srcs = ["toolchain.bzl"], + visibility = ["//visibility:public"], +) +""") + repository_ctx.file("toolchain.bzl", """\ +# Generated by zig_host_toolchain.bzl + +load("@rules_zig//zig/private/common:zig_cache.bzl", "env_zig_cache_prefix") + +_ZIG = Label({zig}) +_PLATFORM = {platform} + +def zig_path(ctx): + return ctx.path(_ZIG) + +def zig_cache(ctx): + return env_zig_cache_prefix(ctx.os.environ, _PLATFORM) +""".format(zig = repr(str(zig)), platform = repr(platform))) + +zig_host_toolchain = repository_rule( + _zig_host_toolchain_impl, + doc = DOC, + environ = [_VERSION_ENV], +) diff --git a/zig/private/repo/zig_package.bzl b/zig/private/repo/zig_package.bzl new file mode 100644 index 00000000..2f442797 --- /dev/null +++ b/zig/private/repo/zig_package.bzl @@ -0,0 +1,612 @@ +"""Implementation of the `zig_package` repository rule.""" + +load("@rules_zig_host_toolchain//:toolchain.bzl", "zig_cache", "zig_path") + +DOC = """\ +Fetch a Zig package with the Zig SDK. + +The Zig SDK downloads, verifies, and prunes the package according to its +`build.zig.zon`, and supports `git+` URLs. Fetching fails if the resulting +package hash does not match the expected `zig_hash`. The package's `build.zig` +is then configured to extract its public module graph (`module_manifest.json`). +""" + +ATTRS = { + "url": attr.string(mandatory = True, doc = "The package URL, e.g. `https://...` or `git+https://...`."), + "zig_hash": attr.string(mandatory = True, doc = "The expected Zig package hash."), + "deps": attr.string( + default = "{\"root_deps\": [], \"packages\": {}}", + doc = "JSON `{root_deps, packages}` describing the `@dependencies` closure used to configure the package.", + ), + "dep_build_files": attr.string_keyed_label_dict( + doc = "Map from each dependency package hash to its `build.zig`, used to wire `@dependencies`.", + ), + "system_libraries": attr.string_keyed_label_dict( + doc = "Map from a system-library name (as passed to `linkSystemLibrary`) to a `cc_library` providing it.", + ), + "system_integrations": attr.string_list( + doc = "Names of optional system integrations (`systemIntegrationOption`) to enable when configuring the package.", + ), + "configs": attr.string( + default = "", + doc = "JSON configuration-matrix cells; empty configures once at the host default.", + ), + "config_settings": attr.string_keyed_label_dict( + doc = "Map from a non-fallback cell name to its config_setting_group label.", + ), +} + +def _package_prefix(repository_ctx, zig, helper, cache, archive): + # The archive nests the package under `//`; strip up to + # the directory that holds `build.zig.zon`. + result = repository_ctx.execute( + [zig, "run", "--cache-dir", cache, "--global-cache-dir", cache, helper, "--", str(archive)], + ) + if result.return_code != 0: + fail("Failed to inspect the Zig package archive '{}':\n{}".format(archive, result.stderr)) + return result.stdout.strip() + +_EMPTY_DEPS = """\ +pub const packages = struct {}; +pub const root_deps: []const struct { []const u8, []const u8 } = &.{}; +""" + +def _zig_string(value): + return "\"" + value.replace("\\", "\\\\").replace("\"", "\\\"") + "\"" + +def _edge_lines(edges, indent): + return [ + "{}.{{ {}, {} }},".format(indent, _zig_string(name), _zig_string(key)) + for name, key in edges + ] + +def _build_root(repository_ctx, key, package): + # A sub-tree path dependency lives inside this package's own tree; a URL + # dependency lives in its own spoke. + if package["path"] != None: + return str(repository_ctx.path(package["path"])) + return str(repository_ctx.path(repository_ctx.attr.dep_build_files[key]).dirname) + +def _build_zig(repository_ctx, key, package): + if package["path"] != None: + return str(repository_ctx.path(package["path"] + "/build.zig")) + return str(repository_ctx.path(repository_ctx.attr.dep_build_files[key])) + +def _dependencies_source(repository_ctx, deps): + """Render the `@dependencies` module that `b.dependency` consumes.""" + packages = deps["packages"] + if not packages: + return _EMPTY_DEPS + + lines = ["pub const packages = struct {"] + for key in sorted(packages): + package = packages[key] + lines.append(" pub const @\"{}\" = struct {{".format(key)) + lines.append(" pub const build_root = {};".format(_zig_string(_build_root(repository_ctx, key, package)))) + lines.append(" pub const build_zig = @import(\"{}\");".format(key)) + lines.append(" pub const deps: []const struct { []const u8, []const u8 } = &.{") + lines.extend(_edge_lines(package["deps"], " ")) + lines.append(" };") + lines.append(" };") + lines.append("};") + lines.append("") + lines.append("pub const root_deps: []const struct { []const u8, []const u8 } = &.{") + lines.extend(_edge_lines(deps["root_deps"], " ")) + lines.append("};") + return "\n".join(lines) + "\n" + +_FILES = """\ +filegroup( + name = "files", + srcs = glob(["**"], exclude = ["BUILD.bazel", "module_manifest.json"]), + visibility = ["//visibility:public"], +) +""" + +_ZIG_LIBRARY = """\ +zig_library( + name = "{name}", + main = "{main}", + import_name = "{name}", + srcs = glob(["**/*.zig"], exclude = ["{main}"]), + deps = {deps}, + import_names = {import_names}, + visibility = ["//visibility:public"], +) +""" + +_ZIG_LIBRARY_SUBTREE = """\ +zig_library( + name = "{name}", + main = "{main}", + import_name = "{import_name}", + srcs = glob(["{subpath}/**/*.zig"], exclude = ["{main}"]), + deps = {deps}, + import_names = {import_names}, + visibility = ["//visibility:public"], +) +""" + +_HEADER_EXTENSIONS = ["h", "hh", "hpp", "hxx"] + +# vendored C is compiled via sibling `cc_library` targets. +_CC_LIBRARY = """\ +cc_library( + name = "{name}", + srcs = {srcs}, + hdrs = glob({hdrs}, allow_empty = True), + copts = {copts}, + includes = {includes}, + visibility = ["//visibility:public"], +) +""" + +_CC_LIBRARY_GROUP = """\ +cc_library( + name = "{name}", + deps = {deps}, + visibility = ["//visibility:public"], +) +""" + +def parse_cells(configs): + """Parse the `configs` attr into the package's configuration matrix cells. + + Args: + configs: the JSON `configs` attr, or "" for the host default. + + Returns: + (error, cells), each cell `struct(name, zig_options, config_setting)`. + """ + if not configs: + return (None, [struct(name = "", zig_options = [], config_setting = "")]) + + cells = [ + struct(name = cell["name"], zig_options = cell["zig_options"], config_setting = cell["config_setting"]) + for cell in json.decode(configs) + ] + fallbacks = [cell for cell in cells if cell.config_setting == ""] + if len(fallbacks) != 1: + return ("expected exactly one fallback cell, found {}".format(len(fallbacks)), None) + + rest = [cell for cell in cells if cell.config_setting != ""] + return (None, fallbacks + rest) + +def merge(package, per_cell_data, cells): + """Merge each cell's target records into one set, pushing `select()` to attrs. + + The set of target names must agree across cells, as must each target's `kind` + and its `fixed` attrs. Each `varying` attr resolves to `("const", value)` when + equal across cells, else `("select", {cell: value})`. + + Args: + package: the package URL, for error messages. + per_cell_data: map from cell name to that cell's list of target records. + cells: the ordered cells, the fallback first. + + Returns: + (error, merged), each merged record a `struct(kind, name, fixed, varying)`, + fallback first. + """ + fallback = cells[0].name + + indexed = { + cell.name: {record.name: record for record in per_cell_data[cell.name]} + for cell in cells + } + + base_names = sorted(indexed[fallback]) + for cell in cells[1:]: + if sorted(indexed[cell.name]) != base_names: + return ("package '{}' produces a different set of targets under config '{}' than under '{}'".format( + package, + cell.name, + fallback, + ), None) + + merged = [] + for name in indexed[fallback]: + base = indexed[fallback][name] + + for cell in cells[1:]: + other = indexed[cell.name][name] + if other.kind != base.kind: + return ("package '{}' target '{}' has kind '{}' under config '{}' but '{}' under '{}'".format( + package, + name, + other.kind, + cell.name, + base.kind, + fallback, + ), None) + for attr in base.fixed: + if base.fixed[attr] != other.fixed[attr]: + return ("package '{}' target '{}' fixed attribute '{}' varies by configuration".format( + package, + base.name, + attr, + ), None) + + resolved = {} + for attr in base.varying: + value = base.varying[attr] + uniform = all([indexed[cell.name][name].varying[attr] == value for cell in cells[1:]]) + if uniform: + resolved[attr] = ("const", value) + else: + resolved[attr] = ("select", {cell.name: indexed[cell.name][name].varying[attr] for cell in cells}) + + merged.append(struct(kind = base.kind, name = base.name, fixed = base.fixed, varying = resolved)) + + return (None, merged) + +def render_attr(config_settings, resolved, cells): + """Render a merged attribute as a literal or a `select()`. + + Args: + config_settings: map from a non-fallback cell name to its config_setting label. + resolved: `("const", value)` or `("select", {cell: value})`. + cells: configuration matrix cells. + + Returns: + the attribute's Starlark code. + """ + if resolved[0] == "const": + return json.encode(resolved[1]) + + by_cell = resolved[1] + lines = ["select({"] + for cell in cells: + if cell.config_setting != "": + lines.append(" {}: {},".format(json.encode(config_settings[cell.name]), json.encode(by_cell[cell.name]))) + for cell in cells: + if cell.config_setting == "": + lines.append(" {}: {},".format(json.encode("//conditions:default"), json.encode(by_cell[cell.name]))) + lines.append("})") + return "\n".join(lines) + +def render(config_settings, merged, cells): + """Render merged target records into the spoke's `BUILD.bazel` text. + + Args: + config_settings: map from a non-fallback cell name to its config_setting label. + merged: the merged target records from `merge`. + cells: configuration matrix cells. + + Returns: + the `BUILD.bazel` contents. + """ + cc_chunks = [] + library_chunks = [] + has_cc = False + for record in merged: + fixed = record.fixed + varying = record.varying + if record.kind == "zig_library": + library_chunks.append(_ZIG_LIBRARY.format( + name = record.name, + main = fixed["main"], + deps = render_attr(config_settings, varying["deps"], cells), + import_names = render_attr(config_settings, varying["import_names"], cells), + )) + elif record.kind == "zig_library_subtree": + library_chunks.append(_ZIG_LIBRARY_SUBTREE.format( + name = record.name, + import_name = fixed["import_name"], + main = fixed["main"], + subpath = fixed["subpath"], + deps = render_attr(config_settings, varying["deps"], cells), + import_names = render_attr(config_settings, varying["import_names"], cells), + )) + elif record.kind == "cc_library": + has_cc = True + cc_chunks.append(_CC_LIBRARY.format( + name = record.name, + srcs = render_attr(config_settings, varying["srcs"], cells), + hdrs = json.encode(fixed["header_globs"]), + copts = render_attr(config_settings, varying["copts"], cells), + includes = render_attr(config_settings, varying["includes"], cells), + )) + elif record.kind == "cc_library_group": + has_cc = True + cc_chunks.append(_CC_LIBRARY_GROUP.format( + name = record.name, + deps = render_attr(config_settings, varying["deps"], cells), + )) + + loads = ["load(\"@rules_zig//zig:defs.bzl\", \"zig_library\")"] + if has_cc: + loads.append("load(\"@rules_cc//cc:defs.bzl\", \"cc_library\")") + return "\n".join(loads + ["", _FILES] + cc_chunks + library_chunks) + +def _is_subtree(packages, owner): + return bool(owner) and owner in packages and packages[owner]["path"] != None + +def _target_name(packages, owner, module): + # Root-package modules keep their bare name (the spoke's public API). In-tree + # sub-tree modules are namespaced by their sub-path so that identically named + # modules in different sub-trees do not collide. Targets are keyed by the + # module's own name, independent of the (possibly aliased) name it is imported + # under. + if not owner: + return module + return packages[owner]["path"] + "/" + module + +def _csrc_path(packages, owner, path): + if not owner: + return path + return packages[owner]["path"] + "/" + path + +def _module_dep(repository_ctx, imported, packages): + key = imported["package"] + if _is_subtree(packages, key): + # An in-tree (sub-tree) module is generated as a sibling in this spoke. + return ":" + _target_name(packages, key, imported["module"]) + if key: + # A cross-package import resolves to the module in the dependency's spoke. + spoke = repository_ctx.attr.dep_build_files[key] + return str(spoke.same_package_label(imported["module"])) + return ":" + imported["module"] + +def _target_data(repository_ctx, modules, packages): + """Compute one configuration cell's generated-target records.""" + cc_records = [] + library_records = [] + for module in modules: + if not module["root_source"]: + continue + + # The configurer reports every reachable module tagged with its owner: the + # root package being configured (empty key) becomes a top-level library; an + # in-tree sub-tree path dependency becomes a library scoped to its sub-path; + # a module owned by a URL dependency lives in its own spoke and is skipped. + owner = module["package"] + if owner and not _is_subtree(packages, owner): + continue + + unsupported = module.get("unsupported") + if unsupported: + fail(("The Zig package '{}' module '{}' uses unsupported constructs: {}.").format( + repository_ctx.attr.url, + module["name"], + "; ".join(unsupported), + )) + + # A dependency is imported under its own name by default; an import that + # uses a different name is remapped per-edge via `import_names`, so the + # same module can be imported under different names by different modules. + deps = [] + import_names = {} + for imported in module["imports"]: + target = _module_dep(repository_ctx, imported, packages) + deps.append(target) + if imported["name"] != imported["module"]: + import_names[target] = imported["name"] + + if module.get("link_libc"): + deps.append("@rules_zig//zig/lib:libc") + if module.get("link_libcpp"): + deps.append("@rules_zig//zig/lib:libc++") + + for name in module.get("system_libs", []): + lib = repository_ctx.attr.system_libraries.get(name) + if lib == None: + fail(("The Zig package '{}' module '{}' requires the system library '{}', " + + "which is not provided. Map it to a cc_library with a " + + "`zig_packages.system_library(name = \"{}\", lib = ...)` annotation.").format( + repository_ctx.attr.url, + module["name"], + name, + name, + )) + deps.append(str(lib)) + + c_sources = [] + for csrc in module.get("csrcs", []): + if csrc["language"] not in (None, "c", "cpp"): + fail(("The Zig package '{}' module '{}' declares the unsupported language '{}' for a C source.").format( + repository_ctx.attr.url, + module["name"], + csrc["language"], + )) + c_sources.append((_csrc_path(packages, owner, csrc["path"]), csrc["flags"])) + + if c_sources: + cinc = _target_name(packages, owner, module["name"]) + ".cinc" + prefix = (packages[owner]["path"] + "/") if owner else "" + + header_dirs = {prefix + inc["path"]: None for inc in module.get("include_dirs", [])} + for path, _flags in c_sources: + header_dirs[path.rpartition("/")[0]] = None + header_globs = [ + (dir + "/" if dir else "") + "**/*." + ext + for dir in sorted(header_dirs) + for ext in _HEADER_EXTENSIONS + ] + includes = [prefix + inc["path"] for inc in module.get("include_dirs", [])] + + # Partition C sources by common `copts`. + groups = [] + for path, flags in c_sources: + if groups and groups[-1][0] == flags: + groups[-1][1].append(path) + else: + groups.append((flags, [path])) + + group_labels = [] + for index in range(len(groups)): + flags = groups[index][0] + paths = groups[index][1] + group_name = "{}.{}".format(cinc, index) + group_labels.append(":" + group_name) + cc_records.append(struct( + kind = "cc_library", + name = group_name, + fixed = {"header_globs": header_globs}, + varying = {"srcs": paths, "copts": flags, "includes": includes}, + )) + cc_records.append(struct( + kind = "cc_library_group", + name = cinc, + fixed = {}, + varying = {"deps": group_labels}, + )) + deps.append(":" + cinc) + + if not owner: + library_records.append(struct( + kind = "zig_library", + name = module["name"], + fixed = {"main": module["root_source"], "import_name": module["name"]}, + varying = {"deps": deps, "import_names": import_names}, + )) + else: + subpath = packages[owner]["path"] + library_records.append(struct( + kind = "zig_library_subtree", + name = _target_name(packages, owner, module["name"]), + fixed = {"main": subpath + "/" + module["root_source"], "import_name": module["name"], "subpath": subpath}, + varying = {"deps": deps, "import_names": import_names}, + )) + + return cc_records + library_records + +def _compile_configurer(repository_ctx, zig, build_zig, cache): + """Compile the configurer once, returning its executable path.""" + configurer = repository_ctx.path(Label("//zig/private:configurer.zig")) + deps = json.decode(repository_ctx.attr.deps) + + for key in sorted(deps["packages"]): + package = deps["packages"][key] + if package["path"] != None and not repository_ctx.path(package["path"] + "/build.zig").exists: + fail(("The Zig package '{}' has a source-only dependency at '{}' (a " + + "`build.zig.zon` with no `build.zig`); source-only dependencies " + + "are not supported.").format(repository_ctx.attr.url, package["path"])) + + repository_ctx.file("_configure/deps.zig", _dependencies_source(repository_ctx, deps)) + + keys = sorted(deps["packages"]) + + args = [zig, "build-exe", "--dep", "pkg", "--dep", "deps", "-Mroot=" + str(configurer), "-Mpkg=" + str(build_zig)] + for key in keys: + args.extend(["--dep", key]) + args.append("-Mdeps=" + str(repository_ctx.path("_configure/deps.zig"))) + for key in keys: + args.append("-M{}={}".format(key, _build_zig(repository_ctx, key, deps["packages"][key]))) + args.extend([ + "--cache-dir", + cache, + "--global-cache-dir", + cache, + "-femit-bin=" + str(repository_ctx.path("_configure/configurer")), + ]) + + compiled = repository_ctx.execute(args) + if compiled.return_code != 0: + fail("Failed to compile the Zig configurer for '{}':\n{}".format(repository_ctx.attr.url, compiled.stderr)) + + return repository_ctx.path("_configure/configurer") + +def _configure(repository_ctx, zig, configurer_bin, cell): + """Run the compiled configurer for one cell, returning its module-graph JSON.""" + args = [str(configurer_bin), "--zig", str(zig), "--build-root", str(repository_ctx.path("."))] + for name in repository_ctx.attr.system_integrations: + args.extend(["--system-integration", name]) + for option in cell.zig_options: + args.extend(["--zig-option", option]) + + configured = repository_ctx.execute(args) + if configured.return_code != 0: + scope = " under configuration '{}'".format(cell.name) if cell.name else "" + fail("Failed to configure the Zig package '{}'{}:\n{}".format(repository_ctx.attr.url, scope, configured.stderr)) + return configured.stdout + +def _materialize_generated(repository_ctx, modules, packages): + for module in modules: + generated = module.get("generated_source") + if generated == None: + continue + rel = "_zig_generated/" + module["name"] + ".zig" + if _is_subtree(packages, module["package"]): + repository_ctx.file(packages[module["package"]]["path"] + "/" + rel, generated) + else: + repository_ctx.file(rel, generated) + module["root_source"] = rel + +def _zig_package_impl(repository_ctx): + zig = zig_path(repository_ctx) + helper = repository_ctx.path(Label("//zig/private:package_prefix.zig")) + cache = zig_cache(repository_ctx) + + fetch = repository_ctx.execute([zig, "fetch", "--global-cache-dir", cache, repository_ctx.attr.url]) + if fetch.return_code != 0: + fail("`zig fetch {}` failed:\n{}".format(repository_ctx.attr.url, fetch.stderr)) + + fetched_hash = fetch.stdout.strip() + if fetched_hash != repository_ctx.attr.zig_hash: + fail("Zig package hash mismatch for '{}':\n expected: {}\n fetched: {}".format( + repository_ctx.attr.url, + repository_ctx.attr.zig_hash, + fetched_hash, + )) + + archive = repository_ctx.path(cache).get_child("p").get_child(fetched_hash + ".tar.gz") + repository_ctx.extract(archive, strip_prefix = _package_prefix(repository_ctx, zig, helper, cache, archive)) + + error, cells = parse_cells(repository_ctx.attr.configs) + if error != None: + fail("The Zig package '{}' has an invalid configuration matrix: {}.".format(repository_ctx.attr.url, error)) + fallback = cells[0].name + + packages = json.decode(repository_ctx.attr.deps)["packages"] + config_settings = {name: str(label) for name, label in repository_ctx.attr.config_settings.items()} + + build_zig = repository_ctx.path("build.zig") + if not build_zig.exists: + repository_ctx.file("BUILD.bazel", render(config_settings, [], cells)) + return + + # Configure once per cell with that cell's `-D` build options. + configurer_bin = _compile_configurer(repository_ctx, zig, build_zig, cache) + per_cell_modules = {} + for cell in cells: + manifest = _configure(repository_ctx, zig, configurer_bin, cell) + if cell.name == fallback: + repository_ctx.file("module_manifest.json", manifest) + per_cell_modules[cell.name] = json.decode(manifest)["modules"] + repository_ctx.delete("_configure") + + # Generated source is materialized to one file shared by every cell, so it + # must not vary across them; reflect the fallback's into the others. + fallback_generated = { + module["name"]: module["generated_source"] + for module in per_cell_modules[fallback] + if module.get("generated_source") != None + } + for cell in cells[1:]: + for module in per_cell_modules[cell.name]: + if module.get("generated_source") != fallback_generated.get(module["name"]): + fail(("The Zig package '{}' module '{}' produces different generated source under " + + "configuration '{}' than under '{}'; config-varying generated source is not " + + "supported.").format(repository_ctx.attr.url, module["name"], cell.name, fallback)) + _materialize_generated(repository_ctx, per_cell_modules[fallback], packages) + fallback_root_source = {module["name"]: module["root_source"] for module in per_cell_modules[fallback]} + for cell in cells[1:]: + for module in per_cell_modules[cell.name]: + if module["name"] in fallback_root_source: + module["root_source"] = fallback_root_source[module["name"]] + + per_cell_data = { + cell.name: _target_data(repository_ctx, per_cell_modules[cell.name], packages) + for cell in cells + } + error, merged = merge(repository_ctx.attr.url, per_cell_data, cells) + if error != None: + fail("The Zig package '{}' cannot be configured across the matrix: {}.".format(repository_ctx.attr.url, error)) + repository_ctx.file("BUILD.bazel", render(config_settings, merged, cells)) + +zig_package = repository_rule( + _zig_package_impl, + attrs = ATTRS, + doc = DOC, +) diff --git a/zig/private/zon2json.zig b/zig/private/zon2json.zig new file mode 100644 index 00000000..28ac79f1 --- /dev/null +++ b/zig/private/zon2json.zig @@ -0,0 +1,329 @@ +//! Resolve a Zig package dependency graph by recursively parsing `build.zig.zon` +//! manifests, and emit the merged graph as JSON on stdout. +//! +//! Usage: zon2json ... +//! +//! URL dependencies are fetched with ` fetch` into `` as +//! content-addressed tarballs (no source-tree unpacking); their `build.zig.zon` +//! manifests are extracted under `/`. The remaining arguments are +//! the root manifests to resolve. Resolving manifests ourselves (rather than via +//! `zig build --fetch --pkg-dir`) lets path dependencies inside fetched packages +//! resolve relative to the extracted tree. +//! +//! Packages are keyed by their Zig hash (URL dependencies) or by their resolved +//! absolute path (path dependencies), and listed in topological order: a package +//! always precedes any package that lists it as a dependency. The emitted JSON +//! has the shape: +//! +//! { +//! "roots": [{"deps": {"": ""}}], +//! "packages": {"": {"url": ..., "path": ..., "paths": [...], "deps": {"": ""}}} +//! } + +const std = @import("std"); +const Zoir = std.zig.Zoir; +const Allocator = std.mem.Allocator; +const Io = std.Io; +const flate = std.compress.flate; +const tar = std.tar; + +const Dep = struct { + name: []const u8, + url: ?[]const u8 = null, + hash: ?[]const u8 = null, + path: ?[]const u8 = null, +}; + +const Manifest = struct { + deps: []const Dep, + paths: []const []const u8, +}; + +const Edge = struct { + name: []const u8, + key: []const u8, +}; + +const Package = struct { + url: ?[]const u8, + path: ?[]const u8, + paths: []const []const u8, + deps: []const Edge, +}; + +const Resolved = struct { + key: []const u8, + url: ?[]const u8, + path: ?[]const u8, + dir: []const u8, +}; + +const Walker = struct { + arena: Allocator, + io: Io, + zig: []const u8, + global_cache: []const u8, + pkg_dir: []const u8, + packages: std.StringArrayHashMapUnmanaged(Package) = .empty, + visited: std.StringHashMapUnmanaged(void) = .empty, + + fn resolveDep(walker: *Walker, dep: Dep, parent_dir: []const u8) !Resolved { + if (dep.url) |url| { + const declared = dep.hash orelse fatal("URL dependency '{s}' is missing a hash", .{dep.name}); + const hash = try walker.fetch(url); + if (!std.mem.eql(u8, hash, declared)) { + fatal("hash mismatch for '{s}':\n declared: {s}\n fetched: {s}", .{ dep.name, declared, hash }); + } + return .{ + .key = hash, + .url = url, + .path = null, + .dir = try std.fs.path.join(walker.arena, &.{ walker.pkg_dir, hash }), + }; + } + if (dep.path) |rel| { + const dir = try std.fs.path.resolve(walker.arena, &.{ parent_dir, rel }); + return .{ .key = dir, .url = null, .path = dir, .dir = dir }; + } + fatal("dependency '{s}' has neither a url nor a path", .{dep.name}); + } + + /// Fetch a URL package as a content-addressed tarball (no source-tree + /// unpacking), then extract its `build.zig.zon` manifests into `pkg_dir` so + /// the walk can resolve the dependency graph from the filesystem. + fn fetch(walker: *Walker, url: []const u8) ![]const u8 { + const result = try std.process.run(walker.arena, walker.io, .{ + .argv = &.{ walker.zig, "fetch", "--global-cache-dir", walker.global_cache, url }, + }); + switch (result.term) { + .exited => |code| if (code != 0) fatal("`zig fetch {s}` failed:\n{s}", .{ url, result.stderr }), + .signal => |sig| fatal("`zig fetch {s}` killed by signal {d}:\nstdout:\n{s}\nstderr:\n{s}", .{ url, sig, result.stdout, result.stderr }), + else => |term| fatal("`zig fetch {s}` terminated abnormally ({s}):\n{s}", .{ url, @tagName(term), result.stderr }), + } + const hash = try walker.arena.dupe(u8, std.mem.trim(u8, result.stdout, " \t\r\n")); + + const dest = try std.fs.path.join(walker.arena, &.{ walker.pkg_dir, hash }); + if (Io.Dir.cwd().access(walker.io, dest, .{})) |_| return hash else |_| {} + try walker.extractManifests(hash, dest); + return hash; + } + + fn extractManifests(walker: *Walker, hash: []const u8, dest: []const u8) !void { + const arena = walker.arena; + const io = walker.io; + const tarball = try std.fmt.allocPrint(arena, "{s}/p/{s}.tar.gz", .{ walker.global_cache, hash }); + + var file = try Io.Dir.cwd().openFile(io, tarball, .{}); + defer file.close(io); + var read_buffer: [64 * 1024]u8 = undefined; + var file_reader = file.reader(io, &read_buffer); + var window: [flate.max_window_len]u8 = undefined; + var decompress: flate.Decompress = .init(&file_reader.interface, .gzip, &window); + + var name_buffer: [std.fs.max_path_bytes]u8 = undefined; + var link_name_buffer: [std.fs.max_path_bytes]u8 = undefined; + var iterator: tar.Iterator = .init(&decompress.reader, .{ + .file_name_buffer = &name_buffer, + .link_name_buffer = &link_name_buffer, + }); + + // Collect every manifest, tracking the shallowest one's directory as the + // archive prefix to strip (the tarball nests the package under it). + const Found = struct { name: []const u8, content: []const u8 }; + var manifests: std.ArrayList(Found) = .empty; + var prefix: []const u8 = ""; + var have_prefix = false; + while (try iterator.next()) |entry| { + if (entry.kind == .directory) continue; + if (!std.mem.eql(u8, std.fs.path.basename(entry.name), "build.zig.zon")) continue; + var content: std.Io.Writer.Allocating = .init(arena); + try iterator.streamRemaining(entry, &content.writer); + const name = try arena.dupe(u8, entry.name); + const dir = std.fs.path.dirname(name) orelse ""; + if (!have_prefix or dir.len < prefix.len) { + prefix = dir; + have_prefix = true; + } + try manifests.append(arena, .{ .name = name, .content = content.written() }); + } + + try Io.Dir.cwd().createDirPath(io, dest); + var dest_dir = try Io.Dir.cwd().openDir(io, dest, .{}); + defer dest_dir.close(io); + for (manifests.items) |found| { + const rel = if (prefix.len == 0) found.name else found.name[prefix.len + 1 ..]; + if (std.fs.path.dirname(rel)) |parent| try dest_dir.createDirPath(io, parent); + try dest_dir.writeFile(io, .{ .sub_path = rel, .data = found.content }); + } + } + + fn resolveEdges(walker: *Walker, manifest: Manifest, dir: []const u8) ![]const Edge { + var edges: std.ArrayList(Edge) = .empty; + for (manifest.deps) |dep| { + const resolved = try walker.resolveDep(dep, dir); + try edges.append(walker.arena, .{ .name = dep.name, .key = resolved.key }); + try walker.walk(resolved); + } + return edges.items; + } + + fn walk(walker: *Walker, resolved: Resolved) anyerror!void { + const gop = try walker.visited.getOrPut(walker.arena, resolved.key); + if (gop.found_existing) return; + + const manifest_path = try std.fs.path.join(walker.arena, &.{ resolved.dir, "build.zig.zon" }); + const manifest = try parseManifest(walker.arena, walker.io, manifest_path); + const edges = try walker.resolveEdges(manifest, resolved.dir); + + // Append only after the dependencies have been walked, so `packages` ends + // up in topological order: a package precedes any package depending on it. + try walker.packages.put(walker.arena, resolved.key, .{ + .url = resolved.url, + .path = resolved.path, + .paths = manifest.paths, + .deps = edges, + }); + } +}; + +pub fn main(init: std.process.Init) !void { + const arena = init.arena.allocator(); + const io = init.io; + + const args = try init.minimal.args.toSlice(arena); + if (args.len < 4) fatal("usage: zon2json ...", .{}); + + var walker: Walker = .{ + .arena = arena, + .io = io, + .zig = args[1], + .global_cache = args[2], + .pkg_dir = args[3], + }; + + var roots: std.ArrayList([]const Edge) = .empty; + for (args[4..]) |root_path| { + const dir = std.fs.path.dirname(root_path) orelse "."; + const manifest = try parseManifest(arena, io, root_path); + try roots.append(arena, try walker.resolveEdges(manifest, dir)); + } + + var stdout_buffer: [4096]u8 = undefined; + var stdout = std.Io.File.stdout().writerStreaming(io, &stdout_buffer); + const writer = &stdout.interface; + + var json: std.json.Stringify = .{ .writer = writer }; + try json.beginObject(); + + try json.objectField("roots"); + try json.beginArray(); + for (roots.items) |edges| { + try json.beginObject(); + try json.objectField("deps"); + try writeEdges(&json, edges); + try json.endObject(); + } + try json.endArray(); + + try json.objectField("packages"); + try json.beginObject(); + for (walker.packages.keys(), walker.packages.values()) |key, package| { + try json.objectField(key); + try json.beginObject(); + try json.objectField("url"); + try json.write(package.url); + try json.objectField("path"); + try json.write(package.path); + try json.objectField("paths"); + try json.write(package.paths); + try json.objectField("deps"); + try writeEdges(&json, package.deps); + try json.endObject(); + } + try json.endObject(); + + try json.endObject(); + try writer.writeByte('\n'); + try writer.flush(); +} + +fn writeEdges(json: *std.json.Stringify, edges: []const Edge) !void { + try json.beginObject(); + for (edges) |edge| { + try json.objectField(edge.name); + try json.write(edge.key); + } + try json.endObject(); +} + +fn parseManifest(arena: Allocator, io: Io, path: []const u8) !Manifest { + const source = try std.Io.Dir.cwd().readFileAllocOptions(io, path, arena, .unlimited, .of(u8), 0); + + const ast = try std.zig.Ast.parse(arena, source, .zon); + const zoir = try std.zig.ZonGen.generate(arena, ast, .{}); + if (zoir.compile_errors.len != 0) fatal("'{s}' is not valid ZON", .{path}); + + var deps: std.ArrayList(Dep) = .empty; + var paths: std.ArrayList([]const u8) = .empty; + + switch (Zoir.Node.Index.root.get(zoir)) { + .struct_literal => |fields| for (fields.names, 0..) |name, i| { + const field = name.get(zoir); + const value = fields.vals.at(@intCast(i)); + if (std.mem.eql(u8, field, "dependencies")) { + try parseDeps(arena, zoir, value, &deps); + } else if (std.mem.eql(u8, field, "paths")) { + try parsePaths(arena, zoir, value, &paths); + } + }, + else => fatal("'{s}' does not contain a struct", .{path}), + } + + return .{ .deps = deps.items, .paths = paths.items }; +} + +fn parseDeps(arena: Allocator, zoir: Zoir, index: Zoir.Node.Index, deps: *std.ArrayList(Dep)) !void { + const fields = switch (index.get(zoir)) { + .struct_literal => |fields| fields, + else => return, + }; + for (fields.names, 0..) |name, i| { + var dep: Dep = .{ .name = name.get(zoir) }; + switch (fields.vals.at(@intCast(i)).get(zoir)) { + .struct_literal => |entry| for (entry.names, 0..) |key, j| { + const value = entry.vals.at(@intCast(j)); + const field = key.get(zoir); + if (std.mem.eql(u8, field, "url")) { + dep.url = stringOf(zoir, value); + } else if (std.mem.eql(u8, field, "hash")) { + dep.hash = stringOf(zoir, value); + } else if (std.mem.eql(u8, field, "path")) { + dep.path = stringOf(zoir, value); + } + }, + else => {}, + } + try deps.append(arena, dep); + } +} + +fn parsePaths(arena: Allocator, zoir: Zoir, index: Zoir.Node.Index, paths: *std.ArrayList([]const u8)) !void { + switch (index.get(zoir)) { + .array_literal => |elements| for (0..elements.len) |i| { + try paths.append(arena, stringOf(zoir, elements.at(@intCast(i)))); + }, + else => {}, + } +} + +fn stringOf(zoir: Zoir, index: Zoir.Node.Index) []const u8 { + return switch (index.get(zoir)) { + .string_literal => |string| string, + else => "", + }; +} + +fn fatal(comptime format: []const u8, args: anytype) noreturn { + std.debug.print("zon2json: " ++ format ++ "\n", args); + std.process.exit(1); +} diff --git a/zig/tests/BUILD.bazel b/zig/tests/BUILD.bazel index 48078ccc..139f1931 100644 --- a/zig/tests/BUILD.bazel +++ b/zig/tests/BUILD.bazel @@ -16,6 +16,9 @@ load(":translate_c_action_test.bzl", "translate_c_action_test_suite") load(":use_cc_common_link_test.bzl", "use_cc_common_link_test_suite") load(":util.bzl", "forward_exec_settings") load(":versions_test.bzl", "versions_test_suite") +load(":zig_deps_hub_test.bzl", "zig_deps_hub_test_suite") +load(":zig_package_test.bzl", "zig_package_test_suite") +load(":zig_packages_test.bzl", "zig_packages_test_suite") load(":zigopt_test.bzl", "zigopt_test_suite") forward_exec_settings(name = "exec_settings") @@ -52,6 +55,12 @@ translate_c_action_test_suite(name = "translate_c_action_test") versions_test_suite(name = "versions_test") +zig_deps_hub_test_suite(name = "zig_deps_hub_test") + +zig_package_test_suite(name = "zig_package_test") + +zig_packages_test_suite(name = "zig_packages_test") + zigopt_test_suite(name = "zigopt_test") bzl_library( diff --git a/zig/tests/integration_tests/BUILD.bazel b/zig/tests/integration_tests/BUILD.bazel index 5609d5c4..df3f5489 100644 --- a/zig/tests/integration_tests/BUILD.bazel +++ b/zig/tests/integration_tests/BUILD.bazel @@ -149,6 +149,32 @@ bazel_integration_tests( workspace_path = "mirrors", ) +packages_files = integration_test_utils.glob_workspace_files("packages") + [ + "//:all_files", + "//:bazelrc", + ".bazelrc.meta", +] + +zig_test( + name = "packages_tests_runner", + main = "packages_tests_runner.zig", + tags = ["manual"], + deps = [":integration_testing"], +) + +bazel_integration_test( + name = "packages_test", + size = "large", + bazel_version = bazel_binaries.versions.current, + tags = ["requires-network"] + integration_test_utils.DEFAULT_INTEGRATION_TEST_TAGS, + # `zig fetch` (`0.17.0-dev.813`) segfaults (SIGSEGV) on aarch64-macOS when + # fetching `file://` fixture tarballs. Retry on `0.17.0` release. + target_compatible_with = ["@platforms//os:linux"], + test_runner = ":packages_tests_runner", + workspace_files = packages_files, + workspace_path = "packages", +) + test_suite( name = "integration_tests", tags = integration_test_utils.DEFAULT_INTEGRATION_TEST_TAGS, @@ -162,6 +188,7 @@ test_suite( "bzlmod_test", bazel_binaries.versions.all, ) + [ + ":packages_test", ":zig_version_tests", ], ) diff --git a/zig/tests/integration_tests/integration_testing.zig b/zig/tests/integration_tests/integration_testing.zig index 8853247f..0b0245d6 100644 --- a/zig/tests/integration_tests/integration_testing.zig +++ b/zig/tests/integration_tests/integration_testing.zig @@ -200,6 +200,66 @@ pub const BitContext = struct { return true; } + pub const writeWorkspaceFile = if (is_zig_0_16_or_later) writeWorkspaceFile_016 else writeWorkspaceFile_pre_016; + + fn writeWorkspaceFile_pre_016(self: BitContext, sub_path: []const u8, content: []const u8) !void { + var workspace = try self.openWorkspace(); + defer closeWorkspaceDir(&workspace); + workspace.deleteFile(sub_path) catch {}; + var file = try workspace.createFile(sub_path, .{}); + defer file.close(); + try file.writeAll(content); + } + + fn writeWorkspaceFile_016(self: BitContext, sub_path: []const u8, content: []const u8) !void { + var workspace = try self.openWorkspace(); + defer closeWorkspaceDir(&workspace); + workspace.deleteFile(std.testing.io, sub_path) catch {}; + var file = try workspace.createFile(std.testing.io, sub_path, .{}); + defer file.close(std.testing.io); + var buffer: [4096]u8 = undefined; + var writer = file.writer(std.testing.io, &buffer); + try writer.interface.writeAll(content); + try writer.interface.flush(); + } + + /// Create a symlink at `sym_link_sub_path` pointing to `target`. Used to + /// introduce an in-package symlink at test time, since the harness stages the + /// workspace by dereferencing symlinks. + pub const symLinkWorkspaceFile = if (is_zig_0_16_or_later) symLinkWorkspaceFile_016 else symLinkWorkspaceFile_pre_016; + + fn symLinkWorkspaceFile_pre_016(self: BitContext, target: []const u8, sym_link_sub_path: []const u8) !void { + var workspace = try self.openWorkspace(); + defer closeWorkspaceDir(&workspace); + workspace.deleteFile(sym_link_sub_path) catch {}; + try workspace.symLink(target, sym_link_sub_path, .{}); + } + + fn symLinkWorkspaceFile_016(self: BitContext, target: []const u8, sym_link_sub_path: []const u8) !void { + var workspace = try self.openWorkspace(); + defer closeWorkspaceDir(&workspace); + workspace.deleteFile(std.testing.io, sym_link_sub_path) catch {}; + try workspace.symLink(std.testing.io, target, sym_link_sub_path, .{}); + } + + /// Replace each `needle` with its `replacement` in a workspace file, writing + /// a fresh file so the original source (a symlink in the test sandbox) is + /// never modified. + pub fn patchWorkspaceFile( + self: BitContext, + sub_path: []const u8, + replacements: []const [2][]const u8, + ) !void { + var content = try self.readWorkspaceFileAlloc(sub_path, 4 * 1024 * 1024); + for (replacements) |replacement| { + const patched = try std.mem.replaceOwned(u8, std.testing.allocator, content, replacement[0], replacement[1]); + std.testing.allocator.free(content); + content = patched; + } + defer std.testing.allocator.free(content); + try self.writeWorkspaceFile(sub_path, content); + } + pub const BazelResult = struct { success: bool, term: Term, diff --git a/zig/tests/integration_tests/packages/.bazelrc b/zig/tests/integration_tests/packages/.bazelrc new file mode 100644 index 00000000..15fc888e --- /dev/null +++ b/zig/tests/integration_tests/packages/.bazelrc @@ -0,0 +1 @@ +import %workspace%/../../../../.bazelrc.common diff --git a/zig/tests/integration_tests/packages/BUILD.bazel b/zig/tests/integration_tests/packages/BUILD.bazel new file mode 100644 index 00000000..1f1225a0 --- /dev/null +++ b/zig/tests/integration_tests/packages/BUILD.bazel @@ -0,0 +1,42 @@ +load("@rules_cc//cc:defs.bzl", "cc_library") +load("@rules_zig//zig:defs.bzl", "zig_binary") +load("@zig_deps//:defs.bzl", "zig_dep", "zig_deps") + +# `zig_deps()` imports each dependency's same-named module (including `multi`'s +# primary `multi` module). `zig_dep("multi", module = "widget")` selects a second +# module from the same dependency whose name differs from the dependency name. +zig_binary( + name = "binary", + main = "main.zig", + deps = zig_deps() + [ + "@child_module//:child_lib", + zig_dep( + "multi", + module = "widget", + ), + ], +) + +cc_library( + name = "mymath_impl", + srcs = ["mymath.c"], + visibility = ["//visibility:public"], +) + +cc_library( + name = "optmath_impl", + srcs = ["optmath.c"], + visibility = ["//visibility:public"], +) + +cc_library( + name = "dbgonly_impl", + srcs = ["dbgonly.c"], + visibility = ["//visibility:public"], +) + +cc_library( + name = "relonly_impl", + srcs = ["relonly.c"], + visibility = ["//visibility:public"], +) diff --git a/zig/tests/integration_tests/packages/MODULE.bazel b/zig/tests/integration_tests/packages/MODULE.bazel new file mode 100644 index 00000000..f8926db5 --- /dev/null +++ b/zig/tests/integration_tests/packages/MODULE.bazel @@ -0,0 +1,63 @@ +module(name = "packages") + +bazel_dep(name = "rules_zig", version = "0.0.0") +bazel_dep(name = "platforms", version = "0.0.6") +bazel_dep(name = "rules_cc", version = "0.1.1") +bazel_dep(name = "child_module", version = "0.0.0") + +local_path_override( + module_name = "rules_zig", + path = "../../../..", +) + +local_path_override( + module_name = "child_module", + path = "child", +) + +zig = use_extension("@rules_zig//zig:extensions.bzl", "zig") +zig.index(file = "extra-versions.json") +zig.toolchain( + default = True, + zig_version = "0.17.0-dev.813+2153f8143", +) +use_repo(zig, "zig_toolchains") + +zig_packages = use_extension("@rules_zig//zig:packages.bzl", "zig_packages") +zig_packages.from_file(build_zig_zon = "//:build.zig.zon") +zig_packages.from_file(build_zig_zon = "//path_deps/greeter:build.zig.zon") +zig_packages.from_file(build_zig_zon = "//path_deps/message:build.zig.zon") +zig_packages.system_library( + name = "mymath", + lib = "//:mymath_impl", +) +zig_packages.system_integration(name = "optmath") +zig_packages.system_library( + name = "optmath", + lib = "//:optmath_impl", +) +zig_packages.system_library( + name = "dbgonly", + lib = "//:dbgonly_impl", +) +zig_packages.system_library( + name = "relonly", + lib = "//:relonly_impl", +) +zig_packages.config( + name = "dbg", + optimize = "debug", +) +zig_packages.config( + name = "rel", + optimize = "release_fast", +) +zig_packages.configure( + configs = [ + "dbg", + "rel", + ], + fallback = "dbg", + package = "cfgdep", +) +use_repo(zig_packages, "zig_deps") diff --git a/zig/tests/integration_tests/packages/WORKSPACE b/zig/tests/integration_tests/packages/WORKSPACE new file mode 100644 index 00000000..e69de29b diff --git a/zig/tests/integration_tests/packages/build.zig b/zig/tests/integration_tests/packages/build.zig new file mode 100644 index 00000000..ab632e05 --- /dev/null +++ b/zig/tests/integration_tests/packages/build.zig @@ -0,0 +1,3 @@ +pub fn build(b: *@import("std").Build) void { + _ = b; +} diff --git a/zig/tests/integration_tests/packages/build.zig.zon b/zig/tests/integration_tests/packages/build.zig.zon new file mode 100644 index 00000000..434ded81 --- /dev/null +++ b/zig/tests/integration_tests/packages/build.zig.zon @@ -0,0 +1,78 @@ +.{ + .name = .packages, + .version = "0.0.0", + .fingerprint = 0x9bb5c0a73538e72b, + .dependencies = .{ + .leaf = .{ + .url = "__LEAF_URL__", + .hash = "__LEAF_HASH__", + }, + .host = .{ + .url = "__HOST_URL__", + .hash = "__HOST_HASH__", + }, + .top = .{ + .url = "__TOP_URL__", + .hash = "__TOP_HASH__", + }, + .multi = .{ + .url = "__MULTI_URL__", + .hash = "__MULTI_HASH__", + }, + .greeter = .{ + .path = "path_deps/greeter", + }, + .pruned = .{ + .url = "__PRUNED_URL__", + .hash = "__PRUNED_HASH__", + }, + .lib = .{ + .url = "__LIBV1_URL__", + .hash = "__LIBV1_HASH__", + }, + // Eager dependency on a package that has a *lazy* dependency; verifies + // that the importer fetches lazy deps eagerly. + .lazyhost = .{ + .url = "__LAZYHOST_URL__", + .hash = "__LAZYHOST_HASH__", + }, + .symlinked = .{ + .url = "__SYMLINKED_URL__", + .hash = "__SYMLINKED_HASH__", + }, + .genopts = .{ + .url = "__GENOPTS_URL__", + .hash = "__GENOPTS_HASH__", + }, + .usec = .{ + .url = "__USEC_URL__", + .hash = "__USEC_HASH__", + }, + .cdep = .{ + .url = "__CDEP_URL__", + .hash = "__CDEP_HASH__", + }, + .syslibdep = .{ + .url = "__SYSLIBDEP_URL__", + .hash = "__SYSLIBDEP_HASH__", + }, + .optdep = .{ + .url = "__OPTDEP_URL__", + .hash = "__OPTDEP_HASH__", + }, + .cppdep = .{ + .url = "__CPPDEP_URL__", + .hash = "__CPPDEP_HASH__", + }, + .cfgdep = .{ + .url = "__CFGDEP_URL__", + .hash = "__CFGDEP_HASH__", + }, + // A source-only sub-tree dependency; only enabled by the test. + // .srconly = .{ .url = "__SRCONLY_URL__", .hash = "__SRCONLY_HASH__" }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + }, +} diff --git a/zig/tests/integration_tests/packages/child/BUILD.bazel b/zig/tests/integration_tests/packages/child/BUILD.bazel new file mode 100644 index 00000000..7968bb1d --- /dev/null +++ b/zig/tests/integration_tests/packages/child/BUILD.bazel @@ -0,0 +1,10 @@ +load("@rules_zig//zig:defs.bzl", "zig_library") +load("@zig_deps//:defs.bzl", "zig_deps") + +zig_library( + name = "child_lib", + import_name = "child", + main = "src/child.zig", + visibility = ["//visibility:public"], + deps = zig_deps(), +) diff --git a/zig/tests/integration_tests/packages/child/MODULE.bazel b/zig/tests/integration_tests/packages/child/MODULE.bazel new file mode 100644 index 00000000..6097bb6c --- /dev/null +++ b/zig/tests/integration_tests/packages/child/MODULE.bazel @@ -0,0 +1,10 @@ +module( + name = "child_module", + version = "0.0.0", +) + +bazel_dep(name = "rules_zig", version = "0.0.0") + +zig_packages = use_extension("@rules_zig//zig:packages.bzl", "zig_packages") +zig_packages.from_file(build_zig_zon = "//:build.zig.zon") +use_repo(zig_packages, "zig_deps") diff --git a/zig/tests/integration_tests/packages/child/WORKSPACE b/zig/tests/integration_tests/packages/child/WORKSPACE new file mode 100644 index 00000000..e69de29b diff --git a/zig/tests/integration_tests/packages/child/build.zig.zon b/zig/tests/integration_tests/packages/child/build.zig.zon new file mode 100644 index 00000000..51f51de0 --- /dev/null +++ b/zig/tests/integration_tests/packages/child/build.zig.zon @@ -0,0 +1,21 @@ +.{ + .name = .child, + .version = "0.0.0", + .fingerprint = 0x2b9d6e1f7a4c8053, + .dependencies = .{ + .leaf = .{ + .url = "__LEAF_URL__", + .hash = "__LEAF_HASH__", + }, + // The same package `lib` as the root module depends on, but a different + // version: the two must resolve to separate spokes (no MVS). + .lib = .{ + .url = "__LIBV2_URL__", + .hash = "__LIBV2_HASH__", + }, + }, + .paths = .{ + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/child/src/child.zig b/zig/tests/integration_tests/packages/child/src/child.zig new file mode 100644 index 00000000..2494dbd2 --- /dev/null +++ b/zig/tests/integration_tests/packages/child/src/child.zig @@ -0,0 +1,4 @@ +const leaf = @import("leaf"); +const lib = @import("lib"); + +pub const value: u32 = leaf.value + lib.v2 + 100; diff --git a/zig/tests/integration_tests/packages/dbgonly.c b/zig/tests/integration_tests/packages/dbgonly.c new file mode 100644 index 00000000..4ae6362b --- /dev/null +++ b/zig/tests/integration_tests/packages/dbgonly.c @@ -0,0 +1,3 @@ +int dbg_compute(void) { + return 1; +} diff --git a/zig/tests/integration_tests/packages/extra-versions.json b/zig/tests/integration_tests/packages/extra-versions.json new file mode 100644 index 00000000..8929cf19 --- /dev/null +++ b/zig/tests/integration_tests/packages/extra-versions.json @@ -0,0 +1,148 @@ +{ + "master": { + "version": "0.17.0-dev.813+2153f8143", + "date": "2026-06-07", + "docs": "https://ziglang.org/documentation/master/", + "stdDocs": "https://ziglang.org/documentation/master/std/", + "src": { + "tarball": "https://ziglang.org/builds/zig-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "bcb2ec46a2353620f2d90fcda1b046895ac7ba16b6b9febc9a0f9bd48f568c31", + "size": "22688996" + }, + "bootstrap": { + "tarball": "https://ziglang.org/builds/zig-bootstrap-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "d93ea88bcd934b24695de5e445f3640d8e04afc544f88dbed6fef1d91c4c5292", + "size": "56681092" + }, + "x86_64-macos": { + "tarball": "https://ziglang.org/builds/zig-x86_64-macos-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "3938c46ae4bca3c13f423b09503e3ef00bb4b7ef12b8bc1e5122ede366057a5b", + "size": "59323140" + }, + "aarch64-macos": { + "tarball": "https://ziglang.org/builds/zig-aarch64-macos-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "36673d2513afa4a96c86780648ba504beedd7f0451389091cf9d53e38d5b4840", + "size": "53999760" + }, + "x86_64-linux": { + "tarball": "https://ziglang.org/builds/zig-x86_64-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "b0d46ffc4587b9e8dd0b524ee5bc4da1e67f28bba55e7c534cec64af2f2d7a74", + "size": "57296196" + }, + "aarch64-linux": { + "tarball": "https://ziglang.org/builds/zig-aarch64-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "aa67b418d50bdde3043cfe765016d5387a2333b514ada2c57f24baae4005c331", + "size": "52949940" + }, + "arm-linux": { + "tarball": "https://ziglang.org/builds/zig-arm-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "7e365014a7520ca405b18d8690d802592199ce178d9a3bbfa526b1417edcfaa5", + "size": "53772136" + }, + "riscv64-linux": { + "tarball": "https://ziglang.org/builds/zig-riscv64-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "e2282b0784722eb3e41a41dcdea257618bc02c6b10fdd462c5f8e514b09628da", + "size": "57219904" + }, + "powerpc64le-linux": { + "tarball": "https://ziglang.org/builds/zig-powerpc64le-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "4b4ffe65aa052e1399319f4d80b5d117e6902d514d52d7bca5dfce66153474ec", + "size": "57144644" + }, + "x86-linux": { + "tarball": "https://ziglang.org/builds/zig-x86-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "cb14dab396b988221e02c4736680a0e166048d5592fafc4ba2fde362d3bc78b3", + "size": "59897932" + }, + "loongarch64-linux": { + "tarball": "https://ziglang.org/builds/zig-loongarch64-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "9a078bfac64a0d51752de87eb31348d33035451043f9a03546bf57d05c070a43", + "size": "54315620" + }, + "s390x-linux": { + "tarball": "https://ziglang.org/builds/zig-s390x-linux-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "c631cf79d8a405edf9d941c621131ac262d4468566cdbf1442087f764df46eb7", + "size": "57127508" + }, + "x86_64-windows": { + "tarball": "https://ziglang.org/builds/zig-x86_64-windows-0.17.0-dev.813+2153f8143.zip", + "shasum": "2a8f1a34402076ab7931e4535bd379b20c83fc263d1387cb3f70cb2e397f9ebe", + "size": "101062660" + }, + "aarch64-windows": { + "tarball": "https://ziglang.org/builds/zig-aarch64-windows-0.17.0-dev.813+2153f8143.zip", + "shasum": "c335026c4b666a995ac2f4d5481f74f7f9a455dd7ab3620b3e04779d3f6055a8", + "size": "96847518" + }, + "x86-windows": { + "tarball": "https://ziglang.org/builds/zig-x86-windows-0.17.0-dev.813+2153f8143.zip", + "shasum": "f341544a7263651616c7ce292e23a257c85f892ac72c0eeea9a7f0c6e93351da", + "size": "102769164" + }, + "aarch64-freebsd": { + "tarball": "https://ziglang.org/builds/zig-aarch64-freebsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "aa0da8ed92903cb7e32b18bccd4d6d5770192105749af6053ca5faafe6afdfbe", + "size": "52893704" + }, + "arm-freebsd": { + "tarball": "https://ziglang.org/builds/zig-arm-freebsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "60b75fa5e895e9ef01b9a745495bd1ed48078a7a8bebd546278bda82be27dc1d", + "size": "54464888" + }, + "powerpc64le-freebsd": { + "tarball": "https://ziglang.org/builds/zig-powerpc64le-freebsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "22c735fb40f8d0ad7342660a6a8a405615f27b66432dd5021efca1267b51191c", + "size": "57177396" + }, + "riscv64-freebsd": { + "tarball": "https://ziglang.org/builds/zig-riscv64-freebsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "0876f2330308279466a6d86630d3510f0498d94e11238c5653bee7532e6f078d", + "size": "57367100" + }, + "x86_64-freebsd": { + "tarball": "https://ziglang.org/builds/zig-x86_64-freebsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "a75220898fe9f403d24e9712ef56fa7cf622ddb96be21f2d2b7f01535a4f26a9", + "size": "57464308" + }, + "aarch64-netbsd": { + "tarball": "https://ziglang.org/builds/zig-aarch64-netbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "c004ad712df83a2eed7a0724fd582a08bb09eaee262996c4e6b82680a22d7b6f", + "size": "52847860" + }, + "arm-netbsd": { + "tarball": "https://ziglang.org/builds/zig-arm-netbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "611fdc70b82e6bf88053a72efba73a788962ef0bd9008336e0202bb43eb67967", + "size": "55526764" + }, + "x86-netbsd": { + "tarball": "https://ziglang.org/builds/zig-x86-netbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "1296c141a8603a0911547e60532c63638cf5dd859ab4acf2ead33a925d64776b", + "size": "60476932" + }, + "x86_64-netbsd": { + "tarball": "https://ziglang.org/builds/zig-x86_64-netbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "bd88fad25ef1251d74a8051c30b886256c8b0f0292b4209b1013e925b54cd314", + "size": "57360376" + }, + "aarch64-openbsd": { + "tarball": "https://ziglang.org/builds/zig-aarch64-openbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "7730b3b2bc98317c154fda750a74790a3e6e79526a868a9bc04d63a9bb71c79e", + "size": "53347332" + }, + "arm-openbsd": { + "tarball": "https://ziglang.org/builds/zig-arm-openbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "d067209109995e5311c2f196f75fec59124d55aa140b5727c9b2eb386f46cf6a", + "size": "53971816" + }, + "riscv64-openbsd": { + "tarball": "https://ziglang.org/builds/zig-riscv64-openbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "32a6df5e4e0a3647ac4354b0a7f103a6bab9d0c74225ec5c0e0d9aea0f4d5d74", + "size": "57655868" + }, + "x86_64-openbsd": { + "tarball": "https://ziglang.org/builds/zig-x86_64-openbsd-0.17.0-dev.813+2153f8143.tar.xz", + "shasum": "fb173bd2caff90b7483a513f7759181de0b4f60d87698f89bc7caf8150a00e0a", + "size": "58620540" + } + } +} diff --git a/zig/tests/integration_tests/packages/fixtures/base/build.zig b/zig/tests/integration_tests/packages/fixtures/base/build.zig new file mode 100644 index 00000000..f21ce72e --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/base/build.zig @@ -0,0 +1,3 @@ +pub fn build(b: *@import("std").Build) void { + _ = b.addModule("base", .{ .root_source_file = b.path("src/base.zig") }); +} diff --git a/zig/tests/integration_tests/packages/fixtures/base/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/base/build.zig.zon new file mode 100644 index 00000000..378252c8 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/base/build.zig.zon @@ -0,0 +1 @@ +.{ .name = .base, .version = "0.0.0", .fingerprint = 0xc0b4fe61db8e1e53, .paths = .{ "build.zig", "build.zig.zon", "src" } } diff --git a/zig/tests/integration_tests/packages/fixtures/base/src/base.zig b/zig/tests/integration_tests/packages/fixtures/base/src/base.zig new file mode 100644 index 00000000..75c2e74c --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/base/src/base.zig @@ -0,0 +1 @@ +pub const value: u32 = 1; diff --git a/zig/tests/integration_tests/packages/fixtures/bottom/build.zig b/zig/tests/integration_tests/packages/fixtures/bottom/build.zig new file mode 100644 index 00000000..af72b345 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/bottom/build.zig @@ -0,0 +1,7 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const base = b.dependency("base", .{}); + const mod = b.addModule("bottom", .{ .root_source_file = b.path("src/bottom.zig") }); + mod.addImport("base", base.module("base")); +} diff --git a/zig/tests/integration_tests/packages/fixtures/bottom/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/bottom/build.zig.zon new file mode 100644 index 00000000..c8b16a19 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/bottom/build.zig.zon @@ -0,0 +1,9 @@ +.{ + .name = .bottom, + .version = "0.0.0", + .fingerprint = 0x895f72a49d5f57fe, + .dependencies = .{ + .base = .{ .url = "__BASE_URL__", .hash = "__BASE_HASH__" }, + }, + .paths = .{ "build.zig", "build.zig.zon", "src" }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/bottom/src/bottom.zig b/zig/tests/integration_tests/packages/fixtures/bottom/src/bottom.zig new file mode 100644 index 00000000..c0e85c0f --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/bottom/src/bottom.zig @@ -0,0 +1 @@ +const base = @import("base"); pub const value: u32 = base.value + 1; diff --git a/zig/tests/integration_tests/packages/fixtures/cdep/build.zig b/zig/tests/integration_tests/packages/fixtures/cdep/build.zig new file mode 100644 index 00000000..c573a638 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/cdep/build.zig @@ -0,0 +1,16 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const cdep = b.addModule("cdep", .{ + .root_source_file = b.path("src/cdep.zig"), + }); + cdep.addCSourceFile(.{ + .file = b.path("src/value.c"), + .flags = &.{"-DSCALE=3"}, + }); + cdep.addCSourceFile(.{ + .file = b.path("src/other.c"), + .flags = &.{"-DSCALE=7"}, + }); + cdep.addIncludePath(b.path("include")); +} diff --git a/zig/tests/integration_tests/packages/fixtures/cdep/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/cdep/build.zig.zon new file mode 100644 index 00000000..94b6eeee --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/cdep/build.zig.zon @@ -0,0 +1,11 @@ +.{ + .name = .cdep, + .version = "0.0.0", + .fingerprint = 0x0f860ad394dc4b83, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "include", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/cdep/include/value.h b/zig/tests/integration_tests/packages/fixtures/cdep/include/value.h new file mode 100644 index 00000000..cbecbc4c --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/cdep/include/value.h @@ -0,0 +1,7 @@ +#ifndef VALUE_H +#define VALUE_H + +int scaled_value(void); +int offset_value(void); + +#endif diff --git a/zig/tests/integration_tests/packages/fixtures/cdep/src/cdep.zig b/zig/tests/integration_tests/packages/fixtures/cdep/src/cdep.zig new file mode 100644 index 00000000..a756365b --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/cdep/src/cdep.zig @@ -0,0 +1,6 @@ +extern fn scaled_value() c_int; +extern fn offset_value() c_int; + +pub fn value() c_int { + return scaled_value() + offset_value(); +} diff --git a/zig/tests/integration_tests/packages/fixtures/cdep/src/other.c b/zig/tests/integration_tests/packages/fixtures/cdep/src/other.c new file mode 100644 index 00000000..34d97d10 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/cdep/src/other.c @@ -0,0 +1,9 @@ +#include "value.h" + +#if SCALE != 7 +#error "other.c expected -DSCALE=7" +#endif + +int offset_value(void) { + return SCALE * 2; +} diff --git a/zig/tests/integration_tests/packages/fixtures/cdep/src/value.c b/zig/tests/integration_tests/packages/fixtures/cdep/src/value.c new file mode 100644 index 00000000..2dfd1d2d --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/cdep/src/value.c @@ -0,0 +1,9 @@ +#include "value.h" + +#if SCALE != 3 +#error "value.c expected -DSCALE=3" +#endif + +int scaled_value(void) { + return SCALE * 14; +} diff --git a/zig/tests/integration_tests/packages/fixtures/cfgdep/build.zig b/zig/tests/integration_tests/packages/fixtures/cfgdep/build.zig new file mode 100644 index 00000000..73059968 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/cfgdep/build.zig @@ -0,0 +1,15 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const optimize = b.standardOptimizeOption(.{}); + const cfgdep = b.addModule("cfgdep", .{ + .root_source_file = b.path("src/cfgdep.zig"), + .target = b.standardTargetOptions(.{}), + .optimize = optimize, + }); + if (optimize == .Debug) { + cfgdep.linkSystemLibrary("dbgonly", .{}); + } else { + cfgdep.linkSystemLibrary("relonly", .{}); + } +} diff --git a/zig/tests/integration_tests/packages/fixtures/cfgdep/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/cfgdep/build.zig.zon new file mode 100644 index 00000000..94609520 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/cfgdep/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .cfgdep, + .version = "0.0.0", + .fingerprint = 0x88701c22be06c646, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/cfgdep/src/cfgdep.zig b/zig/tests/integration_tests/packages/fixtures/cfgdep/src/cfgdep.zig new file mode 100644 index 00000000..74459c4b --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/cfgdep/src/cfgdep.zig @@ -0,0 +1,12 @@ +const builtin = @import("builtin"); + +extern fn dbg_compute() c_int; +extern fn rel_compute() c_int; + +pub fn value() c_int { + if (builtin.mode == .Debug) { + return dbg_compute(); + } else { + return rel_compute(); + } +} diff --git a/zig/tests/integration_tests/packages/fixtures/cppdep/build.zig b/zig/tests/integration_tests/packages/fixtures/cppdep/build.zig new file mode 100644 index 00000000..d89c6d5c --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/cppdep/build.zig @@ -0,0 +1,9 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const cppdep = b.addModule("cppdep", .{ + .root_source_file = b.path("src/cppdep.zig"), + .link_libcpp = true, + }); + cppdep.addCSourceFile(.{ .file = b.path("src/impl.cpp") }); +} diff --git a/zig/tests/integration_tests/packages/fixtures/cppdep/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/cppdep/build.zig.zon new file mode 100644 index 00000000..c449fdd2 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/cppdep/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .cppdep, + .version = "0.0.0", + .fingerprint = 0xaa1e1126b9a5e407, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/cppdep/src/cppdep.zig b/zig/tests/integration_tests/packages/fixtures/cppdep/src/cppdep.zig new file mode 100644 index 00000000..7de853a6 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/cppdep/src/cppdep.zig @@ -0,0 +1,5 @@ +extern fn cpp_compute(c_int) c_int; + +pub fn value() c_int { + return cpp_compute(2); +} diff --git a/zig/tests/integration_tests/packages/fixtures/cppdep/src/impl.cpp b/zig/tests/integration_tests/packages/fixtures/cppdep/src/impl.cpp new file mode 100644 index 00000000..fcd1d076 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/cppdep/src/impl.cpp @@ -0,0 +1,8 @@ +// Uses operator new/delete (C++ runtime symbols whose mangling is shared by +// libc++ and libstdc++). +extern "C" int cpp_compute(int x) { + int *p = new int(x); + int r = *p + 1; + delete p; + return r; +} diff --git a/zig/tests/integration_tests/packages/fixtures/genopts/build.zig b/zig/tests/integration_tests/packages/fixtures/genopts/build.zig new file mode 100644 index 00000000..86dc06d2 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/genopts/build.zig @@ -0,0 +1,11 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const options = b.addOptions(); + options.addOption(bool, "feature", true); + + const genopts = b.addModule("genopts", .{ + .root_source_file = b.path("src/root.zig"), + }); + genopts.addImport("build_options", options.createModule()); +} diff --git a/zig/tests/integration_tests/packages/fixtures/genopts/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/genopts/build.zig.zon new file mode 100644 index 00000000..1eab8e51 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/genopts/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .genopts, + .version = "0.0.0", + .fingerprint = 0x18ade9a14ecf9519, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/genopts/src/root.zig b/zig/tests/integration_tests/packages/fixtures/genopts/src/root.zig new file mode 100644 index 00000000..289b1702 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/genopts/src/root.zig @@ -0,0 +1,3 @@ +const build_options = @import("build_options"); + +pub const value: u32 = if (build_options.feature) 7 else 0; diff --git a/zig/tests/integration_tests/packages/fixtures/host/build.zig b/zig/tests/integration_tests/packages/fixtures/host/build.zig new file mode 100644 index 00000000..9cd47023 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/host/build.zig @@ -0,0 +1,9 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const foo = b.dependency("foo", .{}); + const bar = b.dependency("bar", .{}); + const mod = b.addModule("host", .{ .root_source_file = b.path("src/root.zig") }); + mod.addImport("foo", foo.module("foo")); + mod.addImport("barlib", bar.module("bar")); +} diff --git a/zig/tests/integration_tests/packages/fixtures/host/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/host/build.zig.zon new file mode 100644 index 00000000..be846b05 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/host/build.zig.zon @@ -0,0 +1,15 @@ +.{ + .name = .host, + .version = "0.0.0", + .fingerprint = 0xcf2713fdd0ee5aad, + .dependencies = .{ + .foo = .{ .path = "libs/foo" }, + .bar = .{ .path = "libs/bar" }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "libs", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/host/libs/bar/build.zig b/zig/tests/integration_tests/packages/fixtures/host/libs/bar/build.zig new file mode 100644 index 00000000..6e79e82b --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/host/libs/bar/build.zig @@ -0,0 +1,3 @@ +pub fn build(b: *@import("std").Build) void { + _ = b.addModule("bar", .{ .root_source_file = b.path("src/bar.zig") }); +} diff --git a/zig/tests/integration_tests/packages/fixtures/host/libs/bar/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/host/libs/bar/build.zig.zon new file mode 100644 index 00000000..83101d6d --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/host/libs/bar/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .bar, + .version = "0.0.0", + .fingerprint = 0x76ff8caad565ac33, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/host/libs/bar/src/bar.zig b/zig/tests/integration_tests/packages/fixtures/host/libs/bar/src/bar.zig new file mode 100644 index 00000000..da3d5ffb --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/host/libs/bar/src/bar.zig @@ -0,0 +1 @@ +pub const value: u32 = 9; diff --git a/zig/tests/integration_tests/packages/fixtures/host/libs/foo/build.zig b/zig/tests/integration_tests/packages/fixtures/host/libs/foo/build.zig new file mode 100644 index 00000000..94503b0d --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/host/libs/foo/build.zig @@ -0,0 +1,7 @@ +pub fn build(b: *@import("std").Build) void { + const bar = b.dependency("bar", .{}); + const leaf = b.dependency("leaf", .{}); + const foo = b.addModule("foo", .{ .root_source_file = b.path("src/foo.zig") }); + foo.addImport("bar", bar.module("bar")); + foo.addImport("leaflib", leaf.module("leaf")); +} diff --git a/zig/tests/integration_tests/packages/fixtures/host/libs/foo/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/host/libs/foo/build.zig.zon new file mode 100644 index 00000000..262317dc --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/host/libs/foo/build.zig.zon @@ -0,0 +1,18 @@ +.{ + .name = .foo, + .version = "0.0.0", + .fingerprint = 0x8c736521d1c2c9ec, + .dependencies = .{ + .bar = .{ .path = "libs/bar" }, + .leaf = .{ + .url = "__LEAF_URL__", + .hash = "__LEAF_HASH__", + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "libs", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/host/libs/foo/libs/bar/build.zig b/zig/tests/integration_tests/packages/fixtures/host/libs/foo/libs/bar/build.zig new file mode 100644 index 00000000..6e79e82b --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/host/libs/foo/libs/bar/build.zig @@ -0,0 +1,3 @@ +pub fn build(b: *@import("std").Build) void { + _ = b.addModule("bar", .{ .root_source_file = b.path("src/bar.zig") }); +} diff --git a/zig/tests/integration_tests/packages/fixtures/host/libs/foo/libs/bar/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/host/libs/foo/libs/bar/build.zig.zon new file mode 100644 index 00000000..50109672 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/host/libs/foo/libs/bar/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .bar, + .version = "0.0.0", + .fingerprint = 0x76ff8caa9fb92ea7, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/host/libs/foo/libs/bar/src/bar.zig b/zig/tests/integration_tests/packages/fixtures/host/libs/foo/libs/bar/src/bar.zig new file mode 100644 index 00000000..cb3a6e63 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/host/libs/foo/libs/bar/src/bar.zig @@ -0,0 +1 @@ +pub const value: u32 = 5; diff --git a/zig/tests/integration_tests/packages/fixtures/host/libs/foo/src/foo.zig b/zig/tests/integration_tests/packages/fixtures/host/libs/foo/src/foo.zig new file mode 100644 index 00000000..c897c965 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/host/libs/foo/src/foo.zig @@ -0,0 +1,4 @@ +const bar = @import("bar"); +const leaf = @import("leaflib"); + +pub const value: u32 = bar.value + leaf.value + 42; diff --git a/zig/tests/integration_tests/packages/fixtures/host/src/root.zig b/zig/tests/integration_tests/packages/fixtures/host/src/root.zig new file mode 100644 index 00000000..77d6862b --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/host/src/root.zig @@ -0,0 +1,4 @@ +const foo = @import("foo"); +const barlib = @import("barlib"); + +pub const value = foo.value + barlib.value; diff --git a/zig/tests/integration_tests/packages/fixtures/lazyhost/build.zig b/zig/tests/integration_tests/packages/fixtures/lazyhost/build.zig new file mode 100644 index 00000000..a0650d9e --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/lazyhost/build.zig @@ -0,0 +1,8 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const mod = b.addModule("lazyhost", .{ .root_source_file = b.path("src/lazyhost.zig") }); + if (b.lazyDependency("lazyleaf", .{})) |lazyleaf| { + mod.addImport("lazyleaf", lazyleaf.module("lazyleaf")); + } +} diff --git a/zig/tests/integration_tests/packages/fixtures/lazyhost/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/lazyhost/build.zig.zon new file mode 100644 index 00000000..c2e1d2d8 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/lazyhost/build.zig.zon @@ -0,0 +1,17 @@ +.{ + .name = .lazyhost, + .version = "0.0.0", + .fingerprint = 0x7fbf4d34c537a82c, + .dependencies = .{ + .lazyleaf = .{ + .url = "__LAZYLEAF_URL__", + .hash = "__LAZYLEAF_HASH__", + .lazy = true, + }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/lazyhost/src/lazyhost.zig b/zig/tests/integration_tests/packages/fixtures/lazyhost/src/lazyhost.zig new file mode 100644 index 00000000..6bfe0f57 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/lazyhost/src/lazyhost.zig @@ -0,0 +1,3 @@ +const lazyleaf = @import("lazyleaf"); + +pub const value: u32 = lazyleaf.value + 1; diff --git a/zig/tests/integration_tests/packages/fixtures/lazyleaf/build.zig b/zig/tests/integration_tests/packages/fixtures/lazyleaf/build.zig new file mode 100644 index 00000000..bb211d01 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/lazyleaf/build.zig @@ -0,0 +1,3 @@ +pub fn build(b: *@import("std").Build) void { + _ = b.addModule("lazyleaf", .{ .root_source_file = b.path("src/lazyleaf.zig") }); +} diff --git a/zig/tests/integration_tests/packages/fixtures/lazyleaf/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/lazyleaf/build.zig.zon new file mode 100644 index 00000000..a1089191 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/lazyleaf/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .lazyleaf, + .version = "0.0.0", + .fingerprint = 0x76075e2e0af3a2a5, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/lazyleaf/src/lazyleaf.zig b/zig/tests/integration_tests/packages/fixtures/lazyleaf/src/lazyleaf.zig new file mode 100644 index 00000000..41abe6da --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/lazyleaf/src/lazyleaf.zig @@ -0,0 +1 @@ +pub const value: u32 = 2000; diff --git a/zig/tests/integration_tests/packages/fixtures/leaf/build.zig b/zig/tests/integration_tests/packages/fixtures/leaf/build.zig new file mode 100644 index 00000000..73d3aa47 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/leaf/build.zig @@ -0,0 +1,3 @@ +pub fn build(b: *@import("std").Build) void { + _ = b.addModule("leaf", .{ .root_source_file = b.path("src/leaf.zig") }); +} diff --git a/zig/tests/integration_tests/packages/fixtures/leaf/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/leaf/build.zig.zon new file mode 100644 index 00000000..a3a6049c --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/leaf/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .leaf, + .version = "0.0.0", + .fingerprint = 0xc69f00e73c05ec58, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/leaf/src/leaf.zig b/zig/tests/integration_tests/packages/fixtures/leaf/src/leaf.zig new file mode 100644 index 00000000..ef1c4a67 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/leaf/src/leaf.zig @@ -0,0 +1 @@ +pub const value: u32 = 7; diff --git a/zig/tests/integration_tests/packages/fixtures/left/build.zig b/zig/tests/integration_tests/packages/fixtures/left/build.zig new file mode 100644 index 00000000..38390b44 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/left/build.zig @@ -0,0 +1,7 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const bottom = b.dependency("bottom", .{}); + const mod = b.addModule("left", .{ .root_source_file = b.path("src/left.zig") }); + mod.addImport("bottom", bottom.module("bottom")); +} diff --git a/zig/tests/integration_tests/packages/fixtures/left/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/left/build.zig.zon new file mode 100644 index 00000000..a054d7c6 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/left/build.zig.zon @@ -0,0 +1,9 @@ +.{ + .name = .left, + .version = "0.0.0", + .fingerprint = 0x7a67e768d7122cc8, + .dependencies = .{ + .bottom = .{ .url = "__BOTTOM_URL__", .hash = "__BOTTOM_HASH__" }, + }, + .paths = .{ "build.zig", "build.zig.zon", "src" }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/left/src/left.zig b/zig/tests/integration_tests/packages/fixtures/left/src/left.zig new file mode 100644 index 00000000..975fde7d --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/left/src/left.zig @@ -0,0 +1 @@ +const bottom = @import("bottom"); pub const value: u32 = bottom.value + 1; diff --git a/zig/tests/integration_tests/packages/fixtures/libv1/build.zig b/zig/tests/integration_tests/packages/fixtures/libv1/build.zig new file mode 100644 index 00000000..cda0cc35 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/libv1/build.zig @@ -0,0 +1,3 @@ +pub fn build(b: *@import("std").Build) void { + _ = b.addModule("lib", .{ .root_source_file = b.path("src/lib.zig") }); +} diff --git a/zig/tests/integration_tests/packages/fixtures/libv1/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/libv1/build.zig.zon new file mode 100644 index 00000000..8b837914 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/libv1/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .lib, + .version = "1.0.0", + .fingerprint = 0xa90f3bccad0f02e5, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/libv1/src/lib.zig b/zig/tests/integration_tests/packages/fixtures/libv1/src/lib.zig new file mode 100644 index 00000000..c4d68974 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/libv1/src/lib.zig @@ -0,0 +1,3 @@ +// Distinct symbol from v2, so a consumer wired to the wrong version fails to +// compile rather than silently picking the other. +pub const v1: u32 = 1; diff --git a/zig/tests/integration_tests/packages/fixtures/libv2/build.zig b/zig/tests/integration_tests/packages/fixtures/libv2/build.zig new file mode 100644 index 00000000..cda0cc35 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/libv2/build.zig @@ -0,0 +1,3 @@ +pub fn build(b: *@import("std").Build) void { + _ = b.addModule("lib", .{ .root_source_file = b.path("src/lib.zig") }); +} diff --git a/zig/tests/integration_tests/packages/fixtures/libv2/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/libv2/build.zig.zon new file mode 100644 index 00000000..6e43cddd --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/libv2/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .lib, + .version = "2.0.0", + .fingerprint = 0xa90f3bccad0f02e5, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/libv2/src/lib.zig b/zig/tests/integration_tests/packages/fixtures/libv2/src/lib.zig new file mode 100644 index 00000000..06c7ec96 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/libv2/src/lib.zig @@ -0,0 +1,3 @@ +// Distinct symbol from v1, so a consumer wired to the wrong version fails to +// compile rather than silently picking the other. +pub const v2: u32 = 2; diff --git a/zig/tests/integration_tests/packages/fixtures/multi/build.zig b/zig/tests/integration_tests/packages/fixtures/multi/build.zig new file mode 100644 index 00000000..40cc4c8e --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/multi/build.zig @@ -0,0 +1,11 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const widget = b.addModule("widget", .{ .root_source_file = b.path("src/widget.zig") }); + + const internal = b.createModule(.{ .root_source_file = b.path("src/internal.zig") }); + + const multi = b.addModule("multi", .{ .root_source_file = b.path("src/multi.zig") }); + multi.addImport("widget", widget); + multi.addImport("internal", internal); +} diff --git a/zig/tests/integration_tests/packages/fixtures/multi/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/multi/build.zig.zon new file mode 100644 index 00000000..c451adb5 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/multi/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .multi, + .version = "0.0.0", + .fingerprint = 0xc5914305857f07ef, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/multi/src/internal.zig b/zig/tests/integration_tests/packages/fixtures/multi/src/internal.zig new file mode 100644 index 00000000..a2730532 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/multi/src/internal.zig @@ -0,0 +1 @@ +pub const value: u32 = 99; diff --git a/zig/tests/integration_tests/packages/fixtures/multi/src/multi.zig b/zig/tests/integration_tests/packages/fixtures/multi/src/multi.zig new file mode 100644 index 00000000..24e109b2 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/multi/src/multi.zig @@ -0,0 +1,4 @@ +const widget = @import("widget"); +const internal = @import("internal"); + +pub const value: u32 = widget.value + internal.value + 10; diff --git a/zig/tests/integration_tests/packages/fixtures/multi/src/widget.zig b/zig/tests/integration_tests/packages/fixtures/multi/src/widget.zig new file mode 100644 index 00000000..ca458229 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/multi/src/widget.zig @@ -0,0 +1 @@ +pub const value: u32 = 3; diff --git a/zig/tests/integration_tests/packages/fixtures/optdep/build.zig b/zig/tests/integration_tests/packages/fixtures/optdep/build.zig new file mode 100644 index 00000000..71f90fe8 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/optdep/build.zig @@ -0,0 +1,11 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const m = b.addModule("optdep", .{ + .root_source_file = b.path("src/optdep.zig"), + .target = b.standardTargetOptions(.{}), + }); + if (b.systemIntegrationOption("optmath", .{})) { + m.linkSystemLibrary("optmath", .{}); + } +} diff --git a/zig/tests/integration_tests/packages/fixtures/optdep/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/optdep/build.zig.zon new file mode 100644 index 00000000..07285d28 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/optdep/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .optdep, + .version = "0.0.0", + .fingerprint = 0x52be460a7daafc05, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/optdep/src/optdep.zig b/zig/tests/integration_tests/packages/fixtures/optdep/src/optdep.zig new file mode 100644 index 00000000..f30e70f5 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/optdep/src/optdep.zig @@ -0,0 +1,5 @@ +extern fn opt_compute(c_int) c_int; + +pub fn compute(x: c_int) c_int { + return opt_compute(x); +} diff --git a/zig/tests/integration_tests/packages/fixtures/pruned/build.zig b/zig/tests/integration_tests/packages/fixtures/pruned/build.zig new file mode 100644 index 00000000..dd58807c --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/pruned/build.zig @@ -0,0 +1,3 @@ +pub fn build(b: *@import("std").Build) void { + _ = b.addModule("pruned", .{ .root_source_file = b.path("src/pruned.zig") }); +} diff --git a/zig/tests/integration_tests/packages/fixtures/pruned/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/pruned/build.zig.zon new file mode 100644 index 00000000..7b8f0d5c --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/pruned/build.zig.zon @@ -0,0 +1,12 @@ +.{ + .name = .pruned, + .version = "0.0.0", + .fingerprint = 0x750f93ea2c71346e, + // `extra.zig` and `tests/` are deliberately omitted so the packer must prune + // them from the tarball and the hash, matching `zig fetch`. + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/pruned/extra.zig b/zig/tests/integration_tests/packages/fixtures/pruned/extra.zig new file mode 100644 index 00000000..5d7797fa --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/pruned/extra.zig @@ -0,0 +1 @@ +pub const excluded = "not packaged"; diff --git a/zig/tests/integration_tests/packages/fixtures/pruned/src/pruned.zig b/zig/tests/integration_tests/packages/fixtures/pruned/src/pruned.zig new file mode 100644 index 00000000..7d8c47b3 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/pruned/src/pruned.zig @@ -0,0 +1 @@ +pub const value: u32 = 13; diff --git a/zig/tests/integration_tests/packages/fixtures/pruned/tests/test.zig b/zig/tests/integration_tests/packages/fixtures/pruned/tests/test.zig new file mode 100644 index 00000000..b55512b5 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/pruned/tests/test.zig @@ -0,0 +1 @@ +pub const unused = 0; diff --git a/zig/tests/integration_tests/packages/fixtures/right/build.zig b/zig/tests/integration_tests/packages/fixtures/right/build.zig new file mode 100644 index 00000000..c39c486d --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/right/build.zig @@ -0,0 +1,7 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const bottom = b.dependency("bottom", .{}); + const mod = b.addModule("right", .{ .root_source_file = b.path("src/right.zig") }); + mod.addImport("bottom", bottom.module("bottom")); +} diff --git a/zig/tests/integration_tests/packages/fixtures/right/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/right/build.zig.zon new file mode 100644 index 00000000..24f25480 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/right/build.zig.zon @@ -0,0 +1,9 @@ +.{ + .name = .right, + .version = "0.0.0", + .fingerprint = 0xb4ca7514bed08aba, + .dependencies = .{ + .bottom = .{ .url = "__BOTTOM_URL__", .hash = "__BOTTOM_HASH__" }, + }, + .paths = .{ "build.zig", "build.zig.zon", "src" }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/right/src/right.zig b/zig/tests/integration_tests/packages/fixtures/right/src/right.zig new file mode 100644 index 00000000..975fde7d --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/right/src/right.zig @@ -0,0 +1 @@ +const bottom = @import("bottom"); pub const value: u32 = bottom.value + 1; diff --git a/zig/tests/integration_tests/packages/fixtures/srconly/build.zig b/zig/tests/integration_tests/packages/fixtures/srconly/build.zig new file mode 100644 index 00000000..09c03d5c --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/srconly/build.zig @@ -0,0 +1,5 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + _ = b.addModule("srconly", .{ .root_source_file = b.path("src/root.zig") }); +} diff --git a/zig/tests/integration_tests/packages/fixtures/srconly/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/srconly/build.zig.zon new file mode 100644 index 00000000..def03386 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/srconly/build.zig.zon @@ -0,0 +1,14 @@ +.{ + .name = .srconly, + .version = "0.0.0", + .fingerprint = 0x190958d3eade8856, + .dependencies = .{ + .data = .{ .path = "libs/data" }, + }, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + "libs", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/srconly/libs/data/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/srconly/libs/data/build.zig.zon new file mode 100644 index 00000000..17503507 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/srconly/libs/data/build.zig.zon @@ -0,0 +1,9 @@ +.{ + .name = .data, + .version = "0.0.0", + .fingerprint = 0x190958d3d1799fc3, + .paths = .{ + "build.zig.zon", + "data.zig", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/srconly/libs/data/data.zig b/zig/tests/integration_tests/packages/fixtures/srconly/libs/data/data.zig new file mode 100644 index 00000000..ef1c4a67 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/srconly/libs/data/data.zig @@ -0,0 +1 @@ +pub const value: u32 = 7; diff --git a/zig/tests/integration_tests/packages/fixtures/srconly/src/root.zig b/zig/tests/integration_tests/packages/fixtures/srconly/src/root.zig new file mode 100644 index 00000000..a2730532 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/srconly/src/root.zig @@ -0,0 +1 @@ +pub const value: u32 = 99; diff --git a/zig/tests/integration_tests/packages/fixtures/symlinked/build.zig b/zig/tests/integration_tests/packages/fixtures/symlinked/build.zig new file mode 100644 index 00000000..f9c44c90 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/symlinked/build.zig @@ -0,0 +1,3 @@ +pub fn build(b: *@import("std").Build) void { + _ = b.addModule("symlinked", .{ .root_source_file = b.path("src/symlinked.zig") }); +} diff --git a/zig/tests/integration_tests/packages/fixtures/symlinked/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/symlinked/build.zig.zon new file mode 100644 index 00000000..bfdab3bf --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/symlinked/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .symlinked, + .version = "0.0.0", + .fingerprint = 0x5063086f099ee769, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/symlinked/src/real.zig b/zig/tests/integration_tests/packages/fixtures/symlinked/src/real.zig new file mode 100644 index 00000000..ab446f23 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/symlinked/src/real.zig @@ -0,0 +1 @@ +pub const value: u32 = 3000; diff --git a/zig/tests/integration_tests/packages/fixtures/symlinked/src/symlinked.zig b/zig/tests/integration_tests/packages/fixtures/symlinked/src/symlinked.zig new file mode 100644 index 00000000..62f73c84 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/symlinked/src/symlinked.zig @@ -0,0 +1,3 @@ +const aliased = @import("aliased.zig"); + +pub const value: u32 = aliased.value; diff --git a/zig/tests/integration_tests/packages/fixtures/syslibdep/build.zig b/zig/tests/integration_tests/packages/fixtures/syslibdep/build.zig new file mode 100644 index 00000000..adcc35df --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/syslibdep/build.zig @@ -0,0 +1,9 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const m = b.addModule("syslibdep", .{ + .root_source_file = b.path("src/syslibdep.zig"), + .target = b.standardTargetOptions(.{}), + }); + m.linkSystemLibrary("mymath", .{}); +} diff --git a/zig/tests/integration_tests/packages/fixtures/syslibdep/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/syslibdep/build.zig.zon new file mode 100644 index 00000000..ddf19298 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/syslibdep/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .syslibdep, + .version = "0.0.0", + .fingerprint = 0x3cd2c304bd1152dd, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/syslibdep/src/syslibdep.zig b/zig/tests/integration_tests/packages/fixtures/syslibdep/src/syslibdep.zig new file mode 100644 index 00000000..ac6fbfd9 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/syslibdep/src/syslibdep.zig @@ -0,0 +1,5 @@ +extern fn my_compute(c_int) c_int; + +pub fn compute(x: c_int) c_int { + return my_compute(x); +} diff --git a/zig/tests/integration_tests/packages/fixtures/top/build.zig b/zig/tests/integration_tests/packages/fixtures/top/build.zig new file mode 100644 index 00000000..d5f74f76 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/top/build.zig @@ -0,0 +1,9 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const left = b.dependency("left", .{}); + const right = b.dependency("right", .{}); + const mod = b.addModule("top", .{ .root_source_file = b.path("src/top.zig") }); + mod.addImport("left", left.module("left")); + mod.addImport("right", right.module("right")); +} diff --git a/zig/tests/integration_tests/packages/fixtures/top/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/top/build.zig.zon new file mode 100644 index 00000000..041fbe77 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/top/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .top, + .version = "0.0.0", + .fingerprint = 0x1ed91fcabefd4e03, + .dependencies = .{ + .left = .{ .url = "__LEFT_URL__", .hash = "__LEFT_HASH__" }, + .right = .{ .url = "__RIGHT_URL__", .hash = "__RIGHT_HASH__" }, + }, + .paths = .{ "build.zig", "build.zig.zon", "src" }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/top/src/top.zig b/zig/tests/integration_tests/packages/fixtures/top/src/top.zig new file mode 100644 index 00000000..5635996a --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/top/src/top.zig @@ -0,0 +1 @@ +const left = @import("left"); const right = @import("right"); pub const value: u32 = left.value + right.value; diff --git a/zig/tests/integration_tests/packages/fixtures/usec/build.zig b/zig/tests/integration_tests/packages/fixtures/usec/build.zig new file mode 100644 index 00000000..222760c7 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/usec/build.zig @@ -0,0 +1,6 @@ +pub fn build(b: *@import("std").Build) void { + _ = b.addModule("usec", .{ + .root_source_file = b.path("src/usec.zig"), + .link_libc = true, + }); +} diff --git a/zig/tests/integration_tests/packages/fixtures/usec/build.zig.zon b/zig/tests/integration_tests/packages/fixtures/usec/build.zig.zon new file mode 100644 index 00000000..57b5d158 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/usec/build.zig.zon @@ -0,0 +1,10 @@ +.{ + .name = .usec, + .version = "0.0.0", + .fingerprint = 0xe723f6bb3527e304, + .paths = .{ + "build.zig", + "build.zig.zon", + "src", + }, +} diff --git a/zig/tests/integration_tests/packages/fixtures/usec/src/usec.zig b/zig/tests/integration_tests/packages/fixtures/usec/src/usec.zig new file mode 100644 index 00000000..c376dc04 --- /dev/null +++ b/zig/tests/integration_tests/packages/fixtures/usec/src/usec.zig @@ -0,0 +1,6 @@ +// `getpid` is provided by libc. +extern fn getpid() c_int; + +pub fn pid() c_int { + return getpid(); +} diff --git a/zig/tests/integration_tests/packages/main.zig b/zig/tests/integration_tests/packages/main.zig new file mode 100644 index 00000000..decb612b --- /dev/null +++ b/zig/tests/integration_tests/packages/main.zig @@ -0,0 +1,39 @@ +const leaf = @import("leaf"); +const host = @import("host"); +const top = @import("top"); +const child = @import("child"); +const multi = @import("multi"); +const widget = @import("widget"); +const greeter = @import("greeter"); +const pruned = @import("pruned"); +const lib = @import("lib"); +const lazyhost = @import("lazyhost"); +const symlinked = @import("symlinked"); +const genopts = @import("genopts"); +const usec = @import("usec"); +const cdep = @import("cdep"); +const syslibdep = @import("syslibdep"); +const optdep = @import("optdep"); +const cppdep = @import("cppdep"); +const cfgdep = @import("cfgdep"); + +pub fn main() void { + _ = leaf.value; + _ = host.value; + _ = top.value; + _ = child.value; + _ = multi.value; + _ = widget.value; + _ = greeter.value; + _ = pruned.value; + _ = lib.v1; + _ = lazyhost.value; + _ = symlinked.value; + _ = genopts.value; + _ = usec.pid(); + _ = cdep.value(); + _ = syslibdep.compute(21); + _ = optdep.compute(1); + _ = cppdep.value(); + _ = cfgdep.value(); +} diff --git a/zig/tests/integration_tests/packages/mymath.c b/zig/tests/integration_tests/packages/mymath.c new file mode 100644 index 00000000..24022bca --- /dev/null +++ b/zig/tests/integration_tests/packages/mymath.c @@ -0,0 +1,3 @@ +int my_compute(int x) { + return x * 2; +} diff --git a/zig/tests/integration_tests/packages/optmath.c b/zig/tests/integration_tests/packages/optmath.c new file mode 100644 index 00000000..f42c677c --- /dev/null +++ b/zig/tests/integration_tests/packages/optmath.c @@ -0,0 +1,3 @@ +int opt_compute(int x) { + return x + 100; +} diff --git a/zig/tests/integration_tests/packages/path_deps/greeter/BUILD.bazel b/zig/tests/integration_tests/packages/path_deps/greeter/BUILD.bazel new file mode 100644 index 00000000..d7e0419f --- /dev/null +++ b/zig/tests/integration_tests/packages/path_deps/greeter/BUILD.bazel @@ -0,0 +1,10 @@ +load("@rules_zig//zig:defs.bzl", "zig_library") +load("@zig_deps//:defs.bzl", "zig_deps") + +zig_library( + name = "greeter", + import_name = "greeter", + main = "greeter.zig", + visibility = ["//visibility:public"], + deps = zig_deps(), +) diff --git a/zig/tests/integration_tests/packages/path_deps/greeter/build.zig.zon b/zig/tests/integration_tests/packages/path_deps/greeter/build.zig.zon new file mode 100644 index 00000000..90a62795 --- /dev/null +++ b/zig/tests/integration_tests/packages/path_deps/greeter/build.zig.zon @@ -0,0 +1,11 @@ +.{ + .name = .greeter, + .version = "0.0.0", + .dependencies = .{ + .message = .{ .path = "../message" }, + }, + .paths = .{ + "build.zig.zon", + "greeter.zig", + }, +} diff --git a/zig/tests/integration_tests/packages/path_deps/greeter/greeter.zig b/zig/tests/integration_tests/packages/path_deps/greeter/greeter.zig new file mode 100644 index 00000000..26950a52 --- /dev/null +++ b/zig/tests/integration_tests/packages/path_deps/greeter/greeter.zig @@ -0,0 +1,3 @@ +const message = @import("message"); + +pub const value = message.value; diff --git a/zig/tests/integration_tests/packages/path_deps/message/BUILD.bazel b/zig/tests/integration_tests/packages/path_deps/message/BUILD.bazel new file mode 100644 index 00000000..1d3e50e7 --- /dev/null +++ b/zig/tests/integration_tests/packages/path_deps/message/BUILD.bazel @@ -0,0 +1,8 @@ +load("@rules_zig//zig:defs.bzl", "zig_library") + +zig_library( + name = "message", + import_name = "message", + main = "message.zig", + visibility = ["//visibility:public"], +) diff --git a/zig/tests/integration_tests/packages/path_deps/message/build.zig.zon b/zig/tests/integration_tests/packages/path_deps/message/build.zig.zon new file mode 100644 index 00000000..e0996a87 --- /dev/null +++ b/zig/tests/integration_tests/packages/path_deps/message/build.zig.zon @@ -0,0 +1,8 @@ +.{ + .name = .message, + .version = "0.0.0", + .paths = .{ + "build.zig.zon", + "message.zig", + }, +} diff --git a/zig/tests/integration_tests/packages/path_deps/message/message.zig b/zig/tests/integration_tests/packages/path_deps/message/message.zig new file mode 100644 index 00000000..75c2e74c --- /dev/null +++ b/zig/tests/integration_tests/packages/path_deps/message/message.zig @@ -0,0 +1 @@ +pub const value: u32 = 1; diff --git a/zig/tests/integration_tests/packages/relonly.c b/zig/tests/integration_tests/packages/relonly.c new file mode 100644 index 00000000..0a3733bf --- /dev/null +++ b/zig/tests/integration_tests/packages/relonly.c @@ -0,0 +1,3 @@ +int rel_compute(void) { + return 2; +} diff --git a/zig/tests/integration_tests/packages/tools/BUILD.bazel b/zig/tests/integration_tests/packages/tools/BUILD.bazel new file mode 100644 index 00000000..9679bba1 --- /dev/null +++ b/zig/tests/integration_tests/packages/tools/BUILD.bazel @@ -0,0 +1,7 @@ +load("@rules_zig//zig:defs.bzl", "zig_binary") + +zig_binary( + name = "pack", + main = "pack.zig", + visibility = ["//visibility:public"], +) diff --git a/zig/tests/integration_tests/packages/tools/pack.zig b/zig/tests/integration_tests/packages/tools/pack.zig new file mode 100644 index 00000000..c431ae7d --- /dev/null +++ b/zig/tests/integration_tests/packages/tools/pack.zig @@ -0,0 +1,203 @@ +//! Pack a Zig package directory into a tarball and print its package hash. +//! +//! Usage: pack +//! +//! Writes `` (the package rooted under its name) and prints the +//! package hash on stdout, so test fixtures can be referenced by `.url` + +//! `.hash` without invoking `zig fetch`. +//! +//! The hash replicates the Zig package manager (`src/Package/Fetch.zig` +//! `computeHash` + `Package.Hash.init`): per file `sha256(rel_path ++ {0,0} ++ +//! content)`, combined by `sha256` over the files sorted by path, formatted as +//! `name-version-base64url(LE id ++ LE size ++ digest[0..25])`. Pinned to the +//! toolchain Zig version. + +const std = @import("std"); +const Io = std.Io; +const Allocator = std.mem.Allocator; +const Zoir = std.zig.Zoir; +const Sha256 = std.crypto.hash.sha2.Sha256; + +const Manifest = struct { + name: []const u8, + version: []const u8, + id: u32, + paths: []const []const u8, +}; + +const Entry = struct { + path: []const u8, + digest: [Sha256.digest_length]u8 = undefined, + size: u64 = 0, + + fn lessThan(_: void, a: Entry, b: Entry) bool { + return std.mem.lessThan(u8, a.path, b.path); + } +}; + +pub fn main(init: std.process.Init) !void { + const arena = init.arena.allocator(); + const io = init.io; + + const args = try init.minimal.args.toSlice(arena); + if (args.len != 3) fatal("usage: pack ", .{}); + + var pkg_dir = try Io.Dir.cwd().openDir(io, args[1], .{ .iterate = true }); + defer pkg_dir.close(io); + + const manifest = try parseManifest(arena, io, pkg_dir); + + var entries: std.ArrayList(Entry) = .empty; + var walker = try pkg_dir.walk(arena); + while (try walker.next(io)) |entry| { + // Directories are not hashed; symlinks (e.g. Bazel-staged fixture files) + // are dereferenced and treated as regular files via their content. + if (entry.kind == .directory) continue; + const path = try arena.dupe(u8, entry.path); + if (!includePath(manifest.paths, path)) continue; + try entries.append(arena, .{ .path = path }); + } + + for (entries.items) |*entry| { + const content = try pkg_dir.readFileAlloc(io, entry.path, arena, .unlimited); + var hasher: Sha256 = .init(.{}); + hasher.update(entry.path); + // Hard-coded non-executable bit, matching Zig's `computeHash`. + hasher.update(&.{ 0, 0 }); + hasher.update(content); + hasher.final(&entry.digest); + entry.size = content.len; + } + + std.mem.sortUnstable(Entry, entries.items, {}, Entry.lessThan); + + var combined: Sha256 = .init(.{}); + var total: u64 = 0; + for (entries.items) |entry| { + combined.update(&entry.digest); + total += entry.size; + } + var digest: [Sha256.digest_length]u8 = undefined; + combined.final(&digest); + + try writeTar(io, pkg_dir, manifest.name, entries.items, args[2]); + + const size: u32 = std.math.cast(u32, total) orelse std.math.maxInt(u32); + const hash = formatHash(arena, manifest, digest, size); + + var stdout_buffer: [256]u8 = undefined; + var stdout = std.Io.File.stdout().writerStreaming(io, &stdout_buffer); + try stdout.interface.writeAll(hash); + try stdout.interface.writeByte('\n'); + try stdout.interface.flush(); +} + +fn formatHash(arena: Allocator, manifest: Manifest, digest: [Sha256.digest_length]u8, size: u32) []const u8 { + var hashplus: [33]u8 = undefined; + std.mem.writeInt(u32, hashplus[0..4], manifest.id, .little); + std.mem.writeInt(u32, hashplus[4..8], size, .little); + hashplus[8..33].* = digest[0..25].*; + + var encoded: [44]u8 = undefined; + _ = std.base64.url_safe_no_pad.Encoder.encode(&encoded, &hashplus); + + return std.fmt.allocPrint(arena, "{s}-{s}-{s}", .{ manifest.name, manifest.version, encoded }) catch fatal("out of memory", .{}); +} + +fn includePath(paths: []const []const u8, sub_path: []const u8) bool { + for (paths) |path| { + if (path.len == 0 or std.mem.eql(u8, path, ".")) return true; + if (std.mem.eql(u8, path, sub_path)) return true; + } + var dirname = sub_path; + while (std.fs.path.dirname(dirname)) |parent| : (dirname = parent) { + for (paths) |path| { + if (std.mem.eql(u8, path, parent)) return true; + } + } + return false; +} + +fn writeTar(io: Io, pkg_dir: Io.Dir, root: []const u8, entries: []const Entry, out_path: []const u8) !void { + var out_file = try Io.Dir.cwd().createFile(io, out_path, .{}); + defer out_file.close(io); + + var out_buffer: [64 * 1024]u8 = undefined; + var out_writer = out_file.writer(io, &out_buffer); + + var tar: std.tar.Writer = .{ .underlying_writer = &out_writer.interface }; + try tar.setRoot(root); + for (entries) |entry| { + const content = try pkg_dir.readFileAlloc(io, entry.path, std.heap.page_allocator, .unlimited); + defer std.heap.page_allocator.free(content); + try tar.writeFileBytes(entry.path, content, .{}); + } + try tar.finishPedantically(); + try out_writer.interface.flush(); +} + +fn parseManifest(arena: Allocator, io: Io, pkg_dir: Io.Dir) !Manifest { + const source = try pkg_dir.readFileAllocOptions(io, "build.zig.zon", arena, .unlimited, .of(u8), 0); + + const ast = try std.zig.Ast.parse(arena, source, .zon); + const zoir = try std.zig.ZonGen.generate(arena, ast, .{}); + if (zoir.compile_errors.len != 0) fatal("invalid 'build.zig.zon'", .{}); + + var name: []const u8 = ""; + var version: []const u8 = ""; + var id: u32 = 0; + var paths: std.ArrayList([]const u8) = .empty; + + switch (Zoir.Node.Index.root.get(zoir)) { + .struct_literal => |fields| for (fields.names, 0..) |field_name, i| { + const field = field_name.get(zoir); + const value = fields.vals.at(@intCast(i)); + if (std.mem.eql(u8, field, "name")) { + name = enumLiteral(zoir, value); + } else if (std.mem.eql(u8, field, "version")) { + version = stringOf(zoir, value); + } else if (std.mem.eql(u8, field, "fingerprint")) { + id = @truncate(intOf(zoir, value)); + } else if (std.mem.eql(u8, field, "paths")) { + switch (value.get(zoir)) { + .array_literal => |elements| for (0..elements.len) |j| { + try paths.append(arena, stringOf(zoir, elements.at(@intCast(j)))); + }, + else => {}, + } + } + }, + else => fatal("'build.zig.zon' does not contain a struct", .{}), + } + + return .{ .name = name, .version = version, .id = id, .paths = paths.items }; +} + +fn enumLiteral(zoir: Zoir, index: Zoir.Node.Index) []const u8 { + return switch (index.get(zoir)) { + .enum_literal => |literal| std.mem.sliceTo(zoir.string_bytes[@intFromEnum(literal)..], 0), + else => "", + }; +} + +fn stringOf(zoir: Zoir, index: Zoir.Node.Index) []const u8 { + return switch (index.get(zoir)) { + .string_literal => |string| string, + else => "", + }; +} + +fn intOf(zoir: Zoir, index: Zoir.Node.Index) u64 { + return switch (index.get(zoir)) { + .int_literal => |int| switch (int) { + .small => |small| @bitCast(@as(i64, small)), + .big => |big| big.toInt(u64) catch 0, + }, + else => 0, + }; +} + +fn fatal(comptime format: []const u8, args: anytype) noreturn { + std.debug.print("pack: " ++ format ++ "\n", args); + std.process.exit(1); +} diff --git a/zig/tests/integration_tests/packages_tests_runner.zig b/zig/tests/integration_tests/packages_tests_runner.zig new file mode 100644 index 00000000..2a586251 --- /dev/null +++ b/zig/tests/integration_tests/packages_tests_runner.zig @@ -0,0 +1,188 @@ +const std = @import("std"); +const integration_testing = @import("integration_testing"); +const BitContext = integration_testing.BitContext; + +// A manifest inside a fixture whose URL-dependency placeholders to fill with +// already-packed fixtures' url+hash. +const Patch = struct { + manifest: []const u8 = "build.zig.zon", + deps: []const []const u8, +}; + +const Package = struct { + name: []const u8, + patches: []const Patch = &.{}, + symlink: ?[2][]const u8 = null, +}; + +// Packed in topological order (dependencies first). +const packages = [_]Package{ + .{ .name = "leaf" }, + .{ .name = "host", .patches = &.{.{ .manifest = "libs/foo/build.zig.zon", .deps = &.{"leaf"} }} }, + .{ .name = "base" }, + .{ .name = "bottom", .patches = &.{.{ .deps = &.{"base"} }} }, + .{ .name = "left", .patches = &.{.{ .deps = &.{"bottom"} }} }, + .{ .name = "right", .patches = &.{.{ .deps = &.{"bottom"} }} }, + .{ .name = "top", .patches = &.{.{ .deps = &.{ "left", "right" } }} }, + .{ .name = "multi" }, + .{ .name = "pruned" }, + .{ .name = "libv1" }, + .{ .name = "libv2" }, + .{ .name = "lazyleaf" }, + .{ .name = "lazyhost", .patches = &.{.{ .deps = &.{"lazyleaf"} }} }, + .{ .name = "symlinked", .symlink = .{ "real.zig", "src/aliased.zig" } }, + .{ .name = "srconly" }, + .{ .name = "genopts" }, + .{ .name = "usec" }, + .{ .name = "cdep" }, + .{ .name = "syslibdep" }, + .{ .name = "optdep" }, + .{ .name = "cppdep" }, + .{ .name = "cfgdep" }, +}; + +const Consumer = struct { + manifest: []const u8, + deps: []const []const u8, +}; + +// Manifests that resolve dependencies via `zig_packages.from_file`. +const consumers = [_]Consumer{ + .{ .manifest = "build.zig.zon", .deps = &.{ "leaf", "host", "top", "multi", "pruned", "libv1", "lazyhost", "symlinked", "genopts", "srconly", "usec", "cdep", "syslibdep", "optdep", "cppdep", "cfgdep" } }, + .{ .manifest = "child/build.zig.zon", .deps = &.{ "leaf", "libv2" } }, +}; + +test "Zig packages are imported from file:// tarballs" { + const ctx = try BitContext.init(); + defer ctx.deinit(); + + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var urls = std.StringHashMap([]const u8).init(allocator); + var hashes = std.StringHashMap([]const u8).init(allocator); + + for (packages) |pkg| { + for (pkg.patches) |patch| { + const manifest = try std.fmt.allocPrint(allocator, "fixtures/{s}/{s}", .{ pkg.name, patch.manifest }); + try ctx.patchWorkspaceFile(manifest, try depReplacements(allocator, patch.deps, &urls, &hashes)); + } + + if (pkg.symlink) |link| { + const link_path = try std.fmt.allocPrint(allocator, "fixtures/{s}/{s}", .{ pkg.name, link[1] }); + try ctx.symLinkWorkspaceFile(link[0], link_path); + } + + const dir = try std.fmt.allocPrint(allocator, "{s}/fixtures/{s}", .{ ctx.workspace_path, pkg.name }); + const tarball = try std.fmt.allocPrint(allocator, "{s}/{s}.tar", .{ ctx.workspace_path, pkg.name }); + const pack = try ctx.exec_bazel(.{ + .argv = &[_][]const u8{ "run", "//tools:pack", "--", dir, tarball }, + }); + defer pack.deinit(); + try std.testing.expect(pack.success); + + try hashes.put(pkg.name, try allocator.dupe(u8, std.mem.trim(u8, pack.stdout, " \t\r\n"))); + try urls.put(pkg.name, try std.fmt.allocPrint(allocator, "file://{s}", .{tarball})); + } + + for (consumers) |consumer| { + try ctx.patchWorkspaceFile(consumer.manifest, try depReplacements(allocator, consumer.deps, &urls, &hashes)); + } + + // The importer fetches, configures, and exposes each package (including the + // sub-tree path dependency of `host` and the transitive chain under `top`) + // as a module that the binary imports. + const result = try ctx.exec_bazel(.{ + .argv = &[_][]const u8{ "build", "//:binary" }, + }); + defer result.deinit(); + try std.testing.expect(result.success); + + // Test that the importer correctly selects system libraries per-configuration. + const opt_result = try ctx.exec_bazel(.{ + .argv = &[_][]const u8{ "build", "//:binary", "-c", "opt" }, + }); + defer opt_result.deinit(); + try std.testing.expect(opt_result.success); +} + +// Runs after the positive test above, tarballs are packed and consumer +// manifests are patched. Breaks one thing at a time, asserts the build fails as +// expected, and restores the original. +test "the importer rejects invalid package configurations" { + const ctx = try BitContext.init(); + defer ctx.deinit(); + + // A declared hash that does not match the fetched package. + try ctx.patchWorkspaceFile("build.zig.zon", &.{.{ "leaf-0.0.0-", "leaf-0.0.0-x" }}); + try expectBuildFailure(ctx, "hash mismatch"); + try ctx.patchWorkspaceFile("build.zig.zon", &.{.{ "leaf-0.0.0-x", "leaf-0.0.0-" }}); + + // A path dependency whose `build.zig.zon` is not provided via `from_file`. + const greeter_manifest = "path_deps/greeter/build.zig.zon"; + try ctx.patchWorkspaceFile(greeter_manifest, &.{.{ "../message", "../../fixtures/leaf" }}); + try expectBuildFailure(ctx, "has no provided manifest"); + try ctx.patchWorkspaceFile(greeter_manifest, &.{.{ "../../fixtures/leaf", "../message" }}); + + // `zig_dep` referencing a dependency the manifest does not declare. + const greeter_build = "path_deps/greeter/BUILD.bazel"; + try ctx.patchWorkspaceFile(greeter_build, &.{ + .{ "\"zig_deps\")", "\"zig_dep\", \"zig_deps\")" }, + .{ "deps = zig_deps()", "deps = [zig_dep(\"nonexistent\")]" }, + }); + try expectBuildFailure(ctx, "declares no dependency"); + try ctx.patchWorkspaceFile(greeter_build, &.{ + .{ "\"zig_dep\", \"zig_deps\")", "\"zig_deps\")" }, + .{ "deps = [zig_dep(\"nonexistent\")]", "deps = zig_deps()" }, + }); + + try ctx.patchWorkspaceFile("build.zig.zon", &.{.{ "// .srconly", ".srconly" }}); + try expectBuildFailure(ctx, "source-only"); + try ctx.patchWorkspaceFile("build.zig.zon", &.{.{ ".srconly", "// .srconly" }}); + + // A required system library with no matching annotation fails. + try ctx.patchWorkspaceFile("MODULE.bazel", &.{.{ "name = \"mymath\"", "name = \"mymath-unprovided\"" }}); + try expectBuildFailure(ctx, "system library"); + try ctx.patchWorkspaceFile("MODULE.bazel", &.{.{ "name = \"mymath-unprovided\"", "name = \"mymath\"" }}); + + try ctx.patchWorkspaceFile("MODULE.bazel", &.{.{ "system_integration(name = \"optmath\")", "system_integration(name = \"optmath-off\")" }}); + try expectBuildFailure(ctx, "opt_compute"); + try ctx.patchWorkspaceFile("MODULE.bazel", &.{.{ "system_integration(name = \"optmath-off\")", "system_integration(name = \"optmath\")" }}); +} + +fn expectBuildFailure(ctx: BitContext, expected: []const u8) !void { + const result = try ctx.exec_bazel(.{ + .argv = &[_][]const u8{ "build", "//:binary" }, + .print_on_error = false, + }); + defer result.deinit(); + if (result.success) { + std.debug.print("expected build to FAIL (mentioning '{s}') but it succeeded\n", .{expected}); + return error.BuildUnexpectedlySucceeded; + } + if (std.mem.indexOf(u8, result.stderr, expected) == null) { + std.debug.print("expected build failure mentioning '{s}', stderr:\n{s}\n", .{ expected, result.stderr }); + return error.UnexpectedFailureMessage; + } +} + +fn depReplacements( + allocator: std.mem.Allocator, + deps: []const []const u8, + urls: *std.StringHashMap([]const u8), + hashes: *std.StringHashMap([]const u8), +) ![]const [2][]const u8 { + const replacements = try allocator.alloc([2][]const u8, deps.len * 2); + for (deps, 0..) |dep, i| { + replacements[i * 2] = .{ try placeholder(allocator, dep, "URL"), urls.get(dep).? }; + replacements[i * 2 + 1] = .{ try placeholder(allocator, dep, "HASH"), hashes.get(dep).? }; + } + return replacements; +} + +fn placeholder(allocator: std.mem.Allocator, name: []const u8, kind: []const u8) ![]const u8 { + const upper = try allocator.alloc(u8, name.len); + _ = std.ascii.upperString(upper, name); + return std.fmt.allocPrint(allocator, "__{s}_{s}__", .{ upper, kind }); +} diff --git a/zig/tests/zig_deps_hub_test.bzl b/zig/tests/zig_deps_hub_test.bzl new file mode 100644 index 00000000..a2a27177 --- /dev/null +++ b/zig/tests/zig_deps_hub_test.bzl @@ -0,0 +1,32 @@ +"""Unit tests for the `zig_deps` hub repository rule helpers.""" + +load("@bazel_skylib//lib:partial.bzl", "partial") +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load("//zig/private/repo:zig_deps_hub.bzl", "render_config_groups") + +def _render_config_groups_test_impl(ctx): + env = unittest.begin(ctx) + + asserts.equals( + env, + "", + render_config_groups([]), + "no groups renders nothing", + ) + + out = render_config_groups([{"name": "rel", "select_on": ["@platforms//os:linux", "//zig/config/mode:release_fast"]}]) + asserts.true(env, "load(\"@bazel_skylib//lib:selects.bzl\", \"selects\")" in out, "loads selects") + asserts.true(env, "name = \"cfg_rel\"" in out, "names the group cfg_") + asserts.true(env, "match_all = [" in out, "matches all conditions") + asserts.true(env, "\"@platforms//os:linux\"" in out, "includes the os condition") + asserts.true(env, "\"//zig/config/mode:release_fast\"" in out, "includes the mode condition") + + return unittest.end(env) + +_render_config_groups_test = unittest.make(_render_config_groups_test_impl) + +def zig_deps_hub_test_suite(name): + unittest.suite( + name, + partial.make(_render_config_groups_test, size = "small"), + ) diff --git a/zig/tests/zig_package_test.bzl b/zig/tests/zig_package_test.bzl new file mode 100644 index 00000000..06678c56 --- /dev/null +++ b/zig/tests/zig_package_test.bzl @@ -0,0 +1,204 @@ +"""Unit tests for the `zig_package` repository rule helpers.""" + +load("@bazel_skylib//lib:partial.bzl", "partial") +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load("//zig/private/repo:zig_package.bzl", "merge", "parse_cells", "render", "render_attr") + +def _record(*, kind = "zig_library", name, fixed = {}, varying = {}): + return struct(kind = kind, name = name, fixed = fixed, varying = varying) + +_FALLBACK = struct(name = "dbg", config_setting = "") +_REL = struct(name = "rel", config_setting = "rel") + +def _parse_cells_test_impl(ctx): + env = unittest.begin(ctx) + + asserts.equals( + env, + (None, [struct(name = "", zig_options = [], config_setting = "")]), + parse_cells(""), + "no matrix yields a single host fallback cell", + ) + + dbg = {"name": "dbg", "zig_options": ["-Doptimize=Debug"], "config_setting": ""} + rel = {"name": "rel", "zig_options": ["-Doptimize=ReleaseFast"], "config_setting": "rel"} + asserts.equals( + env, + (None, [ + struct(name = "dbg", zig_options = ["-Doptimize=Debug"], config_setting = ""), + struct(name = "rel", zig_options = ["-Doptimize=ReleaseFast"], config_setting = "rel"), + ]), + parse_cells(json.encode([rel, dbg])), + "the fallback cell is ordered first", + ) + + asserts.equals( + env, + ("expected exactly one fallback cell, found 0", None), + parse_cells(json.encode([rel])), + "a matrix must have a fallback", + ) + + asserts.equals( + env, + ("expected exactly one fallback cell, found 2", None), + parse_cells(json.encode([dbg, dict(rel, config_setting = "")])), + "a matrix must have only one fallback", + ) + + return unittest.end(env) + +_parse_cells_test = unittest.make(_parse_cells_test_impl) + +def _merge_test_impl(ctx): + env = unittest.begin(ctx) + + asserts.equals( + env, + (None, [_record(name = "foo", fixed = {"main": "foo.zig"}, varying = {"deps": ("const", ["a"])})]), + merge("pkg", {"dbg": [_record(name = "foo", fixed = {"main": "foo.zig"}, varying = {"deps": ["a"]})]}, [_FALLBACK]), + "a single cell makes every varying attr constant", + ) + + asserts.equals( + env, + (None, [_record( + name = "foo", + fixed = {"main": "foo.zig"}, + varying = {"deps": ("select", {"dbg": ["a"], "rel": ["b"]}), "import_names": ("const", {})}, + )]), + merge( + "pkg", + { + "dbg": [_record(name = "foo", fixed = {"main": "foo.zig"}, varying = {"deps": ["a"], "import_names": {}})], + "rel": [_record(name = "foo", fixed = {"main": "foo.zig"}, varying = {"deps": ["b"], "import_names": {}})], + }, + [_FALLBACK, _REL], + ), + "a differing attr becomes a select; an agreeing attr stays constant", + ) + + asserts.equals( + env, + ("package 'pkg' produces a different set of targets under config 'rel' than under 'dbg'", None), + merge( + "pkg", + {"dbg": [_record(name = "foo")], "rel": [_record(name = "foo"), _record(name = "bar")]}, + [_FALLBACK, _REL], + ), + "the set of targets must be constant across configs", + ) + + asserts.equals( + env, + ("package 'pkg' target 'foo' has kind 'cc_library' under config 'rel' but 'zig_library' under 'dbg'", None), + merge( + "pkg", + { + "dbg": [_record(kind = "zig_library", name = "foo")], + "rel": [_record(kind = "cc_library", name = "foo")], + }, + [_FALLBACK, _REL], + ), + "a target's kind must be constant across configs", + ) + + asserts.equals( + env, + ("package 'pkg' target 'foo' fixed attribute 'main' varies by configuration", None), + merge( + "pkg", + { + "dbg": [_record(name = "foo", fixed = {"main": "foo.zig"})], + "rel": [_record(name = "foo", fixed = {"main": "bar.zig"})], + }, + [_FALLBACK, _REL], + ), + "a fixed attribute must be constant across configs", + ) + + return unittest.end(env) + +_merge_test = unittest.make(_merge_test_impl) + +def _render_attr_test_impl(ctx): + env = unittest.begin(ctx) + + asserts.equals( + env, + '["a"]', + render_attr({}, ("const", ["a"]), [_FALLBACK]), + "a constant attribute renders without select", + ) + + asserts.equals( + env, + """\ +select({ + "@x//config:cfg_rel": ["b"], + "//conditions:default": ["a"], +})""", + render_attr({"rel": "@x//config:cfg_rel"}, ("select", {"dbg": ["a"], "rel": ["b"]}), [_FALLBACK, _REL]), + "a select renders non-fallback cells, then the fallback as //conditions:default", + ) + + return unittest.end(env) + +_render_attr_test = unittest.make(_render_attr_test_impl) + +def _render_test_impl(ctx): + env = unittest.begin(ctx) + + out = render( + {}, + [_record(name = "foo", fixed = {"main": "foo.zig"}, varying = {"deps": ("const", ["a"]), "import_names": ("const", {})})], + [_FALLBACK], + ) + asserts.true(env, 'load("@rules_zig//zig:defs.bzl", "zig_library")' in out, "loads zig_library") + asserts.true(env, 'name = "foo"' in out, "emits the target") + asserts.true(env, 'deps = ["a"]' in out, "inlines a constant attribute") + asserts.true(env, "@rules_cc" not in out, "omits the cc_library load with no C targets") + + out = render( + {}, + [ + _record( + kind = "zig_library_subtree", + name = "vendor/foo", + fixed = {"import_name": "foo", "main": "vendor/foo/foo.zig", "subpath": "vendor/foo"}, + varying = {"deps": ("const", [":bar"]), "import_names": ("const", {})}, + ), + _record( + kind = "cc_library", + name = "z.cinc.0", + fixed = {"header_globs": ["c/**/*.h"]}, + varying = {"srcs": ("const", ["c/z.c"]), "copts": ("const", ["-DZ"]), "includes": ("const", ["c"])}, + ), + _record( + kind = "cc_library_group", + name = "z.cinc", + varying = {"deps": ("const", [":z.cinc.0"])}, + ), + ], + [_FALLBACK], + ) + asserts.true(env, 'load("@rules_cc//cc:defs.bzl", "cc_library")' in out, "loads cc_library when C targets are present") + asserts.true(env, 'name = "vendor/foo"' in out, "emits the subtree library scoped by sub-path") + asserts.true(env, 'import_name = "foo"' in out, "the subtree library keeps the module's import name") + asserts.true(env, 'srcs = glob(["vendor/foo/**/*.zig"], exclude = ["vendor/foo/foo.zig"])' in out, "globs the subtree's sources") + asserts.true(env, 'srcs = ["c/z.c"]' in out, "emits the cc_library sources") + asserts.true(env, 'hdrs = glob(["c/**/*.h"], allow_empty = True)' in out, "globs the cc_library headers") + asserts.true(env, 'deps = [":z.cinc.0"]' in out, "the cc_library group depends on its source groups") + + return unittest.end(env) + +_render_test = unittest.make(_render_test_impl) + +def zig_package_test_suite(name): + unittest.suite( + name, + partial.make(_parse_cells_test, size = "small"), + partial.make(_merge_test, size = "small"), + partial.make(_render_attr_test, size = "small"), + partial.make(_render_test, size = "small"), + ) diff --git a/zig/tests/zig_packages_test.bzl b/zig/tests/zig_packages_test.bzl new file mode 100644 index 00000000..026eaad8 --- /dev/null +++ b/zig/tests/zig_packages_test.bzl @@ -0,0 +1,311 @@ +"""Unit tests for the `zig_packages` module extension helpers.""" + +load("@bazel_skylib//lib:partial.bzl", "partial") +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load( + "//zig/private/bzlmod:zig_packages.bzl", + "check_cells", + "collect_configs", + "package_cells", + "package_name_version", + "resolve_cell", +) + +def _config(*, name, optimize = "", select_on = [], zig_flags = []): + return struct(name = name, optimize = optimize, select_on = select_on, zig_flags = zig_flags) + +def _module(*, is_root, name = "m", config = [], configure = []): + return struct(is_root = is_root, name = name, tags = struct(config = config, configure = configure)) + +def _cell(*, name, select_on = [], zig_options = [], config_setting = ""): + return struct(name = name, select_on = select_on, zig_options = zig_options, config_setting = config_setting) + +def _resolve_cell_test_impl(ctx): + env = unittest.begin(ctx) + + asserts.equals( + env, + (None, struct( + name = "dbg", + select_on = ["@rules_zig//zig/config/mode:debug"], + zig_options = ["-Doptimize=Debug"], + )), + resolve_cell(_config(name = "dbg", optimize = "debug")), + "optimize expands to a mode condition and -Doptimize", + ) + + asserts.equals( + env, + (None, struct( + name = "x", + select_on = ["@rules_zig//zig/config/mode:release_fast", "//my:flag"], + zig_options = ["-Doptimize=ReleaseFast", "-DDEFINE=a=b"], + )), + resolve_cell(_config( + name = "x", + optimize = "release_fast", + select_on = ["//my:flag"], + zig_flags = ["DEFINE=a=b"], + )), + "select_on and zig_flags append; a flag value may contain '='", + ) + + asserts.equals( + env, + (None, struct(name = "bare", select_on = [], zig_options = [])), + resolve_cell(_config(name = "bare")), + "a cell may carry no conditions", + ) + + asserts.equals( + env, + ("config 'x' has unknown optimize mode 'fast'", None), + resolve_cell(_config(name = "x", optimize = "fast")), + "an unknown optimize mode is rejected", + ) + + asserts.equals( + env, + ("config 'x' flag 'BAD' is not NAME=VALUE", None), + resolve_cell(_config(name = "x", zig_flags = ["BAD"])), + "a zig_flags entry must be NAME=VALUE", + ) + + return unittest.end(env) + +_resolve_cell_test = unittest.make(_resolve_cell_test_impl) + +def _check_cells_test_impl(ctx): + env = unittest.begin(ctx) + + fallback = _cell(name = "dbg", select_on = ["//mode:debug"], config_setting = "") + rel = _cell(name = "rel", select_on = ["//mode:release"], zig_options = ["-Doptimize=ReleaseFast"], config_setting = "rel") + + asserts.equals( + env, + (None, [fallback, rel]), + check_cells("pkg", [fallback, rel]), + "a fallback and a distinct non-fallback cell pass through", + ) + + duplicate = _cell(name = "rel2", select_on = ["//mode:release"], zig_options = ["-Doptimize=ReleaseFast"], config_setting = "rel2") + asserts.equals( + env, + (None, [fallback, rel]), + check_cells("pkg", [fallback, rel, duplicate]), + "duplicate cells are merged", + ) + + conflicting = _cell(name = "rel3", select_on = ["//mode:release"], zig_options = ["-Doptimize=ReleaseSafe"], config_setting = "rel3") + asserts.equals( + env, + ("package 'pkg' configs 'rel' and 'rel3' share conditions but differ in build options", None), + check_cells("pkg", [fallback, rel, conflicting]), + "shared conditions with differing options conflict", + ) + + general = _cell(name = "g", select_on = ["//os:linux"], config_setting = "g") + specific = _cell(name = "s", select_on = ["//os:linux", "//mode:release"], config_setting = "s") + asserts.equals( + env, + ("package 'pkg' config 'g' conditions are a subset of 's'; they would match ambiguously", None), + check_cells("pkg", [fallback, general, specific]), + "a cell whose conditions are a subset of another's is rejected", + ) + + asserts.equals( + env, + ("package 'pkg' config 'e' has no select conditions", None), + check_cells("pkg", [fallback, _cell(name = "e", config_setting = "e")]), + "a non-fallback cell must have conditions", + ) + + return unittest.end(env) + +_check_cells_test = unittest.make(_check_cells_test_impl) + +def _package_name_version_test_impl(ctx): + env = unittest.begin(ctx) + + asserts.equals( + env, + ("cfgdep", "0.0.0"), + package_name_version("cfgdep-0.0.0-AAAABBBBCCCC"), + "name and version split off the digest", + ) + + asserts.equals( + env, + ("ezi_gex", "0.5.0-dev"), + package_name_version("ezi_gex-0.5.0-dev-fTQAPBMbFwDJ"), + "a version may itself contain '-'", + ) + + return unittest.end(env) + +_package_name_version_test = unittest.make(_package_name_version_test_impl) + +_CELLS_BY_NAME = { + "dbg": struct(name = "dbg", select_on = ["//mode:debug"], zig_options = ["-Doptimize=Debug"]), + "rel": struct(name = "rel", select_on = ["//mode:release"], zig_options = ["-Doptimize=ReleaseFast"]), +} + +def _configure(*, configs, fallback, package = "", version = ""): + return struct(configs = configs, fallback = fallback, package = package, version = version) + +def _configured(base, config_setting): + return struct(name = base.name, select_on = base.select_on, zig_options = base.zig_options, config_setting = config_setting) + +def _package_cells_test_impl(ctx): + env = unittest.begin(ctx) + + dbg = _configured(_CELLS_BY_NAME["dbg"], "") + rel = _configured(_CELLS_BY_NAME["rel"], "rel") + + asserts.equals( + env, + (None, None), + package_cells("pkg-1.0.0-xyz", _CELLS_BY_NAME, None, {}), + "no configure means no matrix", + ) + + asserts.equals( + env, + (None, [dbg, rel]), + package_cells("pkg-1.0.0-xyz", _CELLS_BY_NAME, _configure(configs = ["dbg", "rel"], fallback = "dbg"), {}), + "global configure applies", + ) + + asserts.equals( + env, + (None, [_configured(_CELLS_BY_NAME["rel"], "")]), + package_cells( + "pkg-1.0.0-xyz", + _CELLS_BY_NAME, + _configure(configs = ["dbg", "rel"], fallback = "dbg"), + {("pkg", "1.0.0"): _configure(configs = ["rel"], fallback = "rel")}, + ), + "a versioned per-package configure overrides the global one", + ) + + asserts.equals( + env, + (None, [dbg, rel]), + package_cells( + "pkg-1.0.0-xyz", + _CELLS_BY_NAME, + None, + {("pkg", ""): _configure(configs = ["dbg", "rel"], fallback = "dbg")}, + ), + "a version-less per-package configure matches any version", + ) + + asserts.equals( + env, + ("package 'pkg' configure fallback 'rel' is not among its configs", None), + package_cells("pkg-1.0.0-xyz", _CELLS_BY_NAME, _configure(configs = ["dbg"], fallback = "rel"), {}), + "the fallback must be one of the configs", + ) + + asserts.equals( + env, + ("package 'pkg' configure references unknown config 'nope'", None), + package_cells("pkg-1.0.0-xyz", _CELLS_BY_NAME, _configure(configs = ["nope"], fallback = "nope"), {}), + "an unknown config name is rejected", + ) + + asserts.equals( + env, + ("package 'pkg' configure lists config 'dbg' more than once", None), + package_cells("pkg-1.0.0-xyz", _CELLS_BY_NAME, _configure(configs = ["dbg", "rel", "dbg"], fallback = "dbg"), {}), + "a config listed more than once is rejected", + ) + + return unittest.end(env) + +_package_cells_test = unittest.make(_package_cells_test_impl) + +def _collect_configs_test_impl(ctx): + env = unittest.begin(ctx) + + asserts.equals( + env, + (None, struct(cells_by_name = {}, global_configure = None, per_package_configure = {}, ignored = [])), + collect_configs([]), + "no modules yields an empty matrix", + ) + + global_cfg = _configure(configs = ["dbg", "rel"], fallback = "dbg") + per_pkg = _configure(configs = ["rel"], fallback = "rel", package = "curl") + asserts.equals( + env, + (None, struct( + cells_by_name = { + "dbg": struct(name = "dbg", select_on = ["@rules_zig//zig/config/mode:debug"], zig_options = ["-Doptimize=Debug"]), + "rel": struct(name = "rel", select_on = ["@rules_zig//zig/config/mode:release_fast"], zig_options = ["-Doptimize=ReleaseFast"]), + }, + global_configure = global_cfg, + per_package_configure = {("curl", ""): per_pkg}, + ignored = [], + )), + collect_configs([_module( + is_root = True, + config = [_config(name = "dbg", optimize = "debug"), _config(name = "rel", optimize = "release_fast")], + configure = [global_cfg, per_pkg], + )]), + "root config and configure tags are collected", + ) + + asserts.equals( + env, + (None, struct(cells_by_name = {}, global_configure = None, per_package_configure = {}, ignored = ["dep"])), + collect_configs([_module(is_root = False, name = "dep", config = [_config(name = "x", optimize = "debug")])]), + "non-root tags are ignored and reported", + ) + + asserts.equals( + env, + ("conflicting config tags named 'x'", None), + collect_configs([_module( + is_root = True, + config = [_config(name = "x", optimize = "debug"), _config(name = "x", optimize = "release_fast")], + )]), + "config tags sharing a name with differing content conflict", + ) + + asserts.equals( + env, + ("at most one global configure tag is allowed", None), + collect_configs([_module( + is_root = True, + configure = [_configure(configs = ["dbg"], fallback = "dbg"), _configure(configs = ["rel"], fallback = "rel")], + )]), + "only one global configure is allowed", + ) + + asserts.equals( + env, + ("multiple configure tags for package 'curl'", None), + collect_configs([_module( + is_root = True, + configure = [ + _configure(configs = ["dbg"], fallback = "dbg", package = "curl"), + _configure(configs = ["rel"], fallback = "rel", package = "curl"), + ], + )]), + "a package may not have two configure tags", + ) + + return unittest.end(env) + +_collect_configs_test = unittest.make(_collect_configs_test_impl) + +def zig_packages_test_suite(name): + unittest.suite( + name, + partial.make(_resolve_cell_test, size = "small"), + partial.make(_check_cells_test, size = "small"), + partial.make(_package_name_version_test, size = "small"), + partial.make(_package_cells_test, size = "small"), + partial.make(_collect_configs_test, size = "small"), + )