| // Copyright 2018 The Bazel Authors. All rights reserved. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| #include "src/main/cpp/rc_file.h" | |
| #include <algorithm> | |
| #include <memory> | |
| #include <optional> | |
| #include <string> | |
| #include <utility> | |
| #include <vector> | |
| #include "src/main/cpp/sem_ver.h" | |
| #include "src/main/cpp/blaze_util_platform.h" | |
| #include "src/main/cpp/util/file_platform.h" | |
| #include "src/main/cpp/util/logging.h" | |
| #include "src/main/cpp/util/strings.h" | |
| #include "src/main/cpp/workspace_layout.h" | |
| #include "absl/algorithm/container.h" | |
| #include "absl/functional/function_ref.h" | |
| #include "absl/memory/memory.h" | |
| #include "absl/strings/match.h" | |
| #include "absl/strings/str_cat.h" | |
| #include "absl/strings/str_format.h" | |
| #include "absl/strings/str_split.h" | |
| #include "absl/strings/string_view.h" | |
| #include "absl/types/span.h" | |
| #include "re2/re2.h" | |
| namespace blaze { | |
| static constexpr absl::string_view kCommandImport = "import"; | |
| static constexpr absl::string_view kCommandTryImport = "try-import"; | |
| static constexpr absl::string_view kCommandTryImportIfBazelVersion = | |
| "try-import-if-bazel-version"; | |
| // The valid operators to compare against bazel version for | |
| // kCommandTryImportIfBazelVersion. These should match the below regex. | |
| static constexpr absl::string_view kBazelVersionLt = "<"; | |
| static constexpr absl::string_view kBazelVersionLte = "<="; | |
| static constexpr absl::string_view kBazelVersionGt = ">"; | |
| static constexpr absl::string_view kBazelVersionGte = ">="; | |
| static constexpr absl::string_view kBazelVersionEq = "=="; | |
| static constexpr absl::string_view kBazelVersionNeq = "!="; | |
| static constexpr absl::string_view kBazelVersionTilde = "~"; | |
| // Regex to match the comparison operator in kCommandTryImportIfBazelVersion | |
| // statements. Eg. '>=9.0.0' | |
| const LazyRE2 kBazelVersionCmpOp = {R"((<=?|>=?|==|!=|~)(\S+))"}; | |
| /*static*/ std::unique_ptr<RcFile> RcFile::Parse( | |
| const std::string& filename, const WorkspaceLayout* workspace_layout, | |
| const std::string& workspace, const std::string& build_label, | |
| const std::optional<SemVer>& sem_ver, ParseError* error, | |
| std::string* error_text, ReadFileFn read_file, | |
| CanonicalizePathFn canonicalize_path) { | |
| auto rcfile = absl::WrapUnique(new RcFile()); | |
| std::vector<std::string> initial_import_stack = {filename}; | |
| *error = rcfile->ParseFile(filename, workspace, *workspace_layout, | |
| build_label, sem_ver, read_file, canonicalize_path, | |
| initial_import_stack, error_text); | |
| return (*error == ParseError::NONE) ? std::move(rcfile) : nullptr; | |
| } | |
| /*static*/ std::unique_ptr<RcFile> RcFile::Parse( | |
| const std::string& filename, const WorkspaceLayout* workspace_layout, | |
| const std::string& workspace, ParseError* error, std::string* error_text, | |
| ReadFileFn read_file, CanonicalizePathFn canonicalize_path) { | |
| return Parse(filename, workspace_layout, workspace, /*build_label=*/"", | |
| /*sem_ver=*/std::nullopt, error, error_text, read_file, | |
| canonicalize_path); | |
| } | |
| RcFile::ParseError RcFile::ParseFile(const std::string& filename, | |
| const std::string& workspace, | |
| const WorkspaceLayout& workspace_layout, | |
| const std::string& build_label, | |
| const std::optional<SemVer>& sem_ver, | |
| ReadFileFn read_file, | |
| CanonicalizePathFn canonicalize_path, | |
| std::vector<std::string>& import_stack, | |
| std::string* error_text) { | |
| BAZEL_LOG(INFO) << "Parsing the RcFile " << filename; | |
| std::string contents; | |
| if (std::string error_msg; !read_file(filename, &contents, &error_msg)) { | |
| *error_text = absl::StrFormat( | |
| "Unexpected error reading config file '%s': %s", filename, error_msg); | |
| return ParseError::UNREADABLE_FILE; | |
| } | |
| const std::string canonical_filename = canonicalize_path(filename); | |
| int rcfile_index = canonical_rcfile_paths_.size(); | |
| canonical_rcfile_paths_.push_back(canonical_filename); | |
| // A '\' at the end of a line continues the line. | |
| blaze_util::Replace("\\\r\n", "", &contents); | |
| blaze_util::Replace("\\\n", "", &contents); | |
| std::vector<std::string> lines = absl::StrSplit(contents, '\n'); | |
| for (std::string& line : lines) { | |
| blaze_util::StripWhitespace(&line); | |
| // Check for an empty line. | |
| if (line.empty()) continue; | |
| std::vector<std::string> words; | |
| // This will treat "#" as a comment, and properly | |
| // quote single and double quotes, and treat '\' | |
| // as an escape character. | |
| // TODO(bazel-team): This function silently ignores | |
| // dangling backslash escapes and missing end-quotes. | |
| blaze_util::Tokenize(line, '#', &words); | |
| // Could happen if line starts with "#" | |
| if (words.empty()) continue; | |
| const absl::string_view command = words[0]; | |
| if (command != kCommandImport && command != kCommandTryImport && | |
| command != kCommandTryImportIfBazelVersion) { | |
| for (absl::string_view word : absl::MakeConstSpan(words).subspan(1)) { | |
| options_[command].push_back({std::string(word), rcfile_index}); | |
| } | |
| continue; | |
| } | |
| if ((command == kCommandTryImportIfBazelVersion && words.size() != 3) || | |
| (command != kCommandTryImportIfBazelVersion && words.size() != 2)) { | |
| *error_text = absl::StrFormat( | |
| "Invalid import declaration in config file '%s': '%s'", | |
| canonical_filename, line); | |
| return ParseError::INVALID_FORMAT; | |
| } | |
| std::string import_filename; | |
| if (command == kCommandImport || command == kCommandTryImport) { | |
| import_filename = words[1]; | |
| } else { // command == kCommandTryImportIfBazelVersion | |
| if (!sem_ver.has_value()) { | |
| BAZEL_LOG(INFO) << absl::StrFormat( | |
| "Skipping '%s' import because bazel build label '%s' is not a " | |
| "valid semantic version.", | |
| line, build_label); | |
| continue; | |
| } | |
| const auto& conditional = words[1]; | |
| import_filename = words[2]; | |
| absl::string_view op; | |
| std::string version; | |
| if (RE2::FullMatch(conditional, *kBazelVersionCmpOp, &op, &version)) { | |
| std::optional<bool> match = BazelVersionMatchesCondition( | |
| sem_ver.value(), op, version, error_text); | |
| if (!match.has_value()) { | |
| // Annotate the existing error_text filled by the function. | |
| *error_text = absl::StrFormat( | |
| "Invalid import declaration in config file '%s': '%s'. %s", | |
| canonical_filename, line, *error_text); | |
| return ParseError::INVALID_FORMAT; | |
| } | |
| if (!match.value()) { | |
| BAZEL_LOG(INFO) << absl::StrFormat( | |
| "Skipped optional import '%s' because the condition (%s) did not " | |
| "match the current running Bazel version (%s)", | |
| line, conditional, build_label); | |
| continue; | |
| } | |
| } else { | |
| *error_text = absl::StrFormat( | |
| "Invalid version condition in config file '%s': '%s'. Condition " | |
| "'%s'. A valid condition is one of the following 7 comparison " | |
| "operators ('<', '<=', '>', '>=', '==', '!=', '~') followed by a " | |
| "semantic version.", | |
| canonical_filename, line, conditional); | |
| return ParseError::INVALID_FORMAT; | |
| } | |
| } | |
| if (absl::StartsWith(import_filename, WorkspaceLayout::kWorkspacePrefix)) { | |
| const auto resolved_filename = | |
| workspace_layout.ResolveWorkspaceRelativeRcFilePath(workspace, | |
| import_filename); | |
| if (!resolved_filename.has_value()) { | |
| if (command == kCommandImport) { | |
| *error_text = absl::StrFormat( | |
| "Nonexistent path in import declaration in config file '%s': '%s'" | |
| " (are you in your source checkout/WORKSPACE?)", | |
| canonical_filename, line); | |
| return ParseError::INVALID_FORMAT; | |
| } | |
| // For try-import, we ignore it if we couldn't find a file. | |
| BAZEL_LOG(INFO) << "Skipped optional import of " << import_filename | |
| << ", the specified rc file either does not exist or " | |
| << "is not readable."; | |
| continue; | |
| } | |
| import_filename = resolved_filename.value(); | |
| } | |
| if (absl::c_linear_search(import_stack, import_filename)) { | |
| std::string loop; | |
| for (const std::string& imported_rc : import_stack) { | |
| absl::StrAppend(&loop, " ", imported_rc, "\n"); | |
| } | |
| absl::StrAppend(&loop, " ", import_filename, "\n"); // Include the loop. | |
| *error_text = absl::StrCat("Import loop detected:\n", loop); | |
| return ParseError::IMPORT_LOOP; | |
| } | |
| import_stack.push_back(import_filename); | |
| if (ParseError parse_error = | |
| ParseFile(import_filename, workspace, workspace_layout, build_label, | |
| sem_ver, read_file, canonicalize_path, import_stack, | |
| error_text); | |
| parse_error != ParseError::NONE) { | |
| if (parse_error == ParseError::UNREADABLE_FILE && | |
| (command == kCommandTryImport || | |
| command == kCommandTryImportIfBazelVersion)) { | |
| // For try-import.*, we ignore it if we couldn't find a file. | |
| BAZEL_LOG(INFO) << "Skipped optional import of " << import_filename | |
| << ", the specified rc file either does not exist or " | |
| "is not readable."; | |
| *error_text = ""; | |
| } else { | |
| // Files that are there but are malformed or introduce a loop are | |
| // still a problem, though, so perpetuate those errors as we would | |
| // for a normal import statement. | |
| return parse_error; | |
| } | |
| } | |
| import_stack.pop_back(); | |
| } | |
| return ParseError::NONE; | |
| } | |
| RcFile::ParseError RcFile::ParseFile( | |
| const std::string& filename, const std::string& workspace, | |
| const WorkspaceLayout& workspace_layout, ReadFileFn read_file, | |
| CanonicalizePathFn canonicalize_path, | |
| std::vector<std::string>& import_stack, std::string* error_text) { | |
| return ParseFile(filename, workspace, workspace_layout, /*build_label=*/"", | |
| /*sem_ver=*/std::nullopt, read_file, canonicalize_path, | |
| import_stack, error_text); | |
| } | |
| bool RcFile::ReadFileDefault(const std::string& filename, std::string* contents, | |
| std::string* error_msg) { | |
| return blaze_util::ReadFile(filename, contents, error_msg); | |
| } | |
| std::string RcFile::CanonicalizePathDefault(const std::string& filename) { | |
| return blaze_util::MakeCanonical(filename.c_str()); | |
| } | |
| std::optional<bool> | |
| BazelVersionMatchesCondition(const SemVer& build_label, absl::string_view op, | |
| const std::string& compare_version, | |
| std::string* error_text) { | |
| if (op == kBazelVersionTilde) { | |
| // For the tilde operator, the version string after the operator can be a | |
| // partial semantic version (i.e. '8' instead of '8.0.0' or '8.2' instead of | |
| // '8.2.0'). Append additional parts to make it a valid semantic version. | |
| const auto num_dots = | |
| std::count(compare_version.begin(), compare_version.end(), '.'); | |
| std::optional<SemVer> semver_compare_version; | |
| if (num_dots == 0) { // 8 -> 8.0.0 | |
| semver_compare_version = | |
| SemVer::Parse(absl::StrCat(compare_version, ".0.0")); | |
| } else if (num_dots == 1) { // 8.2 -> 8.2.0 | |
| semver_compare_version = | |
| SemVer::Parse(absl::StrCat(compare_version, ".0")); | |
| } else { // Assume a valid semantic version. | |
| semver_compare_version = SemVer::Parse(compare_version); | |
| } | |
| if (!semver_compare_version.has_value()) { | |
| *error_text = absl::StrFormat("Could not parse the tilde range version " | |
| "'%s' as a valid semantic version.", | |
| compare_version); | |
| return std::nullopt; | |
| } | |
| if (num_dots == 0) { // eg. ~8 => version >= 8.0.0 && version < 9.0.0 | |
| return build_label >= semver_compare_version.value() && | |
| build_label < semver_compare_version->NextMajorVersion(); | |
| } | |
| // eg. ~8.1 => version >= 8.1.0 && version < 8.2.0 | |
| // eg. ~8.1.4 => version >= 8.1.4 && version < 8.2.0 | |
| return build_label >= semver_compare_version.value() && | |
| build_label < semver_compare_version->NextMinorVersion(); | |
| } | |
| std::optional<SemVer> semver_compare_version = | |
| SemVer::Parse(compare_version); | |
| if (!semver_compare_version.has_value()) { | |
| *error_text = absl::StrFormat( | |
| "Could not parse version '%s' as a valid semantic version.", | |
| compare_version); | |
| return std::nullopt; | |
| } | |
| if (op == kBazelVersionLt) { | |
| return build_label < semver_compare_version; | |
| } else if (op == kBazelVersionLte) { | |
| return build_label <= semver_compare_version; | |
| } else if (op == kBazelVersionGt) { | |
| return build_label > semver_compare_version; | |
| } else if (op == kBazelVersionGte) { | |
| return build_label >= semver_compare_version; | |
| } else if (op == kBazelVersionEq) { | |
| return build_label == semver_compare_version; | |
| } else if (op == kBazelVersionNeq) { | |
| return build_label != semver_compare_version; | |
| } | |
| // We should never get here since only a valid op should be passed in. | |
| *error_text = absl::StrFormat("Invalid comparison operator '%s'.", op); | |
| return std::nullopt; | |
| } | |
| } // namespace blaze |