diff --git a/build.zig b/build.zig index d30e36f..dcc25aa 100644 --- a/build.zig +++ b/build.zig @@ -107,11 +107,13 @@ fn configureBuildInternal(b: *std.Build, exe: *std.Build.Step.Compile) !void { .target = b.graph.host, .optimize = .ReleaseSafe, }); - try @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, .{}); + // Ignore return value for internal builds + _ = try @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, .{}); } -/// Re-export LambdaConfig for consumers +/// Re-export types for consumers pub const LambdaConfig = @import("lambdabuild.zig").Config; +pub const LambdaBuildInfo = @import("lambdabuild.zig").BuildInfo; /// Configure Lambda build steps for a Zig project. /// @@ -119,6 +121,11 @@ pub const LambdaConfig = @import("lambdabuild.zig").Config; /// Lambda functions to AWS. The `lambda_zig_dep` parameter must be the /// dependency object obtained from `b.dependency("lambda_zig", ...)`. /// +/// Returns a `LambdaBuildInfo` struct containing: +/// - References to all build steps (package, iam, deploy, invoke) +/// - A `deploy_output` LazyPath to a JSON file with deployment info +/// - The function name used +/// /// ## Build Steps /// /// The following build steps are added: @@ -144,6 +151,24 @@ pub const LambdaConfig = @import("lambdabuild.zig").Config; /// - `-Dallow-principal=[string]`: AWS service principal to grant invoke permission /// (e.g., "alexa-appkit.amazon.com" for Alexa Skills Kit) /// +/// ## Deploy Output +/// +/// The `deploy_output` field in the returned struct is a LazyPath to a JSON file +/// containing deployment information (available after deploy completes): +/// +/// ```json +/// { +/// "arn": "arn:aws:lambda:us-east-1:123456789012:function:my-function", +/// "function_name": "my-function", +/// "partition": "aws", +/// "region": "us-east-1", +/// "account_id": "123456789012", +/// "role_arn": "arn:aws:iam::123456789012:role/lambda_basic_execution", +/// "architecture": "arm64", +/// "environment_keys": ["MY_VAR"] +/// } +/// ``` +/// /// ## Example /// /// ```zig @@ -161,13 +186,15 @@ pub const LambdaConfig = @import("lambdabuild.zig").Config; /// const exe = b.addExecutable(.{ ... }); /// b.installArtifact(exe); /// -/// // Use default config (function name defaults to "zig-fn") -/// try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{}); -/// -/// // Or specify project-level defaults -/// try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{ +/// // Configure Lambda build and get deployment info +/// const lambda = try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{ /// .default_function_name = "my-function", /// }); +/// +/// // Use lambda.deploy_output in other steps that need the ARN +/// const my_step = b.addRunArtifact(my_tool); +/// my_step.addFileArg(lambda.deploy_output); +/// my_step.step.dependOn(lambda.deploy_step); // Ensure deploy runs first /// } /// ``` pub fn configureBuild( @@ -175,11 +202,11 @@ pub fn configureBuild( lambda_zig_dep: *std.Build.Dependency, exe: *std.Build.Step.Compile, config: LambdaConfig, -) !void { +) !LambdaBuildInfo { // Get lambda_build from the lambda_zig dependency's Build context const lambda_build_dep = lambda_zig_dep.builder.dependency("lambda_build", .{ .target = b.graph.host, .optimize = .ReleaseSafe, }); - try @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, config); + return @import("lambdabuild.zig").configureBuild(b, lambda_build_dep, exe, config); } diff --git a/lambdabuild.zig b/lambdabuild.zig index e787f12..9aecd27 100644 --- a/lambdabuild.zig +++ b/lambdabuild.zig @@ -18,6 +18,32 @@ pub const Config = struct { default_role_name: []const u8 = "lambda_basic_execution", }; +/// Information about the configured Lambda build steps. +/// +/// Returned by `configureBuild` to allow consumers to depend on steps +/// and access deployment outputs. +pub const BuildInfo = struct { + /// Package step - creates the deployment zip + package_step: *std.Build.Step, + + /// IAM step - creates/verifies the IAM role + iam_step: *std.Build.Step, + + /// Deploy step - deploys the function to AWS Lambda + deploy_step: *std.Build.Step, + + /// Invoke step - invokes the deployed function + invoke_step: *std.Build.Step, + + /// LazyPath to JSON file with deployment info. + /// Contains: arn, function_name, region, account_id, role_arn, architecture, environment_keys + /// Available after deploy_step completes. + deploy_output: std.Build.LazyPath, + + /// The function name used for deployment + function_name: []const u8, +}; + /// Configure Lambda build steps for a Zig project. /// /// Adds the following build steps: @@ -28,12 +54,15 @@ pub const Config = struct { /// /// The `config` parameter allows setting project-level defaults that can /// still be overridden via command-line options. +/// +/// Returns a `BuildInfo` struct containing references to all steps and +/// a `deploy_output` LazyPath to the deployment info JSON file. pub fn configureBuild( b: *std.Build, lambda_build_dep: *std.Build.Dependency, exe: *std.Build.Step.Compile, config: Config, -) !void { +) !BuildInfo { // Get the lambda-build CLI artifact from the dependency const cli = lambda_build_dep.artifact("lambda-build"); @@ -117,6 +146,9 @@ pub fn configureBuild( }); if (env_file) |ef| deploy_cmd.addArgs(&.{ "--env-file", ef }); if (allow_principal) |ap| deploy_cmd.addArgs(&.{ "--allow-principal", ap }); + // Add deploy output file for deployment info JSON + deploy_cmd.addArg("--deploy-output"); + const deploy_output = deploy_cmd.addOutputFileArg("deploy-output.json"); deploy_cmd.step.dependOn(&package_cmd.step); const deploy_step = b.step("awslambda_deploy", "Deploy the Lambda function"); @@ -138,4 +170,13 @@ pub fn configureBuild( const run_step = b.step("awslambda_run", "Invoke the deployed Lambda function"); run_step.dependOn(&invoke_cmd.step); + + return .{ + .package_step = package_step, + .iam_step = iam_step, + .deploy_step = deploy_step, + .invoke_step = run_step, + .deploy_output = deploy_output, + .function_name = function_name, + }; } diff --git a/tools/build/src/deploy.zig b/tools/build/src/deploy.zig index 41399a2..cb83ccc 100644 --- a/tools/build/src/deploy.zig +++ b/tools/build/src/deploy.zig @@ -18,6 +18,7 @@ pub fn run(args: []const []const u8, options: RunOptions) !void { var role_name: []const u8 = "lambda_basic_execution"; var arch: ?[]const u8 = null; var allow_principal: ?[]const u8 = null; + var deploy_output: ?[]const u8 = null; // Environment variables storage var env_vars = std.StringHashMap([]const u8).init(options.allocator); @@ -65,6 +66,10 @@ pub fn run(args: []const []const u8, options: RunOptions) !void { i += 1; if (i >= args.len) return error.MissingAllowPrincipal; allow_principal = args[i]; + } else if (std.mem.eql(u8, arg, "--deploy-output")) { + i += 1; + if (i >= args.len) return error.MissingDeployOutput; + deploy_output = args[i]; } else if (std.mem.eql(u8, arg, "--help") or std.mem.eql(u8, arg, "-h")) { printHelp(options.stdout); try options.stdout.flush(); @@ -98,6 +103,7 @@ pub fn run(args: []const []const u8, options: RunOptions) !void { .arch = arch, .env_vars = if (env_vars.count() > 0) &env_vars else null, .allow_principal = allow_principal, + .deploy_output = deploy_output, }, options); } @@ -187,6 +193,7 @@ fn printHelp(writer: anytype) void { \\ --env-file Load environment variables from file (KEY=VALUE format) \\ --allow-principal

Grant invoke permission to AWS service principal \\ (e.g., alexa-appkit.amazon.com) + \\ --deploy-output Write deployment info (ARN, region, etc.) to JSON file \\ --help, -h Show this help message \\ \\Environment File Format: @@ -212,6 +219,7 @@ const DeployOptions = struct { arch: ?[]const u8, env_vars: ?*const std.StringHashMap([]const u8), allow_principal: ?[]const u8, + deploy_output: ?[]const u8, }; fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void { @@ -275,6 +283,10 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void { var create_options = options.aws_options; create_options.diagnostics = &create_diagnostics; + // Track the function ARN from whichever path succeeds + var function_arn: ?[]const u8 = null; + defer if (function_arn) |arn| options.allocator.free(arn); + const create_result = aws.Request(services.lambda.create_function).call(.{ .function_name = deploy_opts.function_name, .architectures = architectures, @@ -302,6 +314,7 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void { try options.stdout.print("Updated function: {s}\n", .{deploy_opts.function_name}); if (update_result.response.function_arn) |arn| { try options.stdout.print("ARN: {s}\n", .{arn}); + function_arn = try options.allocator.dupe(u8, arn); } try options.stdout.flush(); @@ -318,6 +331,11 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void { try addPermission(deploy_opts.function_name, principal, options); } + // Write deploy output if requested + if (deploy_opts.deploy_output) |output_path| { + try writeDeployOutput(output_path, function_arn.?, role_arn, lambda_arch, deploy_opts.env_vars); + } + return; } @@ -329,6 +347,7 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void { try options.stdout.print("Created function: {s}\n", .{deploy_opts.function_name}); if (create_result.response.function_arn) |arn| { try options.stdout.print("ARN: {s}\n", .{arn}); + function_arn = try options.allocator.dupe(u8, arn); } try options.stdout.flush(); @@ -339,6 +358,11 @@ fn deployFunction(deploy_opts: DeployOptions, options: RunOptions) !void { if (deploy_opts.allow_principal) |principal| { try addPermission(deploy_opts.function_name, principal, options); } + + // Write deploy output if requested + if (deploy_opts.deploy_output) |output_path| { + try writeDeployOutput(output_path, function_arn.?, role_arn, lambda_arch, deploy_opts.env_vars); + } } /// Build environment variables in the format expected by AWS Lambda API @@ -487,3 +511,65 @@ fn addPermission( try options.stdout.print("Added invoke permission for: {s}\n", .{principal}); try options.stdout.flush(); } + +/// Write deployment information to a JSON file +fn writeDeployOutput( + output_path: []const u8, + function_arn: []const u8, + role_arn: []const u8, + architecture: []const u8, + env_vars: ?*const std.StringHashMap([]const u8), +) !void { + // Parse ARN to extract components + // ARN format: arn:{partition}:lambda:{region}:{account_id}:function:{name} + var arn_parts = std.mem.splitScalar(u8, function_arn, ':'); + _ = arn_parts.next(); // arn + const partition = arn_parts.next() orelse return error.InvalidArn; + _ = arn_parts.next(); // lambda + const region = arn_parts.next() orelse return error.InvalidArn; + const account_id = arn_parts.next() orelse return error.InvalidArn; + _ = arn_parts.next(); // function + const function_name = arn_parts.next() orelse return error.InvalidArn; + + const file = try std.fs.cwd().createFile(output_path, .{}); + defer file.close(); + + var write_buffer: [4096]u8 = undefined; + var buffered = file.writer(&write_buffer); + const writer = &buffered.interface; + + try writer.print( + \\{{ + \\ "arn": "{s}", + \\ "function_name": "{s}", + \\ "partition": "{s}", + \\ "region": "{s}", + \\ "account_id": "{s}", + \\ "role_arn": "{s}", + \\ "architecture": "{s}", + \\ "environment_keys": [ + , .{ function_arn, function_name, partition, region, account_id, role_arn, architecture }); + + // Write environment variable keys + if (env_vars) |vars| { + var it = vars.keyIterator(); + var first = true; + while (it.next()) |key| { + if (!first) { + try writer.writeAll(","); + } + try writer.print("\n \"{s}\"", .{key.*}); + first = false; + } + } + + try writer.writeAll( + \\ + \\ ] + \\} + \\ + ); + try writer.flush(); + + std.log.info("Wrote deployment info to: {s}", .{output_path}); +}