commit cfd3bcffec997a18521f685738e4dc7eef940422 (tree)
parent 98b85f72a656ef62a23cf0d0fbc3ed3b5cecc95b
Author: Andrew Kelley <andrew@ziglang.org>
Date: Fri, 4 Oct 2024 19:45:39 -0700
Merge pull request #21591 from patrickwick/issue-19009
zig objcopy: support --add-section
Diffstat:
2 files changed, 405 insertions(+), 16 deletions(-)
diff --git a/lib/compiler/objcopy.zig b/lib/compiler/objcopy.zig
@@ -40,6 +40,9 @@ fn cmdObjCopy(
var only_keep_debug: bool = false;
var compress_debug_sections: bool = false;
var listen = false;
+ var add_section: ?AddSection = null;
+ var set_section_alignment: ?SetSectionAlignment = null;
+ var set_section_flags: ?SetSectionFlags = null;
while (i < args.len) : (i += 1) {
const arg = args[i];
if (!mem.startsWith(u8, arg, "-")) {
@@ -104,6 +107,37 @@ fn cmdObjCopy(
i += 1;
if (i >= args.len) fatal("expected another argument after '{s}'", .{arg});
opt_extract = args[i];
+ } else if (mem.eql(u8, arg, "--set-section-alignment")) {
+ i += 1;
+ if (i >= args.len) fatal("expected section name and alignment arguments after '{s}'", .{arg});
+
+ if (splitOption(args[i])) |split| {
+ const alignment = std.fmt.parseInt(u32, split.second, 10) catch |err| {
+ fatal("unable to parse alignment number: '{s}': {s}", .{ split.second, @errorName(err) });
+ };
+ if (!std.math.isPowerOfTwo(alignment)) fatal("alignment must be a power of two", .{});
+ set_section_alignment = .{ .section_name = split.first, .alignment = alignment };
+ } else {
+ fatal("unrecognized argument: '{s}', expecting <name>=<alignment>", .{args[i]});
+ }
+ } else if (mem.eql(u8, arg, "--set-section-flags")) {
+ i += 1;
+ if (i >= args.len) fatal("expected section name and filename arguments after '{s}'", .{arg});
+
+ if (splitOption(args[i])) |split| {
+ set_section_flags = .{ .section_name = split.first, .flags = parseSectionFlags(split.second) };
+ } else {
+ fatal("unrecognized argument: '{s}', expecting <name>=<flags>", .{args[i]});
+ }
+ } else if (mem.eql(u8, arg, "--add-section")) {
+ i += 1;
+ if (i >= args.len) fatal("expected section name and filename arguments after '{s}'", .{arg});
+
+ if (splitOption(args[i])) |split| {
+ add_section = .{ .section_name = split.first, .file_path = split.second };
+ } else {
+ fatal("unrecognized argument: '{s}', expecting <name>=<file>", .{args[i]});
+ }
} else {
fatal("unrecognized argument: '{s}'", .{arg});
}
@@ -151,6 +185,12 @@ fn cmdObjCopy(
fatal("zig objcopy: ELF to RAW or HEX copying does not support --strip", .{});
if (opt_extract != null)
fatal("zig objcopy: ELF to RAW or HEX copying does not support --extract-to", .{});
+ if (add_section != null)
+ fatal("zig objcopy: ELF to RAW or HEX copying does not support --add-section", .{});
+ if (set_section_alignment != null)
+ fatal("zig objcopy: ELF to RAW or HEX copying does not support --set_section_alignment", .{});
+ if (set_section_flags != null)
+ fatal("zig objcopy: ELF to RAW or HEX copying does not support --set_section_flags", .{});
try emitElf(arena, in_file, out_file, elf_hdr, .{
.ofmt = out_fmt,
@@ -175,6 +215,9 @@ fn cmdObjCopy(
.add_debuglink = opt_add_debuglink,
.extract_to = opt_extract,
.compress_debug = compress_debug_sections,
+ .add_section = add_section,
+ .set_section_alignment = set_section_alignment,
+ .set_section_flags = set_section_flags,
});
return std.process.cleanExit();
},
@@ -217,18 +260,21 @@ const usage =
\\Usage: zig objcopy [options] input output
\\
\\Options:
- \\ -h, --help Print this help and exit
- \\ --output-target=<value> Format of the output file
- \\ -O <value> Alias for --output-target
- \\ --only-section=<section> Remove all but <section>
- \\ -j <value> Alias for --only-section
- \\ --pad-to <addr> Pad the last section up to address <addr>
- \\ --strip-debug, -g Remove all debug sections from the output.
- \\ --strip-all, -S Remove all debug sections and symbol table from the output.
- \\ --only-keep-debug Strip a file, removing contents of any sections that would not be stripped by --strip-debug and leaving the debugging sections intact.
- \\ --add-gnu-debuglink=<file> Creates a .gnu_debuglink section which contains a reference to <file> and adds it to the output file.
- \\ --extract-to <file> Extract the removed sections into <file>, and add a .gnu-debuglink section.
- \\ --compress-debug-sections Compress DWARF debug sections with zlib
+ \\ -h, --help Print this help and exit
+ \\ --output-target=<value> Format of the output file
+ \\ -O <value> Alias for --output-target
+ \\ --only-section=<section> Remove all but <section>
+ \\ -j <value> Alias for --only-section
+ \\ --pad-to <addr> Pad the last section up to address <addr>
+ \\ --strip-debug, -g Remove all debug sections from the output.
+ \\ --strip-all, -S Remove all debug sections and symbol table from the output.
+ \\ --only-keep-debug Strip a file, removing contents of any sections that would not be stripped by --strip-debug and leaving the debugging sections intact.
+ \\ --add-gnu-debuglink=<file> Creates a .gnu_debuglink section which contains a reference to <file> and adds it to the output file.
+ \\ --extract-to <file> Extract the removed sections into <file>, and add a .gnu-debuglink section.
+ \\ --compress-debug-sections Compress DWARF debug sections with zlib
+ \\ --set-section-alignment <name>=<align> Set alignment of section <name> to <align> bytes. Must be a power of two.
+ \\ --set-section-flags <name>=<file> Set flags of section <name> to <flags> represented as a comma separated set of flags.
+ \\ --add-section <name>=<file> Add file content from <file> with the a new section named <name>.
\\
;
@@ -236,6 +282,24 @@ pub const EmitRawElfOptions = struct {
ofmt: std.Target.ObjectFormat,
only_section: ?[]const u8 = null,
pad_to: ?u64 = null,
+ add_section: ?AddSection = null,
+ set_section_alignment: ?SetSectionAlignment = null,
+ set_section_flags: ?SetSectionFlags = null,
+};
+
+const AddSection = struct {
+ section_name: []const u8,
+ file_path: []const u8,
+};
+
+const SetSectionAlignment = struct {
+ section_name: []const u8,
+ alignment: u32,
+};
+
+const SetSectionFlags = struct {
+ section_name: []const u8,
+ flags: SectionFlags,
};
fn emitElf(
@@ -678,6 +742,9 @@ const StripElfOptions = struct {
strip_debug: bool = false,
only_keep_debug: bool = false,
compress_debug: bool = false,
+ add_section: ?AddSection,
+ set_section_alignment: ?SetSectionAlignment,
+ set_section_flags: ?SetSectionFlags,
};
fn stripElf(
@@ -721,6 +788,14 @@ fn stripElf(
var elf_file = try ElfFile(is_64).parse(allocator, in_file, elf_hdr);
defer elf_file.deinit();
+ if (options.add_section) |user_section| {
+ for (elf_file.sections) |section| {
+ if (std.mem.eql(u8, section.name, user_section.section_name)) {
+ fatal("zig objcopy: unable to add section '{s}'. Section already exists in input", .{user_section.section_name});
+ }
+ }
+ }
+
if (filter_complement) |flt| {
// write the .dbg file and close it, so it can be read back to compute the debuglink checksum.
const path = options.extract_to.?;
@@ -733,7 +808,14 @@ fn stripElf(
}
const debuglink: ?DebugLink = if (debuglink_path) |path| ElfFileHelper.createDebugLink(path) else null;
- try elf_file.emit(allocator, out_file, in_file, .{ .section_filter = filter, .debuglink = debuglink, .compress_debug = options.compress_debug });
+ try elf_file.emit(allocator, out_file, in_file, .{
+ .section_filter = filter,
+ .debuglink = debuglink,
+ .compress_debug = options.compress_debug,
+ .add_section = options.add_section,
+ .set_section_alignment = options.set_section_alignment,
+ .set_section_flags = options.set_section_flags,
+ });
},
}
}
@@ -786,7 +868,7 @@ fn ElfFile(comptime is_64: bool) type {
// program header: list of segments
const program_segments = blk: {
if (@sizeOf(Elf_Phdr) != header.phentsize)
- fatal("zig objcopy: unsuported ELF file, unexpected phentsize ({d})", .{header.phentsize});
+ fatal("zig objcopy: unsupported ELF file, unexpected phentsize ({d})", .{header.phentsize});
const program_header = try allocator.alloc(Elf_Phdr, header.phnum);
const bytes_read = try in_file.preadAll(std.mem.sliceAsBytes(program_header), header.phoff);
@@ -798,7 +880,7 @@ fn ElfFile(comptime is_64: bool) type {
// section header
const sections = blk: {
if (@sizeOf(Elf_Shdr) != header.shentsize)
- fatal("zig objcopy: unsuported ELF file, unexpected shentsize ({d})", .{header.shentsize});
+ fatal("zig objcopy: unsupported ELF file, unexpected shentsize ({d})", .{header.shentsize});
const section_header = try allocator.alloc(Section, header.shnum);
@@ -896,6 +978,9 @@ fn ElfFile(comptime is_64: bool) type {
section_filter: Filter = .all,
debuglink: ?DebugLink = null,
compress_debug: bool = false,
+ add_section: ?AddSection = null,
+ set_section_alignment: ?SetSectionAlignment = null,
+ set_section_flags: ?SetSectionFlags = null,
};
fn emit(self: *const Self, gpa: Allocator, out_file: File, in_file: File, options: EmitElfOptions) !void {
var arena = std.heap.ArenaAllocator.init(gpa);
@@ -934,6 +1019,10 @@ fn ElfFile(comptime is_64: bool) type {
if (options.debuglink != null)
next_idx += 1;
+ if (options.add_section != null) {
+ next_idx += 1;
+ }
+
break :blk next_idx;
};
@@ -959,6 +1048,28 @@ fn ElfFile(comptime is_64: bool) type {
break :blk new_offset;
};
+ // add user section to the string table if needed
+ const user_section_name: u32 = blk: {
+ if (options.add_section == null) break :blk elf.SHN_UNDEF;
+ if (self.raw_elf_header.e_shstrndx == elf.SHN_UNDEF)
+ fatal("zig objcopy: no strtab, cannot add the user section", .{}); // TODO add the section if needed?
+
+ const strtab = &self.sections[self.raw_elf_header.e_shstrndx];
+ const update = §ions_update[self.raw_elf_header.e_shstrndx];
+
+ const name = options.add_section.?.section_name;
+ const new_offset: u32 = @intCast(strtab.payload.?.len);
+ const buf = try allocator.alignedAlloc(u8, section_memory_align, new_offset + name.len + 1);
+ @memcpy(buf[0..new_offset], strtab.payload.?);
+ @memcpy(buf[new_offset..][0..name.len], name);
+ buf[new_offset + name.len] = 0;
+
+ assert(update.action == .keep);
+ update.payload = buf;
+
+ break :blk new_offset;
+ };
+
// maybe compress .debug sections
if (options.compress_debug) {
for (self.sections[1..], sections_update[1..]) |section, *update| {
@@ -1018,7 +1129,7 @@ fn ElfFile(comptime is_64: bool) type {
if (section.section.sh_type == elf.SHT_NOBITS)
continue;
if (section.section.sh_offset < offset) {
- fatal("zig objcopy: unsuported ELF file", .{});
+ fatal("zig objcopy: unsupported ELF file", .{});
}
offset = section.section.sh_offset;
}
@@ -1134,10 +1245,100 @@ fn ElfFile(comptime is_64: bool) type {
eof_offset += @as(Elf_OffSize, @intCast(payload.len));
}
+ // --add-section
+ if (options.add_section) |add_section| {
+ var section_file = fs.cwd().openFile(add_section.file_path, .{}) catch |err|
+ fatal("unable to open '{s}': {s}", .{ add_section.file_path, @errorName(err) });
+ defer section_file.close();
+
+ const payload = try section_file.readToEndAlloc(arena.allocator(), std.math.maxInt(usize));
+
+ dest_sections[dest_section_idx] = Elf_Shdr{
+ .sh_name = user_section_name,
+ .sh_type = elf.SHT_PROGBITS,
+ .sh_flags = 0,
+ .sh_addr = 0,
+ .sh_offset = eof_offset,
+ .sh_size = @intCast(payload.len),
+ .sh_link = elf.SHN_UNDEF,
+ .sh_info = elf.SHN_UNDEF,
+ .sh_addralign = 4,
+ .sh_entsize = 0,
+ };
+ dest_section_idx += 1;
+
+ cmdbuf.appendAssumeCapacity(.{ .write_data = .{ .data = payload, .out_offset = eof_offset } });
+ eof_offset += @as(Elf_OffSize, @intCast(payload.len));
+ }
+
assert(dest_section_idx == new_shnum);
break :blk dest_sections;
};
+ // --set-section-alignment: overwrite alignment
+ if (options.set_section_alignment) |set_align| {
+ if (self.raw_elf_header.e_shstrndx == elf.SHN_UNDEF)
+ fatal("zig objcopy: no strtab, cannot add the user section", .{}); // TODO add the section if needed?
+
+ const strtab = §ions_update[self.raw_elf_header.e_shstrndx];
+ for (updated_section_header) |*section| {
+ const section_name = std.mem.span(@as([*:0]const u8, @ptrCast(&strtab.payload.?[section.sh_name])));
+ if (std.mem.eql(u8, section_name, set_align.section_name)) {
+ section.sh_addralign = set_align.alignment;
+ break;
+ }
+ } else std.log.warn("Skipping --set-section-alignment. Section '{s}' not found", .{set_align.section_name});
+ }
+
+ // --set-section-flags: overwrite flags
+ if (options.set_section_flags) |set_flags| {
+ if (self.raw_elf_header.e_shstrndx == elf.SHN_UNDEF)
+ fatal("zig objcopy: no strtab, cannot add the user section", .{}); // TODO add the section if needed?
+
+ const strtab = §ions_update[self.raw_elf_header.e_shstrndx];
+ for (updated_section_header) |*section| {
+ const section_name = std.mem.span(@as([*:0]const u8, @ptrCast(&strtab.payload.?[section.sh_name])));
+ if (std.mem.eql(u8, section_name, set_flags.section_name)) {
+ section.sh_flags = std.elf.SHF_WRITE; // default is writable cleared by "readonly"
+ const f = set_flags.flags;
+
+ // Supporting a subset of GNU and LLVM objcopy for ELF only
+ // GNU:
+ // alloc: add SHF_ALLOC
+ // contents: if section is SHT_NOBITS, set SHT_PROGBITS, otherwise do nothing
+ // load: if section is SHT_NOBITS, set SHT_PROGBITS, otherwise do nothing (same as contents)
+ // noload: not ELF relevant
+ // readonly: clear default SHF_WRITE flag
+ // code: add SHF_EXECINSTR
+ // data: not ELF relevant
+ // rom: ignored
+ // exclude: add SHF_EXCLUDE
+ // share: not ELF relevant
+ // debug: not ELF relevant
+ // large: add SHF_X86_64_LARGE. Fatal error if target is not x86_64
+ if (f.alloc) section.sh_flags |= std.elf.SHF_ALLOC;
+ if (f.contents or f.load) {
+ if (section.sh_type == std.elf.SHT_NOBITS) section.sh_type = std.elf.SHT_PROGBITS;
+ }
+ if (f.readonly) section.sh_flags &= ~@as(@TypeOf(section.sh_type), std.elf.SHF_WRITE);
+ if (f.code) section.sh_flags |= std.elf.SHF_EXECINSTR;
+ if (f.exclude) section.sh_flags |= std.elf.SHF_EXCLUDE;
+ if (f.large) {
+ if (updated_elf_header.e_machine != std.elf.EM.X86_64)
+ fatal("zig objcopy: 'large' section flag is only supported on x86_64 targets", .{});
+ section.sh_flags |= std.elf.SHF_X86_64_LARGE;
+ }
+
+ // LLVM:
+ // merge: add SHF_MERGE
+ // strings: add SHF_STRINGS
+ if (f.merge) section.sh_flags |= std.elf.SHF_MERGE;
+ if (f.strings) section.sh_flags |= std.elf.SHF_STRINGS;
+ break;
+ }
+ } else std.log.warn("Skipping --set-section-flags. Section '{s}' not found", .{set_flags.section_name});
+ }
+
// write the section header at the tail
{
const offset = std.mem.alignForward(Elf_OffSize, eof_offset, @alignOf(Elf_Shdr));
@@ -1361,3 +1562,111 @@ const ElfFileHelper = struct {
return hasher.final();
}
};
+
+const SectionFlags = packed struct {
+ alloc: bool = false,
+ contents: bool = false,
+ load: bool = false,
+ noload: bool = false,
+ readonly: bool = false,
+ code: bool = false,
+ data: bool = false,
+ rom: bool = false,
+ exclude: bool = false,
+ shared: bool = false,
+ debug: bool = false,
+ large: bool = false,
+ merge: bool = false,
+ strings: bool = false,
+};
+
+fn parseSectionFlags(comma_separated_flags: []const u8) SectionFlags {
+ const P = struct {
+ fn parse(flags: *SectionFlags, string: []const u8) void {
+ if (string.len == 0) return;
+
+ if (std.mem.eql(u8, string, "alloc")) {
+ flags.alloc = true;
+ } else if (std.mem.eql(u8, string, "contents")) {
+ flags.contents = true;
+ } else if (std.mem.eql(u8, string, "load")) {
+ flags.load = true;
+ } else if (std.mem.eql(u8, string, "noload")) {
+ flags.noload = true;
+ } else if (std.mem.eql(u8, string, "readonly")) {
+ flags.readonly = true;
+ } else if (std.mem.eql(u8, string, "code")) {
+ flags.code = true;
+ } else if (std.mem.eql(u8, string, "data")) {
+ flags.data = true;
+ } else if (std.mem.eql(u8, string, "rom")) {
+ flags.rom = true;
+ } else if (std.mem.eql(u8, string, "exclude")) {
+ flags.exclude = true;
+ } else if (std.mem.eql(u8, string, "shared")) {
+ flags.shared = true;
+ } else if (std.mem.eql(u8, string, "debug")) {
+ flags.debug = true;
+ } else if (std.mem.eql(u8, string, "large")) {
+ flags.large = true;
+ } else if (std.mem.eql(u8, string, "merge")) {
+ flags.merge = true;
+ } else if (std.mem.eql(u8, string, "strings")) {
+ flags.strings = true;
+ } else {
+ std.log.warn("Skipping unrecognized section flag '{s}'", .{string});
+ }
+ }
+ };
+
+ var flags = SectionFlags{};
+ var offset: usize = 0;
+ for (comma_separated_flags, 0..) |c, i| {
+ if (c == ',') {
+ defer offset = i + 1;
+ const string = comma_separated_flags[offset..i];
+ P.parse(&flags, string);
+ }
+ }
+ P.parse(&flags, comma_separated_flags[offset..]);
+ return flags;
+}
+
+test "Parse section flags" {
+ const F = SectionFlags;
+ try std.testing.expectEqual(F{}, parseSectionFlags(""));
+ try std.testing.expectEqual(F{}, parseSectionFlags(","));
+ try std.testing.expectEqual(F{}, parseSectionFlags("abc"));
+ try std.testing.expectEqual(F{ .alloc = true }, parseSectionFlags("alloc"));
+ try std.testing.expectEqual(F{ .data = true }, parseSectionFlags("data,"));
+ try std.testing.expectEqual(F{ .alloc = true, .code = true }, parseSectionFlags("alloc,code"));
+ try std.testing.expectEqual(F{ .alloc = true, .code = true }, parseSectionFlags("alloc,code,not_supported"));
+}
+
+const SplitResult = struct { first: []const u8, second: []const u8 };
+
+fn splitOption(option: []const u8) ?SplitResult {
+ const separator = '=';
+ if (option.len < 3) return null; // minimum "a=b"
+ for (1..option.len - 1) |i| {
+ if (option[i] == separator) return .{
+ .first = option[0..i],
+ .second = option[i + 1 ..],
+ };
+ }
+ return null;
+}
+
+test "Split option" {
+ {
+ const split = splitOption(".abc=123");
+ try std.testing.expect(split != null);
+ try std.testing.expectEqualStrings(".abc", split.?.first);
+ try std.testing.expectEqualStrings("123", split.?.second);
+ }
+
+ try std.testing.expectEqual(null, splitOption(""));
+ try std.testing.expectEqual(null, splitOption("=abc"));
+ try std.testing.expectEqual(null, splitOption("abc="));
+ try std.testing.expectEqual(null, splitOption("abc"));
+}
diff --git a/lib/std/Build/Step/ObjCopy.zig b/lib/std/Build/Step/ObjCopy.zig
@@ -26,6 +26,50 @@ pub const Strip = enum {
debug_and_symbols,
};
+pub const SectionFlags = packed struct {
+ /// add SHF_ALLOC
+ alloc: bool = false,
+
+ /// if section is SHT_NOBITS, set SHT_PROGBITS, otherwise do nothing
+ contents: bool = false,
+
+ /// if section is SHT_NOBITS, set SHT_PROGBITS, otherwise do nothing (same as contents)
+ load: bool = false,
+
+ /// readonly: clear default SHF_WRITE flag
+ readonly: bool = false,
+
+ /// add SHF_EXECINSTR
+ code: bool = false,
+
+ /// add SHF_EXCLUDE
+ exclude: bool = false,
+
+ /// add SHF_X86_64_LARGE. Fatal error if target is not x86_64
+ large: bool = false,
+
+ /// add SHF_MERGE
+ merge: bool = false,
+
+ /// add SHF_STRINGS
+ strings: bool = false,
+};
+
+pub const AddSection = struct {
+ section_name: []const u8,
+ file_path: std.Build.LazyPath,
+};
+
+pub const SetSectionAlignment = struct {
+ section_name: []const u8,
+ alignment: u32,
+};
+
+pub const SetSectionFlags = struct {
+ section_name: []const u8,
+ flags: SectionFlags,
+};
+
step: Step,
input_file: std.Build.LazyPath,
basename: []const u8,
@@ -38,6 +82,10 @@ pad_to: ?u64,
strip: Strip,
compress_debug: bool,
+add_section: ?AddSection,
+set_section_alignment: ?SetSectionAlignment,
+set_section_flags: ?SetSectionFlags,
+
pub const Options = struct {
basename: ?[]const u8 = null,
format: ?RawFormat = null,
@@ -51,6 +99,10 @@ pub const Options = struct {
/// note: the `basename` is baked into the elf file to specify the link to the separate debug file.
/// see https://sourceware.org/gdb/onlinedocs/gdb/Separate-Debug-Files.html
extract_to_separate_file: bool = false,
+
+ add_section: ?AddSection = null,
+ set_section_alignment: ?SetSectionAlignment = null,
+ set_section_flags: ?SetSectionFlags = null,
};
pub fn create(
@@ -75,6 +127,9 @@ pub fn create(
.pad_to = options.pad_to,
.strip = options.strip,
.compress_debug = options.compress_debug,
+ .add_section = options.add_section,
+ .set_section_alignment = options.set_section_alignment,
+ .set_section_flags = options.set_section_flags,
};
input_file.addStepDependencies(&objcopy.step);
return objcopy;
@@ -155,6 +210,31 @@ fn make(step: *Step, options: Step.MakeOptions) !void {
if (objcopy.output_file_debug != null) {
try argv.appendSlice(&.{b.fmt("--extract-to={s}", .{full_dest_path_debug})});
}
+ if (objcopy.add_section) |section| {
+ try argv.append("--add-section");
+ try argv.appendSlice(&.{b.fmt("{s}={s}", .{ section.section_name, section.file_path.getPath(b) })});
+ }
+ if (objcopy.set_section_alignment) |set_align| {
+ try argv.append("--set-section-alignment");
+ try argv.appendSlice(&.{b.fmt("{s}={d}", .{ set_align.section_name, set_align.alignment })});
+ }
+ if (objcopy.set_section_flags) |set_flags| {
+ const f = set_flags.flags;
+ // trailing comma is allowed
+ try argv.append("--set-section-flags");
+ try argv.appendSlice(&.{b.fmt("{s}={s}{s}{s}{s}{s}{s}{s}{s}{s}", .{
+ set_flags.section_name,
+ if (f.alloc) "alloc," else "",
+ if (f.contents) "contents," else "",
+ if (f.load) "load," else "",
+ if (f.readonly) "readonly," else "",
+ if (f.code) "code," else "",
+ if (f.exclude) "exclude," else "",
+ if (f.large) "large," else "",
+ if (f.merge) "merge," else "",
+ if (f.strings) "strings," else "",
+ })});
+ }
try argv.appendSlice(&.{ full_src_path, full_dest_path });