From 3713635d76c32e3162ca5bc8eece8777716d7c47 Mon Sep 17 00:00:00 2001 From: Jake Hamilton Date: Fri, 14 Jun 2024 07:01:18 -0700 Subject: [PATCH] feat: simple builder --- tidepool/flake.lock | 16 +- tidepool/src/builders/basic.nix | 73 ++++++++ tidepool/src/builders/default.nix | 18 ++ tidepool/src/export.nix | 5 + tidepool/src/lib/packages.nix | 47 ++++- tidepool/src/lib/types.nix | 215 +++++++++++++++++++++-- tidepool/src/modules.nix | 1 + tidepool/src/packages/aux/foundation.nix | 13 +- 8 files changed, 354 insertions(+), 34 deletions(-) create mode 100644 tidepool/src/builders/basic.nix create mode 100644 tidepool/src/builders/default.nix diff --git a/tidepool/flake.lock b/tidepool/flake.lock index 138d5f2..f783170 100644 --- a/tidepool/flake.lock +++ b/tidepool/flake.lock @@ -8,10 +8,10 @@ }, "locked": { "dir": "foundation", - "dirtyRev": "cdc90a46565dadea3b3c8f673b8b55d4ff75ea92-dirty", - "dirtyShortRev": "cdc90a4-dirty", - "lastModified": 1718356339, - "narHash": "sha256-BSiyT1q5xSlO1DyNJlkRWI82WWxVtQ1Xy8zPOqdqUlU=", + "dirtyRev": "b3f9fe574e6ff545e4a6fe6ad5e90fc71baeece3-dirty", + "dirtyShortRev": "b3f9fe5-dirty", + "lastModified": 1718366115, + "narHash": "sha256-/ktNtlY/usvE1YaathJ0f6y60cv3gQjc32SMKApsp7Y=", "type": "git", "url": "file:../?dir=foundation" }, @@ -24,10 +24,10 @@ "lib": { "locked": { "dir": "lib", - "dirtyRev": "cdc90a46565dadea3b3c8f673b8b55d4ff75ea92-dirty", - "dirtyShortRev": "cdc90a4-dirty", - "lastModified": 1718356339, - "narHash": "sha256-BSiyT1q5xSlO1DyNJlkRWI82WWxVtQ1Xy8zPOqdqUlU=", + "dirtyRev": "b3f9fe574e6ff545e4a6fe6ad5e90fc71baeece3-dirty", + "dirtyShortRev": "b3f9fe5-dirty", + "lastModified": 1718366115, + "narHash": "sha256-/ktNtlY/usvE1YaathJ0f6y60cv3gQjc32SMKApsp7Y=", "type": "git", "url": "file:../?dir=lib" }, diff --git a/tidepool/src/builders/basic.nix b/tidepool/src/builders/basic.nix new file mode 100644 index 0000000..ae09616 --- /dev/null +++ b/tidepool/src/builders/basic.nix @@ -0,0 +1,73 @@ +{ + lib, + config, +}: let + cfg = config.builders.basic; + + lib' = config.lib; + + inherit (config) foundation; +in { + config.builders = { + basic = { + executable = "${foundation.stage2-bash}/bin/bash"; + + build = package: let + phases = package.phases; + sorted = lib.dag.sort.topographic phases; + + script = + lib.strings.concatMapSep "\n" ( + entry: + if builtins.isFunction entry.value + then entry.value package + else entry.value + ) + sorted.result; + + system = package.platform.build; + + deps = lib'.packages.dependencies.resolve package.deps system; + in + (builtins.trace script) + builtins.derivation (package.env + // { + inherit (package) name; + inherit script system; + + passAsFile = ["script"]; + + SHELL = cfg.executable; + + PATH = lib.paths.bin ( + (lib'.packages.dependencies.getPackages "build.host" deps) + ++ [ + foundation.stage2-bash + foundation.stage2-coreutils + ] + ); + + builder = cfg.executable; + + args = [ + "-e" + (builtins.toFile "bash-builder.sh" '' + export CONFIG_SHELL=$SHELL + + # Normalize the NIX_BUILD_CORES variable. The value might be 0, which + # means that we're supposed to try and auto-detect the number of + # available CPU cores at run-time. + NIX_BUILD_CORES="''${NIX_BUILD_CORES:-1}" + if ((NIX_BUILD_CORES <= 0)); then + guess=$(nproc 2>/dev/null || true) + ((NIX_BUILD_CORES = guess <= 0 ? 1 : guess)) + fi + export NIX_BUILD_CORES + + bash -eux $scriptPath + '') + ]; + }); + }; + }; +} diff --git a/tidepool/src/builders/default.nix b/tidepool/src/builders/default.nix new file mode 100644 index 0000000..7cb7012 --- /dev/null +++ b/tidepool/src/builders/default.nix @@ -0,0 +1,18 @@ +{ + lib, + config, +}: let + lib' = config.lib; +in { + includes = [ + ./basic.nix + ]; + + options = { + builders = lib.options.create { + description = "A set of builders that can be used to build packages."; + type = lib.types.attrs.of lib'.types.builder; + default.value = {}; + }; + }; +} diff --git a/tidepool/src/export.nix b/tidepool/src/export.nix index 560f6f9..1d879f8 100644 --- a/tidepool/src/export.nix +++ b/tidepool/src/export.nix @@ -4,11 +4,16 @@ lib, config, }: let + lib' = config.lib; in { config = { exports = { lib = config.lib; modules = import ./modules.nix; + + packages = { + example = lib'.packages.export "example.x"; + }; }; }; } diff --git a/tidepool/src/lib/packages.nix b/tidepool/src/lib/packages.nix index ab92d10..617a400 100644 --- a/tidepool/src/lib/packages.nix +++ b/tidepool/src/lib/packages.nix @@ -6,6 +6,47 @@ in { config = { lib.packages = { + dependencies = { + getPackages = path: dependencies: let + resolved = + if builtins.isList path + then path + else lib.strings.split "." path; + + attrs = lib.attrs.select resolved {} dependencies; + in + lib.attrs.mapToList (name: value: value.package) attrs; + + resolve = dependencies: system: + builtins.mapAttrs + # Note that this does not correspond to the "host" and "target" platforms, but rather + # where the code is used and where it is intended to end up. + ( + host: platforms: + builtins.mapAttrs + ( + target: deps: + builtins.mapAttrs + ( + name: dependency: + assert lib.errors.trace (dependency.namespace != null) "Namespace unknown for dependency `${name}`."; let + targeted = lib'.packages.export [dependency.namespace name] system; + in + if targeted == null + then builtins.throw "Dependency `${dependency.namespace}.${name}` does not support system `${system}`." + else targeted + ) + deps + ) + platforms + ) + dependencies; + }; + + ## Get a package from the package set by its path. The path can be either + ## a string or a list of strings that is used to access `config.packages.generic`. + ## + ## @type String | List String -> Package get = path: let resolved = if builtins.isList path @@ -14,12 +55,16 @@ in { package = lib.attrs.selectOrThrow resolved config.packages.generic; in - assert lib.errors.trace (builtins.length resolved > 1) "Packages must have a namespace specified."; + assert lib.errors.trace (builtins.length resolved > 1) "Cannot get package without a namespace."; package // { namespace = lib.modules.override 99 (builtins.head resolved); }; + ## Export a package by its path. Use this function with the `config.exports.packages.*` + ## options. + ## + ## @type String | List String -> String -> Package export = path: system: let resolved = if builtins.isList path diff --git a/tidepool/src/lib/types.nix b/tidepool/src/lib/types.nix index f068a29..c1c34c0 100644 --- a/tidepool/src/lib/types.nix +++ b/tidepool/src/lib/types.nix @@ -11,40 +11,40 @@ in { options = { name = { full = lib.options.create { - type = lib.types.string; description = "The full name of the license."; + type = lib.types.string; }; short = lib.options.create { - type = lib.types.string; description = "The short name of the license."; + type = lib.types.string; }; }; spdx = lib.options.create { + description = "The SPDX identifier for the license."; type = lib.types.nullish lib.types.string; default.value = null; - description = "The SPDX identifier for the license."; }; url = lib.options.create { - type = lib.types.nullish lib.types.string; description = "The URL for the license."; + type = lib.types.nullish lib.types.string; }; free = lib.options.create { + description = "Whether the license is free."; type = lib.types.bool; default.value = true; - description = "Whether the license is free."; }; redistributable = lib.options.create { + description = "Whether the license is allows redistribution."; type = lib.types.bool; default = { text = "config.free"; value = config.free; }; - description = "Whether the license is allows redistribution."; }; }; }); @@ -54,51 +54,51 @@ in { meta = lib.types.submodule { options = { description = lib.options.create { + description = "The description for the package."; type = lib.types.nullish lib.types.string; default.value = null; - description = "The description for the package."; }; homepage = lib.options.create { + description = "The homepage for the package."; type = lib.types.nullish lib.types.string; default.value = null; - description = "The homepage for the package."; }; license = lib.options.create { + description = "The license for the package."; type = lib.types.nullish config.lib.types.license; default.value = null; - description = "The license for the package."; }; free = lib.options.create { + description = "Whether the package is free."; type = lib.types.bool; default.value = true; - description = "Whether the package is free."; }; insecure = lib.options.create { + description = "Whether the package is insecure."; type = lib.types.bool; default.value = false; - description = "Whether the package is insecure."; }; broken = lib.options.create { + description = "Whether the package is broken."; type = lib.types.bool; default.value = false; - description = "Whether the package is broken."; }; main = lib.options.create { + description = "The main entry point for the package."; type = lib.types.nullish lib.types.string; default.value = null; - description = "The main entry point for the package."; }; platforms = lib.options.create { + description = "The platforms the package supports."; type = lib.types.list.of lib.types.string; default.value = []; - description = "The platforms the package supports."; }; }; }; @@ -108,6 +108,7 @@ in { options = { build = lib.options.create { + description = "The build function which takes a package definition and creates a derivation."; type = lib.types.function lib.types.derivation; }; }; @@ -120,6 +121,12 @@ in { ]); })); + versioned = lib.types.attrs.of (lib.types.submodule ({name}: { + freeform = lib.types.attrs.of (lib.types.submodule [ + lib'.types.package.versioned' + ]); + })); + # packages. targeted = lib.types.attrs.of (lib.types.submodule ({name}: let system = name; @@ -148,6 +155,7 @@ in { # packages.. options.cross = lib.attrs.generate lib'.systems.doubles.all (cross: lib.options.create { + description = "A cross-compiling package set."; default.value = {}; # packages... type = lib.types.attrs.of (lib.types.submodule ( @@ -177,7 +185,6 @@ in { dependencies = { generic = lib.types.attrs.of (lib.types.nullish lib'.types.package.generic); - targeted = lib.types.attrs.of (lib.types.nullish lib'.types.package.targeted); }; @@ -188,13 +195,17 @@ in { name ? assert false; null, config, }: { + freeform = lib.types.any; + options = { namespace = lib.options.create { + description = "The namespace for the package."; type = lib.types.nullish lib.types.string; default.value = null; }; pname = lib.options.create { + description = "The program name for the package"; type = lib.types.string; default = { text = " or \"unknown\""; @@ -206,6 +217,139 @@ in { }; version = lib.options.create { + description = "The version for the package."; + type = lib.types.nullish lib.types.version; + default.value = null; + }; + + meta = lib.options.create { + description = "Metadata for the package."; + type = lib'.types.meta; + default.value = { + name = config.pname; + }; + }; + + phases = lib.options.create { + description = "The phases for the package."; + type = lib.types.dag.of ( + lib.types.either + lib.types.string + (lib.types.function lib.types.string) + ); + default.value = {}; + }; + + env = lib.options.create { + description = "The environment for the package."; + type = lib.types.attrs.of lib.types.string; + default.value = {}; + }; + + builder = lib.options.create { + description = "The builder for the package."; + type = lib'.types.builder; + }; + + deps = { + build = { + only = lib.options.create { + description = "Dependencies which are only used in the build environment."; + type = lib'.types.dependencies.generic; + default.value = {}; + }; + + build = lib.options.create { + description = "Dependencies which are created in the build environment and are executed in the build environment."; + type = lib'.types.dependencies.generic; + default.value = {}; + }; + + host = lib.options.create { + description = "Dependencies which are created in the build environment and are executed in the host environment."; + type = lib'.types.dependencies.generic; + default.value = {}; + }; + + target = lib.options.create { + description = "Dependencies which are created in the build environment and are executed in the target environment."; + type = lib'.types.dependencies.generic; + default.value = {}; + }; + }; + + host = { + only = lib.options.create { + description = "Dependencies which are only used in the host environment."; + type = lib'.types.dependencies.generic; + default.value = {}; + }; + + host = lib.options.create { + description = "Dependencies which are executed in the host environment."; + type = lib'.types.dependencies.generic; + default.value = {}; + }; + + target = lib.options.create { + description = "Dependencies which are executed in the host environment which produces code for the target environment."; + type = lib'.types.dependencies.generic; + default.value = {}; + }; + }; + + target = { + only = lib.options.create { + description = "Dependencies which are only used in the target environment."; + type = lib'.types.dependencies.generic; + default.value = {}; + }; + + target = lib.options.create { + description = "Dependencies which are executed in the target environment."; + type = lib'.types.dependencies.generic; + default.value = {}; + }; + }; + }; + + versions = lib.options.create { + description = "Available package versions."; + type = lib.types.attrs.of lib'.types.package.versioned; + default.value = {}; + }; + }; + }; + + versioned = lib.types.submodule lib'.types.package.versioned'; + + versioned' = args @ { + name ? assert false; null, + config, + }: { + freeform = lib.types.any; + + options = { + namespace = lib.options.create { + description = "The namespace for the package."; + type = lib.types.nullish lib.types.string; + default.value = null; + }; + + pname = lib.options.create { + description = "The program name for the package"; + type = lib.types.string; + default = { + text = " or \"unknown\""; + value = + if args ? name + then args.name + else "unknown"; + }; + }; + + version = lib.options.create { + description = "The version for the package."; type = lib.types.nullish lib.types.version; default.value = null; }; @@ -218,6 +362,7 @@ in { }; phases = lib.options.create { + description = "The phases for the package."; type = lib.types.dag.of ( lib.types.either lib.types.string @@ -227,32 +372,38 @@ in { }; env = lib.options.create { + description = "The environment for the package."; type = lib.types.attrs.of lib.types.string; default.value = {}; }; builder = lib.options.create { + description = "The builder for the package."; type = lib'.types.builder; }; deps = { build = { only = lib.options.create { + description = "Dependencies which are only used in the build environment."; type = lib'.types.dependencies.generic; default.value = {}; }; build = lib.options.create { + description = "Dependencies which are created in the build environment and are executed in the build environment."; type = lib'.types.dependencies.generic; default.value = {}; }; host = lib.options.create { + description = "Dependencies which are created in the build environment and are executed in the host environment."; type = lib'.types.dependencies.generic; default.value = {}; }; target = lib.options.create { + description = "Dependencies which are created in the build environment and are executed in the target environment."; type = lib'.types.dependencies.generic; default.value = {}; }; @@ -260,16 +411,19 @@ in { host = { only = lib.options.create { + description = "Dependencies which are only used in the host environment."; type = lib'.types.dependencies.generic; default.value = {}; }; host = lib.options.create { + description = "Dependencies which are executed in the host environment."; type = lib'.types.dependencies.generic; default.value = {}; }; target = lib.options.create { + description = "Dependencies which are executed in the host environment which produces code for the target environment."; type = lib'.types.dependencies.generic; default.value = {}; }; @@ -277,11 +431,13 @@ in { target = { only = lib.options.create { + description = "Dependencies which are only used in the target environment."; type = lib'.types.dependencies.generic; default.value = {}; }; target = lib.options.create { + description = "Dependencies which are executed in the target environment."; type = lib'.types.dependencies.generic; default.value = {}; }; @@ -289,7 +445,8 @@ in { }; versions = lib.options.create { - type = lib.types.attrs.of lib'.types.packages.generic; + description = "Available package versions."; + type = lib.types.attrs.of lib'.types.package.versioned; default.value = {}; }; }; @@ -302,7 +459,10 @@ in { config, }: { options = { + freeform = lib.types.any; + name = lib.options.create { + description = "The name of the package."; type = lib.types.string; default = { text = "\${namespace}-\${pname}-\${version} or \${pname}-\${version}"; @@ -320,11 +480,13 @@ in { }; namespace = lib.options.create { + description = "The namespace for the package."; type = lib.types.nullish lib.types.string; default.value = null; }; pname = lib.options.create { + description = "The program name for the package."; type = lib.types.string; default = { text = " or \"unknown\""; @@ -336,11 +498,13 @@ in { }; version = lib.options.create { + description = "The version for the package."; type = lib.types.nullish lib.types.version; default.value = null; }; meta = lib.options.create { + description = "Metadata for the package."; type = lib'.types.meta; default.value = { name = config.pname; @@ -349,22 +513,26 @@ in { platform = { build = lib.options.create { + description = "The build platform for the package."; type = lib.types.nullish lib.types.string; default.value = null; }; host = lib.options.create { + description = "The host platform for the package."; type = lib.types.nullish lib.types.string; default.value = null; }; target = lib.options.create { + description = "The target platform for the package."; type = lib.types.nullish lib.types.string; default.value = null; }; }; phases = lib.options.create { + description = "Build phases for the package."; type = lib.types.dag.of ( lib.types.either lib.types.string @@ -374,36 +542,43 @@ in { }; env = lib.options.create { + description = "The environment for the package."; type = lib.types.attrs.of lib.types.string; default.value = {}; }; builder = lib.options.create { + description = "The builder for the package."; type = lib'.types.builder; }; package = lib.options.create { + description = "The built derivation."; type = lib.types.derivation; }; deps = { build = { only = lib.options.create { + description = "Dependencies which are only used in the build environment."; type = lib'.types.dependencies.targeted; default.value = {}; }; build = lib.options.create { + description = "Dependencies which are created in the build environment and are executed in the build environment."; type = lib'.types.dependencies.targeted; default.value = {}; }; host = lib.options.create { + description = "Dependencies which are created in the build environment and are executed in the host environment."; type = lib'.types.dependencies.targeted; default.value = {}; }; target = lib.options.create { + description = "Dependencies which are created in the build environment and are executed in the target environment."; type = lib'.types.dependencies.targeted; default.value = {}; }; @@ -411,16 +586,19 @@ in { host = { only = lib.options.create { + description = "Dependencies which are only used in the host environment."; type = lib'.types.dependencies.targeted; default.value = {}; }; host = lib.options.create { + description = "Dependencies which are executed in the host environment."; type = lib'.types.dependencies.targeted; default.value = {}; }; target = lib.options.create { + description = "Dependencies which are executed in the host environment which produces code for the target environment."; type = lib'.types.dependencies.targeted; default.value = {}; }; @@ -428,11 +606,13 @@ in { target = { only = lib.options.create { + description = "Dependencies which are only used in the target environment."; type = lib'.types.dependencies.targeted; default.value = {}; }; target = lib.options.create { + description = "Dependencies which are executed in the target environment."; type = lib'.types.dependencies.targeted; default.value = {}; }; @@ -440,7 +620,8 @@ in { }; versions = lib.options.create { - type = lib.types.attrs.of lib'.types.packages.targeted; + description = "Available package versions."; + type = lib.types.attrs.of lib'.types.package.versioned; default.value = {}; }; }; diff --git a/tidepool/src/modules.nix b/tidepool/src/modules.nix index e2b06d9..0b97ad5 100644 --- a/tidepool/src/modules.nix +++ b/tidepool/src/modules.nix @@ -1,4 +1,5 @@ { + builders = ./builders; exports = ./exports; lib = ./lib; packages = ./packages; diff --git a/tidepool/src/packages/aux/foundation.nix b/tidepool/src/packages/aux/foundation.nix index ddab66d..5470462 100644 --- a/tidepool/src/packages/aux/foundation.nix +++ b/tidepool/src/packages/aux/foundation.nix @@ -6,8 +6,6 @@ lib' = config.lib; in { config = { - exports.packages.example-x = lib'.packages.export "example.x"; - packages = { generic = { example = { @@ -15,12 +13,7 @@ in { meta.platforms = ["i686-linux" "x86_64-linux"]; version = "1.0.0"; - builder.build = package: - derivation { - name = package.name; - builder = "/bin/sh"; - system = package.platform.build; - }; + builder = config.builders.basic; phases = { build = package: '' @@ -31,6 +24,10 @@ in { make install DESTDIR=$out ''; }; + + versions = { + "latest" = config.packages.generic.example.x; + }; }; }; };