diff --git a/src/main/cpp/blaze.cc b/src/main/cpp/blaze.cc index 942faba7d4195d..afbc7dad733d91 100644 --- a/src/main/cpp/blaze.cc +++ b/src/main/cpp/blaze.cc @@ -1481,9 +1481,7 @@ void PrintBazelLeaf() { printf("%s\n", leaf.c_str()); } -void PrintVersionInfo(const string &self_path, const string &product_name) { - string build_label; - ExtractBuildLabel(self_path, &build_label); +void PrintVersionInfo(const string &build_label, const string &product_name) { printf("%s %s\n", product_name.c_str(), build_label.c_str()); } @@ -1495,7 +1493,8 @@ static void RunLauncher(const string &self_path, const WorkspaceLayout &workspace_layout, const string &workspace, LoggingInfo *logging_info, StartupInterceptor *interceptor, - CommandExtensionAdder *command_extension_adder) { + CommandExtensionAdder *command_extension_adder, + const string &build_label) { blaze_server = new BlazeServer(startup_options, command_extension_adder); const std::optional command_wait_duration = @@ -1566,8 +1565,6 @@ static void RunLauncher(const string &self_path, option_processor, startup_options, logging_info, extract_data_duration, command_wait_duration, blaze_server); } else { - string build_label; - ExtractBuildLabel(self_path, &build_label); RunClientServerMode( server_exe, server_exe_args, server_dir, workspace_layout, workspace, option_processor, startup_options, logging_info, extract_data_duration, @@ -1593,8 +1590,15 @@ int Main(int argc, const char *const *argv, WorkspaceLayout *workspace_layout, return blaze_exit_code::SUCCESS; } + // Extract build_label to be used in two places: + // 1) PrintVersionInfo() + // 2) Resolving %bazel.version*% variables in .bazelrc files. + string build_label; + ExtractBuildLabel(self_path, &build_label); + option_processor->SetBuildLabel(build_label); + if (argc == 2 && strcmp(argv[1], "--version") == 0) { - PrintVersionInfo(self_path, option_processor->GetLowercaseProductName()); + PrintVersionInfo(build_label, option_processor->GetLowercaseProductName()); return blaze_exit_code::SUCCESS; } @@ -1671,7 +1675,7 @@ int Main(int argc, const char *const *argv, WorkspaceLayout *workspace_layout, RunLauncher(self_path, archive_contents, install_md5, *startup_options, *option_processor, *workspace_layout, workspace, &logging_info, - interceptor, command_extension_adder); + interceptor, command_extension_adder, build_label); return 0; } diff --git a/src/main/cpp/blaze.h b/src/main/cpp/blaze.h index 58adcc7a6eaf89..66d50525043262 100644 --- a/src/main/cpp/blaze.h +++ b/src/main/cpp/blaze.h @@ -26,7 +26,7 @@ namespace blaze { // Prints client version information to standard output, e.g. when invoking the // client with "--version". -void PrintVersionInfo(const std::string& self_path, +void PrintVersionInfo(const std::string& build_label, const std::string& product_name); int Main(int argc, const char* const* argv, WorkspaceLayout* workspace_layout, diff --git a/src/main/cpp/option_processor.cc b/src/main/cpp/option_processor.cc index b3b363ab7d2b0d..0673fc3225212d 100644 --- a/src/main/cpp/option_processor.cc +++ b/src/main/cpp/option_processor.cc @@ -447,7 +447,8 @@ blaze_exit_code::ExitCode OptionProcessor::GetRcFiles( for (const std::string& top_level_bazelrc_path : rc_files) { std::unique_ptr parsed_rc; blaze_exit_code::ExitCode parse_rcfile_exit_code = ParseRcFile( - workspace_layout, workspace, top_level_bazelrc_path, &parsed_rc, error); + workspace_layout, workspace, top_level_bazelrc_path, build_label_, + &parsed_rc, error); if (parse_rcfile_exit_code != blaze_exit_code::SUCCESS) { return parse_rcfile_exit_code; } @@ -485,17 +486,30 @@ blaze_exit_code::ExitCode OptionProcessor::GetRcFiles( return blaze_exit_code::SUCCESS; } +// When the build label can't be parsed into a proper semantic version (per +// semver.org), this will be the value for each semantic variable part. +constexpr char kNoVersion[] = "no_version"; + blaze_exit_code::ExitCode ParseRcFile(const WorkspaceLayout* workspace_layout, const std::string& workspace, const std::string& rc_file_path, + const std::string& build_label, std::unique_ptr* result_rc_file, std::string* error) { assert(!rc_file_path.empty()); assert(result_rc_file != nullptr); + auto sem_ver = ParseSemVer(build_label); + if (!sem_ver.has_value()) { + // Couldn't parse a version, provide "no_version" values for a SemVer. + SemVer noVersionSemVer = {kNoVersion, kNoVersion}; + sem_ver.emplace(noVersionSemVer); + } + RcFile::ParseError parse_error; std::unique_ptr parsed_file = RcFile::Parse( - rc_file_path, workspace_layout, workspace, &parse_error, error); + rc_file_path, workspace_layout, workspace, &parse_error, error, + sem_ver.value()); if (parsed_file == nullptr) { return internal::ParseErrorToExitCode(parse_error); } diff --git a/src/main/cpp/option_processor.h b/src/main/cpp/option_processor.h index 801689d7c3c7f9..7acb19865a4012 100644 --- a/src/main/cpp/option_processor.h +++ b/src/main/cpp/option_processor.h @@ -127,6 +127,11 @@ class OptionProcessor { // the failure. Otherwise, the server will handle any required logging. void PrintStartupOptionsProvenanceMessage() const; + // Sets the build label. + void SetBuildLabel(const std::string &build_label) { + build_label_ = build_label; + } + // Parse the files in `blazercs` and return all options that need to be passed // to the server. The options are returned in the order they should be appear // on the command line (later options have precedence over earlier ones). @@ -180,12 +185,16 @@ class OptionProcessor { // Path to the system-wide bazelrc configuration file. // This is configurable for testing purposes only. const std::string system_bazelrc_path_; + + // Build label for Bazel. + std::string build_label_; }; // Parses and returns the contents of the rc file. blaze_exit_code::ExitCode ParseRcFile(const WorkspaceLayout* workspace_layout, const std::string& workspace, const std::string& rc_file_path, + const std::string& build_label, std::unique_ptr* result_rc_file, std::string* error); diff --git a/src/main/cpp/rc_file.cc b/src/main/cpp/rc_file.cc index c227e7b4d33b4d..6132b0c7987599 100644 --- a/src/main/cpp/rc_file.cc +++ b/src/main/cpp/rc_file.cc @@ -16,6 +16,7 @@ #include #include +#include #include #include #include @@ -31,6 +32,7 @@ #include "absl/strings/match.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" +#include "absl/strings/str_replace.h" #include "absl/strings/str_split.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" @@ -43,18 +45,20 @@ static constexpr absl::string_view kCommandTryImport = "try-import"; /*static*/ std::unique_ptr 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) { + const SemVer& sem_ver, ReadFileFn read_file, + CanonicalizePathFn canonicalize_path) { auto rcfile = absl::WrapUnique(new RcFile()); std::vector initial_import_stack = {filename}; - *error = rcfile->ParseFile(filename, workspace, *workspace_layout, read_file, - canonicalize_path, initial_import_stack, - error_text); + *error = rcfile->ParseFile(filename, workspace, *workspace_layout, + sem_ver, read_file, canonicalize_path, + initial_import_stack, error_text); return (*error == ParseError::NONE) ? std::move(rcfile) : nullptr; } RcFile::ParseError RcFile::ParseFile(const std::string& filename, const std::string& workspace, const WorkspaceLayout& workspace_layout, + const SemVer& sem_ver, ReadFileFn read_file, CanonicalizePathFn canonicalize_path, std::vector& import_stack, @@ -109,17 +113,24 @@ RcFile::ParseError RcFile::ParseFile(const std::string& filename, return ParseError::INVALID_FORMAT; } - std::string& import_filename = words[1]; + std::string import_filename = ReplaceBuildVars(sem_ver, words[1]); 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) { + // If build variables were replaced in the filename, print out the + // evaluated path so they know the file lookup that was attempted. + std::string evaluated_line = ReplaceBuildVars(sem_ver, line); + std::string evaluated_warning; + if (line != evaluated_line) { + evaluated_warning = absl::StrFormat("file evaluated to '%s' - ", evaluated_line); + } *error_text = absl::StrFormat( "Nonexistent path in import declaration in config file '%s': '%s'" - " (are you in your source checkout/WORKSPACE?)", - canonical_filename, line); + " (%sare you in your source checkout/WORKSPACE?)", + canonical_filename, line, evaluated_warning); return ParseError::INVALID_FORMAT; } // For try-import, we ignore it if we couldn't find a file. @@ -144,8 +155,8 @@ RcFile::ParseError RcFile::ParseFile(const std::string& filename, import_stack.push_back(import_filename); if (ParseError parse_error = - ParseFile(import_filename, workspace, workspace_layout, read_file, - canonicalize_path, import_stack, error_text); + ParseFile(import_filename, workspace, workspace_layout, sem_ver, + read_file, canonicalize_path, import_stack, error_text); parse_error != ParseError::NONE) { if (parse_error == ParseError::UNREADABLE_FILE && command == kCommandTryImport) { @@ -175,5 +186,37 @@ bool RcFile::ReadFileDefault(const std::string& filename, std::string* contents, std::string RcFile::CanonicalizePathDefault(const std::string& filename) { return blaze_util::MakeCanonical(filename.c_str()); } +namespace { +// Variables that can be interpolated in .bazelrc when importing files. +constexpr char kBazelVersionMajor[] = + "%bazel.version.major%"; // Eg. "8" in 8.4.2 +constexpr char kBazelVersionMajorMinor[] = + "%bazel.version.major.minor%"; // Eg. "8.4" in 8.4.2 + +// Semantic version regex copied verbatim from +// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +const std::regex kSemverRe( + R"(^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$)"); +} // namespace + +std::optional ParseSemVer(const std::string& build_label) { + if (std::smatch m; std::regex_match(build_label, m, kSemverRe)) { + SemVer sem_ver; + sem_ver.major = m[1]; + sem_ver.minor = m[2]; + return sem_ver; + } + return std::nullopt; +} + +std::string ReplaceBuildVars(const SemVer& sem_ver, + absl::string_view import_filename) { + return absl::StrReplaceAll( + import_filename, { + {kBazelVersionMajor, sem_ver.major}, + {kBazelVersionMajorMinor, + absl::StrCat(sem_ver.major, ".", sem_ver.minor)}, + }); +} } // namespace blaze diff --git a/src/main/cpp/rc_file.h b/src/main/cpp/rc_file.h index 22cecbb69a2a50..8f6ffae87466d1 100644 --- a/src/main/cpp/rc_file.h +++ b/src/main/cpp/rc_file.h @@ -15,6 +15,7 @@ #define BAZEL_SRC_MAIN_CPP_RC_FILE_H_ #include +#include #include #include @@ -32,6 +33,13 @@ struct RcOption { int source_index; }; +// Represents a semantic version. Only major and minor values are provided since +// those are the only ones used at the moment. +struct SemVer { + std::string major; // "8" in the semantic version "8.4.2" + std::string minor; // "4" in the semantic version "8.4.2" +}; + // Reads and parses a single rc file with all its imports. class RcFile { public: @@ -44,7 +52,7 @@ class RcFile { static std::unique_ptr Parse( const std::string& filename, const WorkspaceLayout* workspace_layout, const std::string& workspace, ParseError* error, std::string* error_text, - ReadFileFn read_file = &ReadFileDefault, + const SemVer& build_label, ReadFileFn read_file = &ReadFileDefault, CanonicalizePathFn canonicalize_path = &CanonicalizePathDefault); // Movable and copyable. @@ -69,6 +77,7 @@ class RcFile { ParseError ParseFile(const std::string& filename, const std::string& workspace, const WorkspaceLayout& workspace_layout, + const SemVer& build_label, ReadFileFn read_file, CanonicalizePathFn canonicalize_path, std::vector& import_stack, @@ -86,6 +95,13 @@ class RcFile { OptionMap options_; }; + +// Parses a version string and returns a structured SemVer. If the argument is +// not a valid semantic version per semver.org, returns nullopt. +std::optional ParseSemVer(const std::string& build_label); + +std::string ReplaceBuildVars(const SemVer& sem_ver, + absl::string_view import_filename); } // namespace blaze #endif // BAZEL_SRC_MAIN_CPP_RC_FILE_H_ diff --git a/src/test/cpp/rc_file_test.cc b/src/test/cpp/rc_file_test.cc index 5b65f71438385f..2c57807d59564a 100644 --- a/src/test/cpp/rc_file_test.cc +++ b/src/test/cpp/rc_file_test.cc @@ -49,6 +49,7 @@ constexpr const char* kNullDevice = "NUL"; #else // Assume POSIX if not Windows. constexpr const char* kNullDevice = "/dev/null"; #endif +constexpr char kTestBuildLabel[] = "8.4.2"; // Matches an RcFile's canonical source paths list. MATCHER_P(CanonicalSourcePathsAre, paths_matcher, "") { @@ -93,6 +94,7 @@ class RcFileTest : public ::testing::Test { option_processor_.reset(new OptionProcessor( workspace_layout_.get(), std::make_unique(), "bazel.bazelrc")); + option_processor_->SetBuildLabel(kTestBuildLabel); } void TearDown() override { @@ -689,6 +691,27 @@ class BlazercImportTest : public ParseOptionsTest { "times from .*workspace.*bazelrc\n"); } + void TestThatDoubleImportWithWorkspaceAndBuildVersionVariablesCauseAWarning( + const std::string &import_type) { + const std::string imported_rc_path = + blaze_util::JoinPath(workspace_, "bazel8.4.bazelrc"); + ASSERT_TRUE(blaze_util::WriteFile("", imported_rc_path, 0755)); + + // Import the custom location twice, once with direct path and once with + // variables. + std::string workspace_rc; + ASSERT_TRUE(SetUpWorkspaceRcFile( + import_type + " " + imported_rc_path + "\n" + import_type + + " %workspace%/bazel%bazel.version.major.minor%.bazelrc\n", + &workspace_rc)); + + const std::vector args = {"bazel", "build"}; + ParseOptionsAndCheckOutput( + args, blaze_exit_code::SUCCESS, "", + "WARNING: Duplicate rc file: .*bazel8.4.bazelrc is imported multiple " + "times from .*workspace.*bazelrc\n"); + } + void TestThatDoubleImportWithExcessPathSyntaxCauseAWarning( const std::string& import_type) { const std::string imported_rc_path = @@ -825,7 +848,33 @@ TEST_F(BlazercImportTest, BazelRcImportDoesNotFallBackToLiteralPlaceholder) { const std::vector args = {"bazel", "build"}; ParseOptionsAndCheckOutput(args, blaze_exit_code::BAD_ARGV, "Nonexistent path in import declaration in config " - "file.*'import %workspace%/tryimported.bazelrc'", + "file.*'import %workspace%/tryimported.bazelrc' " + "\\(are you in your source checkout", + ""); +} + +TEST_F(BlazercImportTest, IncorrectBazelVersionVariablesPrintsEvaluatedPath) { + // User uses %bazel.version*% variables incorrectly - (they wanted 8.bazelrc, + // but they used the nonexistent %bazel.version% instead of + // %bazel.version.major%. The error should show the evaluated path. + + const std::string literal_placeholder_rc_path = + blaze_util::JoinPath(cwd_, "%workspace%/8.bazelrc"); + ASSERT_TRUE(blaze_util::MakeDirectories( + blaze_util::Dirname(literal_placeholder_rc_path), 0755)); + ASSERT_TRUE(blaze_util::WriteFile("import syntax error", + literal_placeholder_rc_path, 0755)); + + std::string workspace_rc; + ASSERT_TRUE(SetUpWorkspaceRcFile( + "import %workspace%/%bazel.version.major.minor%.bazelrc", &workspace_rc)); + + const std::vector args = {"bazel", "build"}; + ParseOptionsAndCheckOutput(args, blaze_exit_code::BAD_ARGV, + "Nonexistent path in import declaration in config " + "file.*'import %workspace%/%bazel.version.major.minor%.bazelrc" + "' \\(file evaluated to " + "'import %workspace%/8.4.bazelrc'", ""); } @@ -886,6 +935,16 @@ TEST_F(BlazercImportTest, TestThatDoubleImportWithWorkspaceRelativeSyntaxCauseAWarning("try-import"); } +TEST_F(BlazercImportTest, + DoubleImportWithWorkspaceAndBuildVersionVariablesCauseAWarning) { + TestThatDoubleImportWithWorkspaceAndBuildVersionVariablesCauseAWarning("import"); +} + +TEST_F(BlazercImportTest, + DoubleTryImportWithWorkspaceAndBuildVersionVariablesCauseAWarning) { + TestThatDoubleImportWithWorkspaceAndBuildVersionVariablesCauseAWarning("try-import"); +} + TEST_F(BlazercImportTest, DoubleImportWithExcessPathSyntaxCauseAWarning) { TestThatDoubleImportWithExcessPathSyntaxCauseAWarning("import"); } @@ -972,4 +1031,62 @@ TEST_F(ParseOptionsTest, ImportingStandardRcBeforeItIsLoadedCausesAWarning) { } #endif // !defined(_WIN32) && !defined(__CYGWIN__) -} // namespace blaze +TEST(TestParseSemVer, ValidBuildLabels) { + auto sem_ver_842 = ParseSemVer("8.4.2"); + ASSERT_TRUE(sem_ver_842.has_value()); + EXPECT_EQ("8", sem_ver_842->major); + EXPECT_EQ("4", sem_ver_842->minor); + + auto sem_ver_912 = ParseSemVer("9.1.2-pre.20251022.1"); + ASSERT_TRUE(sem_ver_912.has_value()); + EXPECT_EQ("9", sem_ver_912->major); + EXPECT_EQ("1", sem_ver_912->minor); +} + +TEST(TestParseSemVer, InalidBuildLabels) { + auto no_version = ParseSemVer("no_version"); + ASSERT_FALSE(no_version.has_value()); + + auto not_full_sem_ver = ParseSemVer("8.2"); + ASSERT_FALSE(not_full_sem_ver.has_value()); +} + +TEST(TestReplaceBuildVars, AllVersionReplacements) { + EXPECT_EQ(ReplaceBuildVars( + SemVer({"9", "4"}), + "bazel.version.major: %bazel.version.major%\n" + "bazel.version.major.minor: %bazel.version.major.minor%\n"), + "bazel.version.major: 9\n" + "bazel.version.major.minor: 9.4\n"); +} + +// Official Build Numbers and standard use case. +TEST(TestReplaceBuildVars, HandlesStandardReplacements) { + EXPECT_EQ( + ReplaceBuildVars(SemVer({"8", "4"}), "%workspace%/%bazel.version.major%"), + "%workspace%/8"); + EXPECT_EQ(ReplaceBuildVars(SemVer({"8", "4"}), + "path/" + "%bazel.version.major.minor%/.bazelrc"), + "path/8.4/.bazelrc"); +} + +TEST(TestReplaceBuildVars, DoesNothingWhenNoVariablesPresent) { + std::string regular_filename = ".rcs/my.bazelrc"; + EXPECT_EQ(ReplaceBuildVars(SemVer({"8", "4"}), regular_filename), + regular_filename); + + // Doesn't have any valid variables with %. + std::string filename_nopercent = "bazel.version.major/.bazelrc"; + EXPECT_EQ(ReplaceBuildVars(SemVer({"8", "4"}), filename_nopercent), + filename_nopercent); +} + +TEST(TestReplaceBuildVars, SimulateInvalidSemanticVersion) { + EXPECT_EQ(ReplaceBuildVars(SemVer({"no_version", "no_version"}), + "path/" + "%bazel.version.major.minor%/.bazelrc"), + "path/no_version.no_version/.bazelrc"); +} + +} // namespace blaze diff --git a/src/test/cpp/rc_options_test.cc b/src/test/cpp/rc_options_test.cc index 2dab13467528e5..35ad16aa870551 100644 --- a/src/test/cpp/rc_options_test.cc +++ b/src/test/cpp/rc_options_test.cc @@ -49,6 +49,7 @@ constexpr bool kIsWindows = true; #else constexpr bool kIsWindows = false; #endif +const SemVer kTestSemVer = SemVer({"8", "4"}); class RcOptionsTest : public ::testing::Test { protected: @@ -79,7 +80,8 @@ class RcOptionsTest : public ::testing::Test { // Set workspace to test_file_dir_ so importing %workspace%/foo works. test_file_dir_, error, - error_text); + error_text, + kTestSemVer); } void SuccessfullyParseRcWithExpectedArgs( @@ -553,7 +555,7 @@ TEST(RemoteFileTest, ParsingRemoteFiles) { std::unique_ptr rcfile = RcFile::Parse( "the base file", &workspace_layout, "my workspace", &error, &error_text, - read_file.AsStdFunction(), canonicalize_path.AsStdFunction()); + kTestSemVer, read_file.AsStdFunction(), canonicalize_path.AsStdFunction()); EXPECT_THAT(error_text, IsEmpty()); ASSERT_EQ(error, RcFile::ParseError::NONE); EXPECT_THAT(rcfile, Pointee(Property(