diff --git a/docs/design/recipe_spec.md b/docs/design/recipe_spec.md index f9f992c..c87a291 100644 --- a/docs/design/recipe_spec.md +++ b/docs/design/recipe_spec.md @@ -121,7 +121,7 @@ The `recipe` node defines package metadata. All properties are specified as chil | `url` | string | "" | Project homepage URL | `url "http://busybox.net"` | | `licenses` | string(s) | [] | License identifiers (multiple allowed) | `licenses "GPL" "MIT"` | | `archs` | string(s) | ["x86_64"] | Supported architectures | `archs "aarch64" "x86_64"` | -| `depends` | string(s) | [] | Build dependencies | `depends "make" "llvm"` | +| `depends` | string(s) | [] | Build dependencies; repeatable entries may use `arch` to select a target architecture | `depends "make" "llvm"` | | `env` | properties | {} | Environment variables for all phases | `env CC="clang"` | **Example:** @@ -136,11 +136,29 @@ recipe { licenses "BSD" archs "x86_64" "aarch64" depends "openssl-dev" "pcre2-dev" "zlib-ng-dev" + depends "nasm" arch="x86_64" env CC="clang" CFLAGS="-O2" } ``` -### `vars` Node (Optional) +### Architecture-specific build dependencies + +A `depends` entry without an `arch` property applies to every architecture supported by the recipe. Add `arch` when a build dependency is needed only for the package target architecture: + +```kdl +recipe { + name "libjpeg-turbo" + version "3.1.0" + release 1 + archs "x86_64" "aarch64" + + depends "cmake" "ninja" + depends "nasm" arch="x86_64" +} +``` + +Here `cmake` and `ninja` are selected for both targets. `nasm` is selected only for the x86_64 package and is omitted for aarch64, where it is unavailable. `arch` always means the package target architecture, not the build host architecture. + Define custom variables for use in string interpolation. diff --git a/src/kdl_schema.zig b/src/kdl_schema.zig index 0129135..87cf7eb 100644 --- a/src/kdl_schema.zig +++ b/src/kdl_schema.zig @@ -466,7 +466,14 @@ const recipe_meta_children = [_]NodeSpec{ .{ .name = "url", .args = .{ .kind = .string, .min = 1, .max = 1, .label = "value" } }, .{ .name = "licenses", .args = .{ .kind = .string, .min = 1, .label = "value" } }, .{ .name = "archs", .args = .{ .kind = .string, .min = 1, .label = "value" } }, - .{ .name = "depends", .args = .{ .kind = .string, .min = 1, .label = "value" } }, + .{ + .name = "depends", + .repeatable = true, + .args = .{ .kind = .string, .min = 1, .label = "value" }, + .properties = .{ .fixed = &[_]PropertySpec{ + .{ .name = "arch", .kind = .string }, + } }, + }, .{ .name = "needs-root", .args = .{ .kind = .boolean, .min = 1, .max = 1, .label = "value" } }, recipe_env_node, }; diff --git a/src/recipe.zig b/src/recipe.zig index 424be52..934a3ae 100644 --- a/src/recipe.zig +++ b/src/recipe.zig @@ -74,6 +74,14 @@ pub fn parse(ctx: *mere.Context, recipe_buf: []const u8) !Recipe { }; } + // Resolve dependencies after architecture selection so an optional + // depends arch="..." selector applies to the recipe target. + for (nodes.items) |*node| { + if (std.mem.eql(u8, node.name, "recipe")) { + try parseKdlDepends(allocator, node, &recipe); + } + } + // Parse remaining sections that can use interpolation. var found_package = false; for (nodes.items) |*node| { @@ -221,15 +229,6 @@ fn parseKdlRecipeNode(allocator: std.mem.Allocator, node: *const kdl.Node, recip } } - // depends "cmake" "ninja" - if (node.findChild("depends")) |depends_node| { - for (depends_node.arguments.items) |arg| { - if (arg.getString()) |s| { - const dup = try allocator.dupe(u8, s); - try recipe.depends.append(allocator, dup); - } - } - } // env CC="clang" CXX="clang++" (properties on an env child node) if (node.findChild("env")) |env_node| { @@ -237,6 +236,61 @@ fn parseKdlRecipeNode(allocator: std.mem.Allocator, node: *const kdl.Node, recip } } +/// Parse dependencies whose optional arch selector matches the recipe target. +fn parseKdlDepends(allocator: std.mem.Allocator, node: *const kdl.Node, recipe: *Recipe) !void { + for (node.children.items) |*child| { + if (!std.mem.eql(u8, child.name, "depends")) continue; + const selector = if (child.getProperty("arch")) |property| + property.getString() orelse return RecipeError.NonStringValue + else + null; + if (!dependencyAppliesToArch(selector, recipe.arch)) continue; + for (child.arguments.items) |arg| { + if (arg.getString()) |dependency| { + try recipe.depends.append(allocator, try allocator.dupe(u8, dependency)); + } + } + } +} + +fn dependencyAppliesToArch(selector: ?[]const u8, recipe_arch: ?[]const u8) bool { + if (selector == null) return true; + return recipe_arch != null and std.mem.eql(u8, selector.?, recipe_arch.?); +} + +test "parseKdlDepends selects target-specific dependencies" { + const th = @import("test_helpers.zig"); + var test_env = try th.createTestEnv(); + defer { + test_env.cleanup(); + std.testing.allocator.destroy(test_env); + } + + const ctx = &test_env.ctx; + var nodes = try kdl.parseDocument( + ctx.allocator, + "recipe {\n depends \"cmake\"\n depends \"nasm\" arch=\"x86_64\"\n}", + ); + defer { + for (nodes.items) |*node| node.deinit(); + nodes.deinit(ctx.allocator); + } + + var x86_recipe = try Recipe.init(ctx.allocator, ctx); + defer x86_recipe.deinit(); + x86_recipe.arch = "x86_64"; + try parseKdlDepends(ctx.allocator, &nodes.items[0], &x86_recipe); + try std.testing.expectEqualStrings("cmake", x86_recipe.depends.items[0]); + try std.testing.expectEqualStrings("nasm", x86_recipe.depends.items[1]); + + var arm_recipe = try Recipe.init(ctx.allocator, ctx); + defer arm_recipe.deinit(); + arm_recipe.arch = "aarch64"; + try parseKdlDepends(ctx.allocator, &nodes.items[0], &arm_recipe); + try std.testing.expectEqual(@as(usize, 1), arm_recipe.depends.items.len); + try std.testing.expectEqualStrings("cmake", arm_recipe.depends.items[0]); +} + /// Parse a vars {} node fn parseKdlVarsNode(allocator: std.mem.Allocator, node: *const kdl.Node, vars: *std.ArrayList(KV)) !void { // Each child node is a var: major "20"