| // Copyright 2016 The Chromium 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/analyzer.h" | |
| #include <algorithm> | |
| #include <iterator> | |
| #include <memory> | |
| #include <set> | |
| #include <vector> | |
| #include "base/json/json_reader.h" | |
| #include "base/json/json_writer.h" | |
| #include "base/strings/string_util.h" | |
| #include "base/values.h" | |
| #include "gn/builder.h" | |
| #include "gn/config.h" | |
| #include "gn/config_values_extractors.h" | |
| #include "gn/deps_iterator.h" | |
| #include "gn/err.h" | |
| #include "gn/filesystem_utils.h" | |
| #include "gn/loader.h" | |
| #include "gn/location.h" | |
| #include "gn/pool.h" | |
| #include "gn/source_file.h" | |
| #include "gn/target.h" | |
| namespace { | |
| struct Inputs { | |
| std::vector<SourceFile> source_vec; | |
| std::vector<Label> compile_vec; | |
| std::vector<Label> test_vec; | |
| bool compile_included_all = false; | |
| std::set<const SourceFile*> source_files; | |
| std::set<Label> compile_labels; | |
| std::set<Label> test_labels; | |
| }; | |
| struct Outputs { | |
| std::string status; | |
| std::string error; | |
| bool compile_includes_all = false; | |
| std::set<Label> compile_labels; | |
| std::set<Label> test_labels; | |
| std::set<Label> invalid_labels; | |
| }; | |
| std::set<Label> LabelsFor(const TargetSet& targets) { | |
| std::set<Label> labels; | |
| for (auto* target : targets) | |
| labels.insert(target->label()); | |
| return labels; | |
| } | |
| TargetSet Intersect(const TargetSet& l, const TargetSet& r) { | |
| return l.intersection_with(r); | |
| } | |
| std::vector<std::string> GetStringVector(const base::DictionaryValue& dict, | |
| const std::string& key, | |
| Err* err) { | |
| std::vector<std::string> strings; | |
| const base::ListValue* lst; | |
| bool ret = dict.GetList(key, &lst); | |
| if (!ret) { | |
| *err = Err(Location(), "Input does not have a key named \"" + key + | |
| "\" with a list value."); | |
| return strings; | |
| } | |
| for (size_t i = 0; i < lst->GetSize(); i++) { | |
| std::string s; | |
| ret = lst->GetString(i, &s); | |
| if (!ret) { | |
| *err = Err(Location(), "Item " + std::to_string(i) + " of \"" + key + | |
| "\" is not a string."); | |
| strings.clear(); | |
| return strings; | |
| } | |
| strings.push_back(std::move(s)); | |
| } | |
| *err = Err(); | |
| return strings; | |
| } | |
| void WriteString(base::DictionaryValue& dict, | |
| const std::string& key, | |
| const std::string& value) { | |
| dict.SetKey(key, base::Value(value)); | |
| } | |
| void WriteLabels(const Label& default_toolchain, | |
| base::DictionaryValue& dict, | |
| const std::string& key, | |
| const std::set<Label>& labels) { | |
| std::vector<std::string> strings; | |
| auto value = std::make_unique<base::ListValue>(); | |
| for (const auto& l : labels) | |
| strings.push_back(l.GetUserVisibleName(default_toolchain)); | |
| std::sort(strings.begin(), strings.end()); | |
| value->AppendStrings(strings); | |
| dict.SetWithoutPathExpansion(key, std::move(value)); | |
| } | |
| Label AbsoluteOrSourceAbsoluteStringToLabel(const Label& default_toolchain, | |
| const std::string& s, | |
| Err* err) { | |
| if (!IsPathSourceAbsolute(s) && !IsPathAbsolute(s)) { | |
| *err = Err(Location(), | |
| "\"" + s + "\" is not a source-absolute or absolute path."); | |
| return Label(); | |
| } | |
| return Label::Resolve(SourceDir("//"), std::string_view(), default_toolchain, | |
| Value(nullptr, s), err); | |
| } | |
| Err JSONToInputs(const Label& default_toolchain, | |
| const std::string input, | |
| Inputs* inputs) { | |
| int error_code_out; | |
| std::string error_msg_out; | |
| int error_line_out; | |
| int error_column_out; | |
| std::unique_ptr<base::Value> value = base::JSONReader::ReadAndReturnError( | |
| input, base::JSONParserOptions::JSON_PARSE_RFC, &error_code_out, | |
| &error_msg_out, &error_line_out, &error_column_out); | |
| if (!value) | |
| return Err(Location(), "Input is not valid JSON:" + error_msg_out); | |
| const base::DictionaryValue* dict; | |
| if (!value->GetAsDictionary(&dict)) | |
| return Err(Location(), "Input is not a dictionary."); | |
| Err err; | |
| const char kFilesKey[] = "files"; | |
| { | |
| std::vector<std::string> files = GetStringVector(*dict, kFilesKey, &err); | |
| if (err.has_error()) | |
| return err; | |
| for (auto& s : files) { | |
| if (!IsPathSourceAbsolute(s) && !IsPathAbsolute(s)) { | |
| return Err(Location(), | |
| "\"" + s + "\" is not a source-absolute or absolute path."); | |
| } | |
| inputs->source_vec.emplace_back(std::move(s)); | |
| } | |
| } | |
| inputs->compile_included_all = false; | |
| const char kAdditonalCompileTargetsKey[] = "additional_compile_targets"; | |
| if (dict->HasKey(kAdditonalCompileTargetsKey)) { | |
| std::vector<std::string> additional_compile_targets = | |
| GetStringVector(*dict, kAdditonalCompileTargetsKey, &err); | |
| if (err.has_error()) | |
| return err; | |
| for (auto& s : additional_compile_targets) { | |
| if (s == "all") { | |
| inputs->compile_included_all = true; | |
| } else { | |
| inputs->compile_vec.push_back( | |
| AbsoluteOrSourceAbsoluteStringToLabel(default_toolchain, s, &err)); | |
| if (err.has_error()) | |
| return err; | |
| } | |
| } | |
| } | |
| const char kTestTargetsKey[] = "test_targets"; | |
| { | |
| std::vector<std::string> test_targets = | |
| GetStringVector(*dict, kTestTargetsKey, &err); | |
| if (err.has_error()) | |
| return err; | |
| for (auto& s : test_targets) { | |
| inputs->test_vec.push_back( | |
| AbsoluteOrSourceAbsoluteStringToLabel(default_toolchain, s, &err)); | |
| if (err.has_error()) | |
| return err; | |
| } | |
| } | |
| for (const auto kv : dict->DictItems()) { | |
| if (kv.first == kFilesKey || kv.first == kAdditonalCompileTargetsKey || | |
| kv.first == kTestTargetsKey) { | |
| continue; | |
| } | |
| return Err(Location(), "Unknown analyze input key \"" + kv.first + "\"."); | |
| } | |
| for (auto& s : inputs->source_vec) | |
| inputs->source_files.insert(&s); | |
| for (auto& l : inputs->compile_vec) | |
| inputs->compile_labels.insert(l); | |
| for (auto& l : inputs->test_vec) | |
| inputs->test_labels.insert(l); | |
| return Err(); | |
| } | |
| std::string OutputsToJSON(const Outputs& outputs, | |
| const Label& default_toolchain, | |
| Err* err) { | |
| std::string output; | |
| auto value = std::make_unique<base::DictionaryValue>(); | |
| if (outputs.error.size()) { | |
| WriteString(*value, "error", outputs.error); | |
| WriteLabels(default_toolchain, *value, "invalid_targets", | |
| outputs.invalid_labels); | |
| } else { | |
| WriteString(*value, "status", outputs.status); | |
| if (outputs.compile_includes_all) { | |
| auto compile_targets = std::make_unique<base::ListValue>(); | |
| compile_targets->AppendString("all"); | |
| value->SetWithoutPathExpansion("compile_targets", | |
| std::move(compile_targets)); | |
| } else { | |
| WriteLabels(default_toolchain, *value, "compile_targets", | |
| outputs.compile_labels); | |
| } | |
| WriteLabels(default_toolchain, *value, "test_targets", outputs.test_labels); | |
| } | |
| if (!base::JSONWriter::Write(*value.get(), &output)) | |
| *err = Err(Location(), "Failed to marshal JSON value for output"); | |
| return output; | |
| } | |
| } // namespace | |
| Analyzer::Analyzer(const Builder& builder, | |
| const SourceFile& build_config_file, | |
| const SourceFile& dot_file, | |
| const SourceFileSet& build_args_dependency_files) | |
| : all_items_(builder.GetAllResolvedItems()), | |
| default_toolchain_(builder.loader()->GetDefaultToolchain()), | |
| build_config_file_(build_config_file), | |
| dot_file_(dot_file), | |
| build_args_dependency_files_(build_args_dependency_files) { | |
| for (const auto* item : all_items_) { | |
| labels_to_items_[item->label()] = item; | |
| // Fill dep_map_. | |
| if (item->AsTarget()) { | |
| for (const auto& dep_target_pair : | |
| item->AsTarget()->GetDeps(Target::DEPS_ALL)) | |
| dep_map_.insert(std::make_pair(dep_target_pair.ptr, item)); | |
| for (const auto& validation_target_pair : item->AsTarget()->validations()) | |
| dep_map_.insert(std::make_pair(validation_target_pair.ptr, item)); | |
| for (const auto& dep_config_pair : item->AsTarget()->configs()) | |
| dep_map_.insert(std::make_pair(dep_config_pair.ptr, item)); | |
| dep_map_.insert(std::make_pair(item->AsTarget()->toolchain(), item)); | |
| if (item->AsTarget()->IsBinary() || | |
| item->AsTarget()->output_type() == Target::ACTION || | |
| item->AsTarget()->output_type() == Target::ACTION_FOREACH) { | |
| const LabelPtrPair<Pool>& pool = item->AsTarget()->pool(); | |
| if (pool.ptr) | |
| dep_map_.insert(std::make_pair(pool.ptr, item)); | |
| } | |
| } else if (item->AsConfig()) { | |
| for (const auto& dep_config_pair : item->AsConfig()->configs()) | |
| dep_map_.insert(std::make_pair(dep_config_pair.ptr, item)); | |
| } else if (item->AsToolchain()) { | |
| for (const auto& dep_pair : item->AsToolchain()->deps()) | |
| dep_map_.insert(std::make_pair(dep_pair.ptr, item)); | |
| } else { | |
| DCHECK(item->AsPool()); | |
| } | |
| } | |
| } | |
| Analyzer::~Analyzer() = default; | |
| std::string Analyzer::Analyze(const std::string& input, Err* err) const { | |
| Inputs inputs; | |
| Outputs outputs; | |
| Err local_err = JSONToInputs(default_toolchain_, input, &inputs); | |
| if (local_err.has_error()) { | |
| outputs.error = local_err.message(); | |
| return OutputsToJSON(outputs, default_toolchain_, err); | |
| } | |
| std::set<Label> invalid_labels; | |
| for (const auto& label : InvalidLabels(inputs.compile_labels)) | |
| invalid_labels.insert(label); | |
| for (const auto& label : InvalidLabels(inputs.test_labels)) | |
| invalid_labels.insert(label); | |
| if (!invalid_labels.empty()) { | |
| outputs.error = "Invalid targets"; | |
| outputs.invalid_labels = invalid_labels; | |
| return OutputsToJSON(outputs, default_toolchain_, err); | |
| } | |
| if (WereMainGNFilesModified(inputs.source_files)) { | |
| outputs.status = "Found dependency (all)"; | |
| if (inputs.compile_included_all) { | |
| outputs.compile_includes_all = true; | |
| } else { | |
| outputs.compile_labels.insert(inputs.compile_labels.begin(), | |
| inputs.compile_labels.end()); | |
| outputs.compile_labels.insert(inputs.test_labels.begin(), | |
| inputs.test_labels.end()); | |
| } | |
| outputs.test_labels = inputs.test_labels; | |
| return OutputsToJSON(outputs, default_toolchain_, err); | |
| } | |
| std::set<const Item*> affected_items = | |
| GetAllAffectedItems(inputs.source_files); | |
| TargetSet affected_targets; | |
| for (const Item* affected_item : affected_items) { | |
| if (affected_item->AsTarget()) | |
| affected_targets.insert(affected_item->AsTarget()); | |
| } | |
| if (affected_targets.empty()) { | |
| outputs.status = "No dependency"; | |
| return OutputsToJSON(outputs, default_toolchain_, err); | |
| } | |
| TargetSet root_targets; | |
| for (const auto* item : all_items_) { | |
| if (item->AsTarget() && dep_map_.find(item) == dep_map_.end()) | |
| root_targets.insert(item->AsTarget()); | |
| } | |
| TargetSet compile_targets = TargetsFor(inputs.compile_labels); | |
| if (inputs.compile_included_all) { | |
| for (auto* root_target : root_targets) | |
| compile_targets.insert(root_target); | |
| } | |
| TargetSet filtered_targets = Filter(compile_targets); | |
| outputs.compile_labels = | |
| LabelsFor(Intersect(filtered_targets, affected_targets)); | |
| // If every target is affected, simply compile All instead of listing all | |
| // the targets to make the output easier to read. | |
| if (inputs.compile_included_all && | |
| outputs.compile_labels.size() == filtered_targets.size()) | |
| outputs.compile_includes_all = true; | |
| TargetSet test_targets = TargetsFor(inputs.test_labels); | |
| outputs.test_labels = LabelsFor(Intersect(test_targets, affected_targets)); | |
| if (outputs.compile_labels.empty() && outputs.test_labels.empty()) | |
| outputs.status = "No dependency"; | |
| else | |
| outputs.status = "Found dependency"; | |
| return OutputsToJSON(outputs, default_toolchain_, err); | |
| } | |
| std::set<const Item*> Analyzer::GetAllAffectedItems( | |
| const std::set<const SourceFile*>& source_files) const { | |
| std::set<const Item*> directly_affected_items; | |
| for (auto* source_file : source_files) | |
| AddItemsDirectlyReferringToFile(source_file, &directly_affected_items); | |
| std::set<const Item*> all_affected_items; | |
| for (auto* affected_item : directly_affected_items) | |
| AddAllItemsReferringToItem(affected_item, &all_affected_items); | |
| return all_affected_items; | |
| } | |
| std::set<Label> Analyzer::InvalidLabels(const std::set<Label>& labels) const { | |
| std::set<Label> invalid_labels; | |
| for (const Label& label : labels) { | |
| if (labels_to_items_.find(label) == labels_to_items_.end()) | |
| invalid_labels.insert(label); | |
| } | |
| return invalid_labels; | |
| } | |
| TargetSet Analyzer::TargetsFor(const std::set<Label>& labels) const { | |
| TargetSet targets; | |
| for (const auto& label : labels) { | |
| auto it = labels_to_items_.find(label); | |
| if (it != labels_to_items_.end()) { | |
| DCHECK(it->second->AsTarget()); | |
| targets.insert(it->second->AsTarget()); | |
| } | |
| } | |
| return targets; | |
| } | |
| TargetSet Analyzer::Filter(const TargetSet& targets) const { | |
| TargetSet seen; | |
| TargetSet filtered; | |
| for (const auto* target : targets) | |
| FilterTarget(target, &seen, &filtered); | |
| return filtered; | |
| } | |
| void Analyzer::FilterTarget(const Target* target, | |
| TargetSet* seen, | |
| TargetSet* filtered) const { | |
| if (seen->add(target)) { | |
| if (target->output_type() != Target::GROUP) { | |
| filtered->insert(target); | |
| } else { | |
| for (const auto& pair : target->GetDeps(Target::DEPS_ALL)) | |
| FilterTarget(pair.ptr, seen, filtered); | |
| for (const auto& pair : target->validations()) | |
| FilterTarget(pair.ptr, seen, filtered); | |
| } | |
| } | |
| } | |
| bool Analyzer::ItemRefersToFile(const Item* item, | |
| const SourceFile* file) const { | |
| for (const auto& cur_file : item->build_dependency_files()) { | |
| if (cur_file == *file) | |
| return true; | |
| } | |
| if (const Config* config = item->AsConfig()) { | |
| for (const auto& config_pair : config->configs()) { | |
| if (ItemRefersToFile(config_pair.ptr, file)) | |
| return true; | |
| } | |
| } | |
| if (!item->AsTarget()) | |
| return false; | |
| const Target* target = item->AsTarget(); | |
| for (const auto& cur_file : target->sources()) { | |
| if (cur_file == *file) | |
| return true; | |
| } | |
| for (const auto& cur_file : target->public_headers()) { | |
| if (cur_file == *file) | |
| return true; | |
| } | |
| for (ConfigValuesIterator iter(target); !iter.done(); iter.Next()) { | |
| for (const auto& cur_file : iter.cur().inputs()) { | |
| if (cur_file == *file) | |
| return true; | |
| } | |
| } | |
| for (const auto& cur_file : target->data()) { | |
| if (cur_file == file->value()) | |
| return true; | |
| if (cur_file.back() == '/' && file->value().starts_with(cur_file)) | |
| return true; | |
| } | |
| if (target->action_values().script().value() == file->value()) | |
| return true; | |
| std::vector<SourceFile> outputs; | |
| target->action_values().GetOutputsAsSourceFiles(target, &outputs); | |
| for (const auto& cur_file : outputs) { | |
| if (cur_file == *file) | |
| return true; | |
| } | |
| return false; | |
| } | |
| void Analyzer::AddItemsDirectlyReferringToFile( | |
| const SourceFile* file, | |
| std::set<const Item*>* directly_affected_items) const { | |
| for (const auto* item : all_items_) { | |
| if (ItemRefersToFile(item, file)) | |
| directly_affected_items->insert(item); | |
| } | |
| } | |
| void Analyzer::AddAllItemsReferringToItem( | |
| const Item* item, | |
| std::set<const Item*>* all_affected_items) const { | |
| if (all_affected_items->find(item) != all_affected_items->end()) | |
| return; // Already found this item. | |
| all_affected_items->insert(item); | |
| auto dep_begin = dep_map_.lower_bound(item); | |
| auto dep_end = dep_map_.upper_bound(item); | |
| for (auto cur_dep = dep_begin; cur_dep != dep_end; ++cur_dep) | |
| AddAllItemsReferringToItem(cur_dep->second, all_affected_items); | |
| } | |
| bool Analyzer::WereMainGNFilesModified( | |
| const std::set<const SourceFile*>& modified_files) const { | |
| for (const auto* file : modified_files) { | |
| if (*file == dot_file_) | |
| return true; | |
| if (*file == build_config_file_) | |
| return true; | |
| for (const auto& build_args_dependency_file : | |
| build_args_dependency_files_) { | |
| if (*file == build_args_dependency_file) | |
| return true; | |
| } | |
| } | |
| return false; | |
| } |