| // Copyright 2026 The GN Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include "gn/edit_command.h" | |
| #include <iomanip> | |
| #include <sstream> | |
| #include <utility> | |
| #include <vector> | |
| #include "gn/build_file_editor.h" | |
| #include "gn/commands.h" | |
| #include "gn/edit_subcommands.h" | |
| #include "gn/filesystem_utils.h" | |
| #include "gn/setup.h" | |
| #include "gn/source_file.h" | |
| #include "gn/standard_out.h" | |
| #include "gn/value.h" | |
| namespace commands { | |
| const char kEdit[] = "edit"; | |
| const char kEdit_HelpShort[] = | |
| "edit: Edit BUILD.gn files from the command line."; | |
| const char kEdit_Help[] = | |
| "gn edit <command> <labels/patterns...>\n" | |
| "\n" | |
| " Executes a command to modify a set of targets\n" | |
| "\n" | |
| " Note: Because GN is an imperative language, it's not always entirely\n" | |
| " clear what the \"correct\" thing is to do.\n" | |
| "\n" | |
| " In cases of ambiguity (eg. conditionals), `gn edit` will leave notes\n" | |
| " in your build files instructing you what to do.\n" | |
| "\n" | |
| "Commands:\n" | |
| " add <attribute> <value(s)>\n" | |
| " Adds <value(s)> to the list attribute <attribute>.\n" | |
| " If the attribute does not exist, it is created.\n" | |
| "\n" | |
| " Example:\n" | |
| " gn edit \"add deps //base //src/tools:utils\" //src/tools:*\n" | |
| "\n" | |
| " delete\n" | |
| " Deletes the matched targets entirely.\n" | |
| "\n" | |
| " Example:\n" | |
| " gn edit \"delete\" //src/tools:old_target\n" | |
| "\n" | |
| " move <from_attribute> <to_attribute> <value(s)>\n" | |
| " Moves <value(s)> from the list <from_attribute> to <to_attribute>.\n" | |
| " If <to_attribute> does not exist, it is created.\n" | |
| "\n" | |
| " Example:\n" | |
| " gn edit \"move deps public_deps //base\" //src/tools:*\n" | |
| "\n" | |
| " new <rule_kind> [(before|after) <relative_rule_name>]\n" | |
| " Adds a new rule at the end of the BUILD file (or before/after\n" | |
| " <relative_rule_name>). The rule name is determined by the target " | |
| "label.\n" | |
| "\n" | |
| " Examples:\n" | |
| " gn edit \"new source_set\" //src/tools:my_target\n" | |
| " gn edit \"new static_library before old_target\" " | |
| "//src/tools:helper\n" | |
| "\n" | |
| " remove <attribute>\n" | |
| " Removes <attribute> entirely.\n" | |
| "\n" | |
| " Example:\n" | |
| " gn edit \"remove testonly\" //src/tools:*\n" | |
| "\n" | |
| " remove <attribute> <value(s)>\n" | |
| " Removes <value(s)> from the list attribute <attribute>.\n" | |
| "\n" | |
| " Example:\n" | |
| " gn edit \"remove deps //base\" //src/tools:*\n" | |
| "\n" | |
| " rename <from_attribute> <to_attribute>\n" | |
| " Renames <from_attribute> to <to_attribute>.\n" | |
| "\n" | |
| " Example:\n" | |
| " gn edit \"rename srcs sources\" //src/tools:*\n" | |
| "\n" | |
| " set <attribute>[:list] <value(s)>\n" | |
| " Sets or overwrites the target's <attribute> to <value(s)>.\n" | |
| " If multiple values are provided, or if the \":list\" suffix is\n" | |
| " appended to the attribute, <value(s)> is interpreted as a list.\n" | |
| "\n" | |
| " Examples:\n" | |
| " gn edit \"set testonly true\" //src/tools:*\n" | |
| " gn edit \"set srcs:list foo.cc\" //:foo\n" | |
| " gn edit \"set deps :bar :baz\" //:foo\n" | |
| "\n" | |
| " shard [sharded_target_type] [group_type]\n" | |
| " Splits the target's sources into fine-grained shard targets,\n" | |
| " inheriting compilation flags, and updates the parent\n" | |
| " target to depend on the newly created shards.\n" | |
| " Note: Sharding strips existing deps and clones conditional blocks\n" | |
| " into each shard. For conditional sources (e.g. `if (is_win)`),\n" | |
| " manual cleanup is often needed, such as:\n" | |
| " * Moving the condition to wrap the shard instead of the sources\n" | |
| " * Stripping empty conditions\n" | |
| "\n" | |
| " Examples:\n" | |
| " gn edit \"shard\" //:large_target\n" | |
| " gn edit \"shard source_set static_library\" //:large_target\n"; | |
| Result<std::pair<std::vector<SourceFile>, EditState>> RunEditImpl( | |
| const std::vector<std::string>& args, | |
| Setup& setup) { | |
| if (args.size() < 2) { | |
| return Err(Location(), "Insufficient arguments.", | |
| "Usage: gn edit <command> <labels...>\n" | |
| "Example: gn edit \"set testonly true\" //foo:*"); | |
| } | |
| // We use std::quoted to tokenize the command. | |
| // eg. set foo "bar baz" -> ["set", "foo", "bar baz"]. | |
| std::stringstream ss(args[0]); | |
| std::vector<std::string> command_tokens; | |
| std::string token; | |
| while (ss >> std::ws && !ss.eof()) { | |
| if (!(ss >> std::quoted(token))) { | |
| return Err(Location(), "Unclosed quote in command string."); | |
| } | |
| command_tokens.push_back(std::move(token)); | |
| } | |
| if (command_tokens.empty()) { | |
| return Err(Location(), "Empty command string."); | |
| } | |
| ASSIGN_OR_RETURN(EditCommand command, | |
| ParseCommand(std::move(command_tokens))); | |
| const SourceDir current_dir = | |
| SourceDirForCurrentDirectory(setup.build_settings().root_path()); | |
| const std::string source_root = setup.build_settings().root_path_utf8(); | |
| std::vector<LabelPattern> patterns; | |
| for (size_t i = 1; i < args.size(); ++i) { | |
| Value val(nullptr, args[i]); | |
| Err err; | |
| LabelPattern pattern = | |
| LabelPattern::GetPattern(current_dir, source_root, val, &err); | |
| if (err.has_error()) { | |
| return err; | |
| } | |
| patterns.push_back(std::move(pattern)); | |
| } | |
| ASSIGN_OR_RETURN(std::vector<BuildFile> build_files, | |
| ::ResolvePatternsToBuildFiles(&setup.build_settings(), | |
| setup.loader(), patterns)); | |
| EditState state(args[0]); | |
| for (auto& build_file : build_files) { | |
| RETURN_IF_ERROR(command(build_file, state)); | |
| RETURN_IF_ERROR(build_file.label_matcher().done()); | |
| } | |
| std::vector<SourceFile> modified_files; | |
| for (auto& build_file : build_files) { | |
| ASSIGN_OR_RETURN(bool wrote, build_file.Write()); | |
| if (wrote) { | |
| modified_files.push_back(build_file.source_file()); | |
| } | |
| } | |
| return std::make_pair(std::move(modified_files), std::move(state)); | |
| } | |
| int RunEdit(const std::vector<std::string>& args) { | |
| Setup setup; | |
| if (!setup.DoSetupForEditing()) { | |
| return 1; | |
| } | |
| auto result = RunEditImpl(args, setup); | |
| if (result.has_error()) { | |
| result.error().PrintToStdout(); | |
| return 1; | |
| } | |
| for (const auto& file : result->first) { | |
| OutputString("Wrote '" + file.value() + "'.\n"); | |
| } | |
| for (const auto& warning : result->second.warnings) { | |
| warning.PrintNonfatalToStdout(); | |
| } | |
| const auto& review_needed = result->second.needs_manual_review; | |
| if (!review_needed.empty()) { | |
| OutputString("\nThe following targets need manual review:\n", | |
| DECORATION_YELLOW); | |
| for (const Label& label : review_needed) { | |
| OutputString("* "); | |
| OutputString(label.GetUserVisibleName(false) + "\n", DECORATION_GREEN); | |
| } | |
| OutputString( | |
| "\nWhere manual review is required, comments have been added to the " | |
| "build file of the form:\n'# TODO(gn edit: <command>): ...'\n", | |
| DECORATION_DIM); | |
| } | |
| const auto& needs_fix_deps = result->second.needs_fix_deps; | |
| if (!needs_fix_deps.empty()) { | |
| OutputString( | |
| "\nTo automatically resolve and populate dependencies for sharded " | |
| "targets, run:\n", | |
| DECORATION_YELLOW); | |
| std::string check_cmd = "gn check <out_dir>"; | |
| for (const Label& label : needs_fix_deps) { | |
| check_cmd += " " + label.GetUserVisibleName(false); | |
| } | |
| check_cmd += " --fix\n"; | |
| OutputString(check_cmd, DECORATION_GREEN); | |
| } | |
| return 0; | |
| } | |
| } // namespace commands |