From b13dee642f8a60dcb480eb314328148a0c60d7ef Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Wed, 4 Feb 2026 14:10:32 -0800 Subject: [PATCH] add profile to alexa permissions tool --- build.zig | 20 +++++++++ src/timezone.zig | 34 +++++++++------ tools/add-alexa-permission.zig | 77 ++++++++++++++++++++++++++++------ 3 files changed, 105 insertions(+), 26 deletions(-) diff --git a/build.zig b/build.zig index 2b74181..ef1320d 100644 --- a/build.zig +++ b/build.zig @@ -50,6 +50,20 @@ pub fn build(b: *std.Build) !void { // Function name defaults to exe.name ("house-control") const lambda = try lambda_zig.configureBuild(b, lambda_zig_dep, exe, .{}); + // Get AWS profile option (already declared by lambda-zig) + const profile = b.user_input_options.get("profile"); + const profile_str: ?[]const u8 = if (profile) |p| switch (p.value) { + .scalar => |s| s, + else => null, + } else null; + + // Get AWS region option (already declared by lambda-zig) + const region = b.user_input_options.get("region"); + const region_str: ?[]const u8 = if (region) |r| switch (r.value) { + .scalar => |s| s, + else => null, + } else null; + // Build the gen-skill-json tool (runs on host) const gen_skill_json_module = b.createModule(.{ .root_source_file = b.path("tools/gen-skill-json.zig"), @@ -112,6 +126,12 @@ pub fn build(b: *std.Build) !void { const add_alexa_perm_cmd = b.addRunArtifact(add_alexa_perm_exe); add_alexa_perm_cmd.addFileArg(lambda.deploy_output); add_alexa_perm_cmd.addFileArg(b.path(".ask/ask-states.json")); + if (profile_str) |p| { + add_alexa_perm_cmd.addArgs(&.{ "--profile", p }); + } + if (region_str) |r| { + add_alexa_perm_cmd.addArgs(&.{ "--region", r }); + } // Must run after ASK deploy (which creates/updates skill ID) and Lambda deploy add_alexa_perm_cmd.step.dependOn(&ask_deploy_cmd.step); diff --git a/src/timezone.zig b/src/timezone.zig index 5691d54..1be56b4 100644 --- a/src/timezone.zig +++ b/src/timezone.zig @@ -1,28 +1,36 @@ //! Timezone utilities: TZif parsing and local timezone resolution. const std = @import("std"); +const builtin = @import("builtin"); const Allocator = std.mem.Allocator; const log = std.log.scoped(.timezone); +// Suppress warnings during tests +fn warn(comptime fmt: []const u8, args: anytype) void { + if (!builtin.is_test) { + log.warn(fmt, args); + } +} + /// Get UTC offset in seconds for a timezone name. /// Reads /usr/share/zoneinfo/{name} and parses TZif format. /// Returns null on any error (file not found, parse error, etc.) pub fn getUtcOffset(allocator: Allocator, timezone_name: []const u8) ?i32 { const path = std.fmt.allocPrint(allocator, "/usr/share/zoneinfo/{s}", .{timezone_name}) catch { - log.warn("Failed to allocate timezone path", .{}); + warn("Failed to allocate timezone path", .{}); return null; }; defer allocator.free(path); const file = std.fs.openFileAbsolute(path, .{}) catch { - log.warn("Failed to open timezone file: {s}", .{path}); + warn("Failed to open timezone file: {s}", .{path}); return null; }; defer file.close(); const data = file.readToEndAlloc(allocator, 64 * 1024) catch { - log.warn("Failed to read timezone file: {s}", .{path}); + warn("Failed to read timezone file: {s}", .{path}); return null; }; defer allocator.free(data); @@ -51,13 +59,13 @@ pub fn getLocalTimezone(allocator: Allocator) ?[]const u8 { // Fall back to /etc/timezone const file = std.fs.openFileAbsolute("/etc/timezone", .{}) catch { - log.warn("No TZ env var and failed to open /etc/timezone", .{}); + warn("No TZ env var and failed to open /etc/timezone", .{}); return null; }; defer file.close(); const content = file.readToEndAlloc(allocator, 256) catch { - log.warn("Failed to read /etc/timezone", .{}); + warn("Failed to read /etc/timezone", .{}); return null; }; @@ -80,13 +88,13 @@ pub fn getLocalTimezone(allocator: Allocator) ?[]const u8 { fn parseTzif(data: []const u8) ?i32 { // TZif header is 44 bytes minimum if (data.len < 44) { - log.warn("TZif file too small: {d} bytes", .{data.len}); + warn("TZif file too small: {d} bytes", .{data.len}); return null; } // Check magic number "TZif" if (!std.mem.eql(u8, data[0..4], "TZif")) { - log.warn("Invalid TZif magic number", .{}); + warn("Invalid TZif magic number", .{}); return null; } @@ -111,7 +119,7 @@ fn parseTzif(data: []const u8) ?i32 { const tzh_charcnt = std.mem.readInt(u32, data[40..44], .big); if (tzh_typecnt == 0) { - log.warn("TZif file has no time types", .{}); + warn("TZif file has no time types", .{}); return null; } @@ -130,13 +138,13 @@ fn parseTzif(data: []const u8) ?i32 { const v2_header_start = 44 + v1_data_size; if (data.len < v2_header_start + 44) { - log.warn("TZif v2 file truncated", .{}); + warn("TZif v2 file truncated", .{}); return null; } // Verify v2 header magic if (!std.mem.eql(u8, data[v2_header_start..][0..4], "TZif")) { - log.warn("Invalid TZif v2 header magic", .{}); + warn("Invalid TZif v2 header magic", .{}); return null; } @@ -145,7 +153,7 @@ fn parseTzif(data: []const u8) ?i32 { const v2_typecnt = std.mem.readInt(u32, data[v2_header_start + 36 ..][0..4], .big); if (v2_typecnt == 0) { - log.warn("TZif v2 has no time types", .{}); + warn("TZif v2 has no time types", .{}); return null; } @@ -171,7 +179,7 @@ fn parseTzif(data: []const u8) ?i32 { /// Each ttinfo is 6 bytes: i32 offset, u8 is_dst, u8 abbr_idx fn readLastTtinfoOffset(data: []const u8, ttinfo_offset: usize, typecnt: u32) ?i32 { if (data.len < ttinfo_offset + 6) { - log.warn("TZif file truncated at ttinfo", .{}); + warn("TZif file truncated at ttinfo", .{}); return null; } @@ -203,7 +211,7 @@ fn readLastTtinfoOffset(data: []const u8, ttinfo_offset: usize, typecnt: u32) ?i // Sanity check if (offset < -14 * 3600 or offset > 14 * 3600) { - log.warn("TZif offset out of range: {d}", .{offset}); + warn("TZif offset out of range: {d}", .{offset}); return null; } diff --git a/tools/add-alexa-permission.zig b/tools/add-alexa-permission.zig index 919a93a..e7dbd81 100644 --- a/tools/add-alexa-permission.zig +++ b/tools/add-alexa-permission.zig @@ -10,51 +10,93 @@ pub fn main() !u8 { defer _ = gpa.deinit(); const allocator = gpa.allocator(); + var stdout_buf: [4096]u8 = undefined; + var stderr_buf: [4096]u8 = undefined; + var stdout_writer = std.fs.File.stdout().writer(&stdout_buf); + var stderr_writer = std.fs.File.stderr().writer(&stderr_buf); + const stdout = &stdout_writer.interface; + const stderr = &stderr_writer.interface; + const args = try std.process.argsAlloc(allocator); defer std.process.argsFree(allocator, args); - if (args.len != 3) { - std.debug.print("Usage: {s} \n", .{args[0]}); + if (args.len < 3) { + try stderr.print("Usage: {s} [--profile ] [--region ]\n", .{args[0]}); + try stderr.flush(); return 1; } + // Parse optional arguments + var profile: ?[]const u8 = null; + var region_override: ?[]const u8 = null; + var i: usize = 3; + while (i < args.len) : (i += 1) { + if (std.mem.eql(u8, args[i], "--profile") and i + 1 < args.len) { + profile = args[i + 1]; + i += 1; + } else if (std.mem.eql(u8, args[i], "--region") and i + 1 < args.len) { + region_override = args[i + 1]; + i += 1; + } + } + // Read deploy output to get function name and region const deploy_output = std.fs.cwd().readFileAlloc(allocator, args[1], 1024 * 1024) catch |err| { - std.debug.print("Failed to read deploy output '{s}': {}\n", .{ args[1], err }); + try stderr.print("Failed to read deploy output '{s}': {}\n", .{ args[1], err }); + try stderr.flush(); return 1; }; defer allocator.free(deploy_output); const deploy_parsed = json.parseFromSlice(json.Value, allocator, deploy_output, .{}) catch |err| { - std.debug.print("Failed to parse deploy output: {}\n", .{err}); + try stderr.print("Failed to parse deploy output: {}\n", .{err}); + try stderr.flush(); return 1; }; defer deploy_parsed.deinit(); const function_name = deploy_parsed.value.object.get("function_name").?.string; - const region = deploy_parsed.value.object.get("region").?.string; + const region = region_override orelse deploy_parsed.value.object.get("region").?.string; // Read ask-states.json to get skill ID const ask_states = std.fs.cwd().readFileAlloc(allocator, args[2], 1024 * 1024) catch |err| { - std.debug.print("Failed to read ask-states.json '{s}': {}\n", .{ args[2], err }); + try stderr.print("Failed to read ask-states.json '{s}': {}\n", .{ args[2], err }); + try stderr.flush(); return 1; }; defer allocator.free(ask_states); const ask_parsed = json.parseFromSlice(json.Value, allocator, ask_states, .{}) catch |err| { - std.debug.print("Failed to parse ask-states.json: {}\n", .{err}); + try stderr.print("Failed to parse ask-states.json: {}\n", .{err}); + try stderr.flush(); return 1; }; defer ask_parsed.deinit(); - const skill_id = ask_parsed.value.object.get("profiles").?.object.get("default").?.object.get("skillId").?.string; + // Use profile name to look up skill ID, defaulting to "default" + const profile_name = profile orelse "default"; + const profiles_obj = ask_parsed.value.object.get("profiles") orelse { + try stderr.print("No 'profiles' field in ask-states.json\n", .{}); + try stderr.flush(); + return 1; + }; + const profile_obj = profiles_obj.object.get(profile_name) orelse { + try stderr.print("Profile '{s}' not found in ask-states.json\n", .{profile_name}); + try stderr.flush(); + return 1; + }; + const skill_id = profile_obj.object.get("skillId").?.string; - std.debug.print("Adding Alexa permission for skill {s} to function {s} in {s}\n", .{ skill_id, function_name, region }); + try stdout.print("Adding Alexa permission for skill {s} to function {s} in {s}", .{ skill_id, function_name, region }); + if (profile) |p| try stdout.print(" (profile: {s})", .{p}); + try stdout.print("\n", .{}); + try stdout.flush(); // Build statement ID from skill ID (use last 12 chars to keep it short but unique) var statement_id_buf: [64]u8 = undefined; const statement_id = std.fmt.bufPrint(&statement_id_buf, "alexa-skill-{s}", .{skill_id[skill_id.len - 12 ..]}) catch { - std.debug.print("Failed to build statement ID\n", .{}); + try stderr.print("Failed to build statement ID\n", .{}); + try stderr.flush(); return 1; }; @@ -72,6 +114,12 @@ pub fn main() !u8 { .client = client, .region = region, .diagnostics = &diagnostics, + .credential_options = .{ + .profile = .{ + .profile_name = profile, + .prefer_profile_from_file = profile != null, + }, + }, }; // Add permission with skill ID as event source token @@ -88,14 +136,17 @@ pub fn main() !u8 { // 409 Conflict means permission already exists - that's fine if (diagnostics.response_status == .conflict) { - std.debug.print("Permission already exists for skill: {s}\n", .{skill_id}); + try stdout.print("Permission already exists for skill: {s}\n", .{skill_id}); + try stdout.flush(); return 0; } - std.debug.print("AddPermission failed: {} (HTTP {})\n", .{ err, diagnostics.response_status }); + try stderr.print("AddPermission failed: {} (HTTP {})\n", .{ err, diagnostics.response_status }); + try stderr.flush(); return 1; }; - std.debug.print("Added Alexa permission for skill: {s}\n", .{skill_id}); + try stdout.print("Added Alexa permission for skill: {s}\n", .{skill_id}); + try stdout.flush(); return 0; }