//! To get started, run this tool with no args and read the help message. //! //! Clang has a file "options.td" which describes all of its command line parameter options. //! When using `zig cc`, Zig acts as a proxy between the user and Clang. It does not need //! to understand all the parameters, but it does need to understand some of them, such as //! the target. This means that Zig must understand when a C command line parameter expects //! to "consume" the next parameter on the command line. //! //! For example, `-z -target` would mean to pass `-target` to the linker, whereas `-E -target` //! would mean that the next parameter specifies the target. const std = @import("std"); const fs = std.fs; const assert = std.debug.assert; const json = std.json; const KnownOpt = struct { name: []const u8, /// Corresponds to stage.zig ClangArgIterator.Kind ident: []const u8, }; const known_options = [_]KnownOpt{ .{ .name = "target", .ident = "target", }, .{ .name = "o", .ident = "o", }, .{ .name = "c", .ident = "c", }, .{ .name = "l", .ident = "l", }, .{ .name = "pipe", .ident = "ignore", }, .{ .name = "help", .ident = "driver_punt", }, .{ .name = "fPIC", .ident = "pic", }, .{ .name = "fno-PIC", .ident = "no_pic", }, .{ .name = "nolibc", .ident = "nostdlib", }, .{ .name = "nostdlib", .ident = "nostdlib", }, .{ .name = "no-standard-libraries", .ident = "nostdlib", }, .{ .name = "nostdlib++", .ident = "nostdlib_cpp", }, .{ .name = "nostdinc++", .ident = "nostdlib_cpp", }, .{ .name = "nostdlibinc", .ident = "nostdlibinc", }, .{ .name = "nostdinc", .ident = "nostdlibinc", }, .{ .name = "no-standard-includes", .ident = "nostdlibinc", }, .{ .name = "shared", .ident = "shared", }, .{ .name = "rdynamic", .ident = "rdynamic", }, .{ .name = "Wl,", .ident = "wl", }, .{ .name = "Xlinker", .ident = "for_linker", }, .{ .name = "for-linker", .ident = "for_linker", }, .{ .name = "for-linker=", .ident = "for_linker", }, .{ .name = "z", .ident = "linker_input_z", }, .{ .name = "E", .ident = "preprocess_only", }, .{ .name = "preprocess", .ident = "preprocess_only", }, .{ .name = "S", .ident = "asm_only", }, .{ .name = "assemble", .ident = "asm_only", }, .{ .name = "O0", .ident = "optimize", }, .{ .name = "O1", .ident = "optimize", }, .{ .name = "O2", .ident = "optimize", }, // O3 is only detected from the joined "-O" option .{ .name = "O4", .ident = "optimize", }, .{ .name = "Og", .ident = "optimize", }, .{ .name = "Os", .ident = "optimize", }, // Oz is only detected from the joined "-O" option .{ .name = "O", .ident = "optimize", }, .{ .name = "Ofast", .ident = "optimize", }, .{ .name = "optimize", .ident = "optimize", }, .{ .name = "g1", .ident = "debug", }, .{ .name = "gline-tables-only", .ident = "debug", }, .{ .name = "g", .ident = "debug", }, .{ .name = "debug", .ident = "debug", }, .{ .name = "g-dwarf", .ident = "debug", }, .{ .name = "g-dwarf-2", .ident = "debug", }, .{ .name = "g-dwarf-3", .ident = "debug", }, .{ .name = "g-dwarf-4", .ident = "debug", }, .{ .name = "g-dwarf-5", .ident = "debug", }, .{ .name = "fsanitize", .ident = "sanitize", }, .{ .name = "T", .ident = "linker_script", }, .{ .name = "###", .ident = "verbose_cmds", }, .{ .name = "L", .ident = "lib_dir", }, .{ .name = "library-directory", .ident = "lib_dir", }, .{ .name = "mcpu", .ident = "mcpu", }, .{ .name = "march", .ident = "mcpu", }, .{ .name = "mtune", .ident = "mcpu", }, .{ .name = "MD", .ident = "dep_file", }, .{ .name = "MV", .ident = "dep_file", }, .{ .name = "MF", .ident = "dep_file", }, .{ .name = "MT", .ident = "dep_file", }, .{ .name = "MG", .ident = "dep_file", }, .{ .name = "MJ", .ident = "dep_file", }, .{ .name = "MM", .ident = "dep_file", }, .{ .name = "MMD", .ident = "dep_file", }, .{ .name = "MP", .ident = "dep_file", }, .{ .name = "MQ", .ident = "dep_file", }, .{ .name = "F", .ident = "framework_dir", }, .{ .name = "framework", .ident = "framework", }, }; const blacklisted_options = [_][]const u8{}; fn knownOption(name: []const u8) ?[]const u8 { const chopped_name = if (std.mem.endsWith(u8, name, "=")) name[0 .. name.len - 1] else name; for (known_options) |item| { if (std.mem.eql(u8, chopped_name, item.name)) { return item.ident; } } return null; } pub fn main() anyerror!void { var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); defer arena.deinit(); const allocator = &arena.allocator; const args = try std.process.argsAlloc(allocator); if (args.len <= 1) { usageAndExit(std.io.getStdErr(), args[0], 1); } if (std.mem.eql(u8, args[1], "--help")) { usageAndExit(std.io.getStdOut(), args[0], 0); } if (args.len < 3) { usageAndExit(std.io.getStdErr(), args[0], 1); } const llvm_tblgen_exe = args[1]; if (std.mem.startsWith(u8, llvm_tblgen_exe, "-")) { usageAndExit(std.io.getStdErr(), args[0], 1); } const llvm_src_root = args[2]; if (std.mem.startsWith(u8, llvm_src_root, "-")) { usageAndExit(std.io.getStdErr(), args[0], 1); } const child_args = [_][]const u8{ llvm_tblgen_exe, "--dump-json", try std.fmt.allocPrint(allocator, "{}/clang/include/clang/Driver/Options.td", .{llvm_src_root}), try std.fmt.allocPrint(allocator, "-I={}/llvm/include", .{llvm_src_root}), try std.fmt.allocPrint(allocator, "-I={}/clang/include/clang/Driver", .{llvm_src_root}), }; const child_result = try std.ChildProcess.exec(.{ .allocator = allocator, .argv = &child_args, .max_output_bytes = 100 * 1024 * 1024, }); std.debug.warn("{}\n", .{child_result.stderr}); const json_text = switch (child_result.term) { .Exited => |code| if (code == 0) child_result.stdout else { std.debug.warn("llvm-tblgen exited with code {}\n", .{code}); std.process.exit(1); }, else => { std.debug.warn("llvm-tblgen crashed\n", .{}); std.process.exit(1); }, }; var parser = json.Parser.init(allocator, false); const tree = try parser.parse(json_text); const root_map = &tree.root.Object; var all_objects = std.ArrayList(*json.ObjectMap).init(allocator); { var it = root_map.iterator(); it_map: while (it.next()) |kv| { if (kv.key.len == 0) continue; if (kv.key[0] == '!') continue; if (kv.value != .Object) continue; if (!kv.value.Object.contains("NumArgs")) continue; if (!kv.value.Object.contains("Name")) continue; for (blacklisted_options) |blacklisted_key| { if (std.mem.eql(u8, blacklisted_key, kv.key)) continue :it_map; } if (kv.value.Object.get("Name").?.String.len == 0) continue; try all_objects.append(&kv.value.Object); } } // Some options have multiple matches. As an example, "-Wl,foo" matches both // "W" and "Wl,". So we sort this list in order of descending priority. std.sort.sort(*json.ObjectMap, all_objects.items, {}, objectLessThan); var stdout_bos = std.io.bufferedOutStream(std.io.getStdOut().outStream()); const stdout = stdout_bos.outStream(); try stdout.writeAll( \\// This file is generated by tools/update_clang_options.zig. \\// zig fmt: off \\usingnamespace @import("clang_options.zig"); \\pub const data = blk: { @setEvalBranchQuota(6000); break :blk &[_]CliArg{ \\ ); for (all_objects.items) |obj| { const name = obj.get("Name").?.String; var pd1 = false; var pd2 = false; var pslash = false; for (obj.get("Prefixes").?.Array.items) |prefix_json| { const prefix = prefix_json.String; if (std.mem.eql(u8, prefix, "-")) { pd1 = true; } else if (std.mem.eql(u8, prefix, "--")) { pd2 = true; } else if (std.mem.eql(u8, prefix, "/")) { pslash = true; } else { std.debug.warn("{} has unrecognized prefix '{}'\n", .{ name, prefix }); std.process.exit(1); } } const syntax = objSyntax(obj); if (std.mem.eql(u8, name, "MT") and syntax == .flag) { // `-MT foo` is ambiguous because there is also an -MT flag // The canonical way to specify the flag is with `/MT` and so we make this // the only way. try stdout.print("flagpsl(\"{}\"),\n", .{name}); } else if (knownOption(name)) |ident| { // Workaround the fact that in 'Options.td' -Ofast is listed as 'joined' const final_syntax = if (std.mem.eql(u8, name, "Ofast")) .flag else syntax; try stdout.print( \\.{{ \\ .name = "{}", \\ .syntax = {}, \\ .zig_equivalent = .{}, \\ .pd1 = {}, \\ .pd2 = {}, \\ .psl = {}, \\}}, \\ , .{ name, final_syntax, ident, pd1, pd2, pslash }); } else if (pd1 and !pd2 and !pslash and syntax == .flag) { try stdout.print("flagpd1(\"{}\"),\n", .{name}); } else if (!pd1 and !pd2 and pslash and syntax == .flag) { try stdout.print("flagpsl(\"{}\"),\n", .{name}); } else if (pd1 and !pd2 and !pslash and syntax == .joined) { try stdout.print("joinpd1(\"{}\"),\n", .{name}); } else if (pd1 and !pd2 and !pslash and syntax == .joined_or_separate) { try stdout.print("jspd1(\"{}\"),\n", .{name}); } else if (pd1 and !pd2 and !pslash and syntax == .separate) { try stdout.print("sepd1(\"{}\"),\n", .{name}); } else { try stdout.print( \\.{{ \\ .name = "{}", \\ .syntax = {}, \\ .zig_equivalent = .other, \\ .pd1 = {}, \\ .pd2 = {}, \\ .psl = {}, \\}}, \\ , .{ name, syntax, pd1, pd2, pslash }); } } try stdout.writeAll( \\};}; \\ ); try stdout_bos.flush(); } // TODO we should be able to import clang_options.zig but currently this is problematic because it will // import stage2.zig and that causes a bunch of stuff to get exported const Syntax = union(enum) { /// A flag with no values. flag, /// An option which prefixes its (single) value. joined, /// An option which is followed by its value. separate, /// An option which is either joined to its (non-empty) value, or followed by its value. joined_or_separate, /// An option which is both joined to its (first) value, and followed by its (second) value. joined_and_separate, /// An option followed by its values, which are separated by commas. comma_joined, /// An option which consumes an optional joined argument and any other remaining arguments. remaining_args_joined, /// An option which is which takes multiple (separate) arguments. multi_arg: u8, pub fn format( self: Syntax, comptime fmt: []const u8, options: std.fmt.FormatOptions, out_stream: anytype, ) !void { switch (self) { .multi_arg => |n| return out_stream.print(".{{.{}={}}}", .{ @tagName(self), n }), else => return out_stream.print(".{}", .{@tagName(self)}), } } }; fn objSyntax(obj: *json.ObjectMap) Syntax { const num_args = @intCast(u8, obj.get("NumArgs").?.Integer); for (obj.get("!superclasses").?.Array.items) |superclass_json| { const superclass = superclass_json.String; if (std.mem.eql(u8, superclass, "Joined")) { return .joined; } else if (std.mem.eql(u8, superclass, "CLJoined")) { return .joined; } else if (std.mem.eql(u8, superclass, "CLIgnoredJoined")) { return .joined; } else if (std.mem.eql(u8, superclass, "CLCompileJoined")) { return .joined; } else if (std.mem.eql(u8, superclass, "JoinedOrSeparate")) { return .joined_or_separate; } else if (std.mem.eql(u8, superclass, "CLJoinedOrSeparate")) { return .joined_or_separate; } else if (std.mem.eql(u8, superclass, "CLCompileJoinedOrSeparate")) { return .joined_or_separate; } else if (std.mem.eql(u8, superclass, "Flag")) { return .flag; } else if (std.mem.eql(u8, superclass, "CLFlag")) { return .flag; } else if (std.mem.eql(u8, superclass, "CLIgnoredFlag")) { return .flag; } else if (std.mem.eql(u8, superclass, "Separate")) { return .separate; } else if (std.mem.eql(u8, superclass, "JoinedAndSeparate")) { return .joined_and_separate; } else if (std.mem.eql(u8, superclass, "CommaJoined")) { return .comma_joined; } else if (std.mem.eql(u8, superclass, "CLRemainingArgsJoined")) { return .remaining_args_joined; } else if (std.mem.eql(u8, superclass, "MultiArg")) { return .{ .multi_arg = num_args }; } } const name = obj.get("Name").?.String; if (std.mem.eql(u8, name, "")) { return .flag; } else if (std.mem.eql(u8, name, "")) { return .flag; } const kind_def = obj.get("Kind").?.Object.get("def").?.String; if (std.mem.eql(u8, kind_def, "KIND_FLAG")) { return .flag; } const key = obj.get("!name").?.String; std.debug.warn("{} (key {}) has unrecognized superclasses:\n", .{ name, key }); for (obj.get("!superclasses").?.Array.items) |superclass_json| { std.debug.warn(" {}\n", .{superclass_json.String}); } std.process.exit(1); } fn syntaxMatchesWithEql(syntax: Syntax) bool { return switch (syntax) { .flag, .separate, .multi_arg, => true, .joined, .joined_or_separate, .joined_and_separate, .comma_joined, .remaining_args_joined, => false, }; } fn objectLessThan(context: void, a: *json.ObjectMap, b: *json.ObjectMap) bool { // Priority is determined by exact matches first, followed by prefix matches in descending // length, with key as a final tiebreaker. const a_syntax = objSyntax(a); const b_syntax = objSyntax(b); const a_match_with_eql = syntaxMatchesWithEql(a_syntax); const b_match_with_eql = syntaxMatchesWithEql(b_syntax); if (a_match_with_eql and !b_match_with_eql) { return true; } else if (!a_match_with_eql and b_match_with_eql) { return false; } if (!a_match_with_eql and !b_match_with_eql) { const a_name = a.get("Name").?.String; const b_name = b.get("Name").?.String; if (a_name.len != b_name.len) { return a_name.len > b_name.len; } } const a_key = a.get("!name").?.String; const b_key = b.get("!name").?.String; return std.mem.lessThan(u8, a_key, b_key); } fn usageAndExit(file: fs.File, arg0: []const u8, code: u8) noreturn { file.outStream().print( \\Usage: {} /path/to/llvm-tblgen /path/to/git/llvm/llvm-project \\Alternative Usage: zig run /path/to/git/zig/tools/update_clang_options.zig -- /path/to/llvm-tblgen /path/to/git/llvm/llvm-project \\ \\Prints to stdout Zig code which you can use to replace the file src/clang_options_data.zig. \\ , .{arg0}) catch std.process.exit(1); std.process.exit(code); }