| # pylint: disable=g-backslash-continuation |
| # Copyright 2023 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. |
| # pylint: disable=g-long-ternary |
| |
| import json |
| import os |
| import tempfile |
| import unittest |
| |
| from src.test.py.bazel import test_base |
| from src.test.py.bazel.bzlmod.test_utils import BazelRegistry |
| from src.test.py.bazel.bzlmod.test_utils import scratchFile |
| |
| |
| class BazelLockfileTest(test_base.TestBase): |
| |
| def setUp(self): |
| test_base.TestBase.setUp(self) |
| self.registries_work_dir = tempfile.mkdtemp(dir=self._test_cwd) |
| self.main_registry = BazelRegistry( |
| os.path.join(self.registries_work_dir, 'main') |
| ) |
| self.main_registry.createCcModule('aaa', '1.0').createCcModule( |
| 'aaa', '1.1' |
| ).createCcModule('bbb', '1.0', {'aaa': '1.0'}).createCcModule( |
| 'bbb', '1.1', {'aaa': '1.1'} |
| ).createCcModule( |
| 'ccc', '1.1', {'aaa': '1.1', 'bbb': '1.1'} |
| ) |
| self.ScratchFile( |
| '.bazelrc', |
| [ |
| # In ipv6 only network, this has to be enabled. |
| # 'startup --host_jvm_args=-Djava.net.preferIPv6Addresses=true', |
| 'common --enable_bzlmod', |
| 'common --experimental_isolated_extension_usages', |
| 'common --registry=' + self.main_registry.getURL(), |
| # We need to have BCR here to make sure built-in modules like |
| # bazel_tools can work. |
| 'common --registry=https://bcr.bazel.build', |
| 'common --verbose_failures', |
| # Set an explicit Java language version |
| 'common --java_language_version=8', |
| 'common --tool_java_language_version=8', |
| 'common --lockfile_mode=update', |
| ], |
| ) |
| self.ScratchFile('WORKSPACE') |
| # The existence of WORKSPACE.bzlmod prevents WORKSPACE prefixes or suffixes |
| # from being used; this allows us to test built-in modules actually work |
| self.ScratchFile('WORKSPACE.bzlmod') |
| |
| def testChangeModuleInRegistryWithoutLockfile(self): |
| # Add module 'sss' to the registry with dep on 'aaa' |
| self.main_registry.createCcModule('sss', '1.3', {'aaa': '1.1'}) |
| # Create a project with deps on 'sss' |
| self.ScratchFile( |
| 'MODULE.bazel', |
| [ |
| 'bazel_dep(name = "sss", version = "1.3")', |
| ], |
| ) |
| self.ScratchFile('BUILD', ['filegroup(name = "hello")']) |
| self.RunBazel( |
| [ |
| 'build', |
| '--nobuild', |
| '--lockfile_mode=off', |
| '//:all', |
| ], |
| allow_failure=False, |
| ) |
| |
| # Change registry -> update 'sss' module file (corrupt it) |
| module_dir = self.main_registry.root.joinpath('modules', 'sss', '1.3') |
| scratchFile(module_dir.joinpath('MODULE.bazel'), ['whatever!']) |
| |
| # Clean bazel to empty any cache of the deps tree |
| self.RunBazel(['clean', '--expunge']) |
| # Runing again will try to get 'sss' which should produce an error |
| exit_code, _, stderr = self.RunBazel( |
| [ |
| 'build', |
| '--nobuild', |
| '--lockfile_mode=off', |
| '//:all', |
| ], |
| allow_failure=True, |
| ) |
| self.AssertExitCode(exit_code, 48, stderr) |
| self.assertIn( |
| ( |
| 'ERROR: Error computing the main repository mapping: error parsing' |
| ' MODULE.bazel file for sss@1.3' |
| ), |
| stderr, |
| ) |
| |
| def testChangeModuleInRegistryWithLockfile(self): |
| # Add module 'sss' to the registry with dep on 'aaa' |
| self.main_registry.createCcModule('sss', '1.3', {'aaa': '1.1'}) |
| # Create a project with deps on 'sss' |
| self.ScratchFile( |
| 'MODULE.bazel', |
| [ |
| 'bazel_dep(name = "sss", version = "1.3")', |
| ], |
| ) |
| self.ScratchFile('BUILD', ['filegroup(name = "hello")']) |
| self.RunBazel( |
| [ |
| 'build', |
| '--nobuild', |
| '//:all', |
| ], |
| allow_failure=False, |
| ) |
| |
| # Change registry -> update 'sss' module file (corrupt it) |
| module_dir = self.main_registry.root.joinpath('modules', 'sss', '1.3') |
| scratchFile(module_dir.joinpath('MODULE.bazel'), ['whatever!']) |
| |
| # Clean bazel to empty any cache of the deps tree |
| self.RunBazel(['clean', '--expunge']) |
| # Running with the lockfile, should not recognize the registry changes |
| # hence find no errors |
| self.RunBazel(['build', '--nobuild', '//:all'], allow_failure=False) |
| |
| def testChangeFlagWithLockfile(self): |
| # Add module 'sss' to the registry with dep on 'aaa' |
| self.main_registry.createCcModule('sss', '1.3', {'aaa': '1.1'}) |
| # Create a project with deps on 'sss' |
| self.ScratchFile( |
| 'MODULE.bazel', |
| [ |
| 'bazel_dep(name = "sss", version = "1.3")', |
| ], |
| ) |
| self.ScratchFile('BUILD', ['filegroup(name = "hello")']) |
| self.RunBazel( |
| ['build', '--nobuild', '//:all'], |
| allow_failure=False, |
| ) |
| |
| # Change registry -> update 'sss' module file (corrupt it) |
| module_dir = self.main_registry.root.joinpath('modules', 'sss', '1.3') |
| scratchFile(module_dir.joinpath('MODULE.bazel'), ['whatever!']) |
| |
| # Clean bazel to empty any cache of the deps tree |
| self.RunBazel(['clean', '--expunge']) |
| # Running with the lockfile, but adding a flag should cause resolution rerun |
| exit_code, _, stderr = self.RunBazel( |
| [ |
| 'build', |
| '--nobuild', |
| '--check_direct_dependencies=error', |
| '//:all', |
| ], |
| allow_failure=True, |
| ) |
| self.AssertExitCode(exit_code, 48, stderr) |
| self.assertRegex( |
| '\n'.join(stderr), |
| "ERROR: .*/sss/1.3/MODULE.bazel:1:9: invalid character: '!'", |
| ) |
| |
| def testLockfileErrorMode(self): |
| self.ScratchFile('MODULE.bazel', []) |
| self.ScratchFile('BUILD', ['filegroup(name = "hello")']) |
| self.RunBazel( |
| [ |
| 'build', |
| '--nobuild', |
| '--check_direct_dependencies=warning', |
| '//:all', |
| ], |
| allow_failure=False, |
| ) |
| |
| # Run with updated module and a different flag |
| self.ScratchFile('MODULE.bazel', ['module(name="lala")']) |
| exit_code, _, stderr = self.RunBazel( |
| [ |
| 'build', |
| '--nobuild', |
| '--check_direct_dependencies=error', |
| '--lockfile_mode=error', |
| '//:all', |
| ], |
| allow_failure=True, |
| ) |
| self.AssertExitCode(exit_code, 48, stderr) |
| self.assertIn( |
| ( |
| 'ERROR: Error computing the main repository mapping: Lock file is' |
| ' no longer up-to-date because: the root MODULE.bazel has been' |
| ' modified, the value of --check_direct_dependencies flag has' |
| ' been modified' |
| ), |
| stderr, |
| ) |
| |
| def testLocalOverrideWithErrorMode(self): |
| self.ScratchFile( |
| 'MODULE.bazel', |
| [ |
| 'module(name="lala")', |
| 'bazel_dep(name="bar")', |
| 'local_path_override(module_name="bar",path="bar")', |
| ], |
| ) |
| self.ScratchFile('BUILD', ['filegroup(name = "hello")']) |
| self.ScratchFile('bar/MODULE.bazel', ['module(name="bar")']) |
| self.ScratchFile('bar/WORKSPACE', []) |
| self.ScratchFile('bar/BUILD', ['filegroup(name = "hello from bar")']) |
| self.RunBazel( |
| [ |
| 'build', |
| '--nobuild', |
| '//:all', |
| ], |
| allow_failure=False, |
| ) |
| |
| # Run with updated module and a different flag |
| self.ScratchFile( |
| 'bar/MODULE.bazel', |
| [ |
| 'module(name="bar")', |
| 'bazel_dep(name="hmmm")', |
| ], |
| ) |
| exit_code, _, stderr = self.RunBazel( |
| ['build', '--nobuild', '--lockfile_mode=error', '//:all'], |
| allow_failure=True, |
| ) |
| self.AssertExitCode(exit_code, 48, stderr) |
| self.assertIn( |
| ( |
| 'ERROR: Error computing the main repository mapping: Lock file is' |
| ' no longer up-to-date because: The MODULE.bazel file has changed' |
| ' for the overriden module: bar' |
| ), |
| stderr, |
| ) |
| |
| def testModuleExtension(self): |
| self.ScratchFile( |
| 'MODULE.bazel', |
| [ |
| 'lockfile_ext = use_extension("extension.bzl", "lockfile_ext")', |
| 'lockfile_ext.dep(name = "bmbm", versions = ["v1", "v2"])', |
| 'use_repo(lockfile_ext, "hello")', |
| ], |
| ) |
| self.ScratchFile('BUILD.bazel') |
| self.ScratchFile( |
| 'extension.bzl', |
| [ |
| 'def _repo_rule_impl(ctx):', |
| ' ctx.file("WORKSPACE")', |
| ' ctx.file("BUILD", "filegroup(name=\'lala\')")', |
| '', |
| 'repo_rule = repository_rule(implementation=_repo_rule_impl)', |
| '', |
| 'def _module_ext_impl(ctx):', |
| ' print("Hello from the other side!")', |
| ' repo_rule(name="hello")', |
| ' for mod in ctx.modules:', |
| ' for dep in mod.tags.dep:', |
| ' print("Name:", dep.name, ", Versions:", dep.versions)', |
| '', |
| ( |
| '_dep = tag_class(attrs={"name": attr.string(), "versions":' |
| ' attr.string_list()})' |
| ), |
| 'lockfile_ext = module_extension(', |
| ' implementation=_module_ext_impl,', |
| ' tag_classes={"dep": _dep},', |
| ')', |
| ], |
| ) |
| |
| _, _, stderr = self.RunBazel(['build', '@hello//:all']) |
| self.assertIn('Hello from the other side!', ''.join(stderr)) |
| self.assertIn('Name: bmbm , Versions: ["v1", "v2"]', ''.join(stderr)) |
| |
| self.RunBazel(['shutdown']) |
| _, _, stderr = self.RunBazel(['build', '@hello//:all']) |
| self.assertNotIn('Hello from the other side!', ''.join(stderr)) |
| |
| def testIsolatedModuleExtension(self): |
| self.ScratchFile( |
| 'MODULE.bazel', |
| [ |
| ( |
| 'lockfile_ext = use_extension("extension.bzl", "lockfile_ext",' |
| ' isolate = True)' |
| ), |
| 'lockfile_ext.dep(name = "bmbm", versions = ["v1", "v2"])', |
| 'use_repo(lockfile_ext, "hello")', |
| ], |
| ) |
| self.ScratchFile('BUILD.bazel') |
| self.ScratchFile( |
| 'extension.bzl', |
| [ |
| 'def _repo_rule_impl(ctx):', |
| ' ctx.file("WORKSPACE")', |
| ' ctx.file("BUILD", "filegroup(name=\'lala\')")', |
| '', |
| 'repo_rule = repository_rule(implementation=_repo_rule_impl)', |
| '', |
| 'def _module_ext_impl(ctx):', |
| ' print("Hello from the other side!")', |
| ' repo_rule(name="hello")', |
| ' for mod in ctx.modules:', |
| ' for dep in mod.tags.dep:', |
| ' print("Name:", dep.name, ", Versions:", dep.versions)', |
| '', |
| '_dep = tag_class(attrs={"name": attr.string(), "versions":', |
| ' attr.string_list()})', |
| 'lockfile_ext = module_extension(', |
| ' implementation=_module_ext_impl,', |
| ' tag_classes={"dep": _dep},', |
| ')', |
| ], |
| ) |
| |
| _, _, stderr = self.RunBazel(['build', '@hello//:all']) |
| self.assertIn('Hello from the other side!', ''.join(stderr)) |
| self.assertIn('Name: bmbm , Versions: ["v1", "v2"]', ''.join(stderr)) |
| |
| self.RunBazel(['shutdown']) |
| _, _, stderr = self.RunBazel(['build', '@hello//:all']) |
| self.assertNotIn('Hello from the other side!', ''.join(stderr)) |
| |
| def testModuleExtensionsInDifferentBuilds(self): |
| # Test that the module extension stays in the lockfile (as long as it's |
| # used in the module) even if it is not in the current build |
| self.ScratchFile( |
| 'MODULE.bazel', |
| [ |
| 'extA = use_extension("extension.bzl", "extA")', |
| 'extB = use_extension("extension.bzl", "extB")', |
| 'use_repo(extA, "hello_ext_A")', |
| 'use_repo(extB, "hello_ext_B")', |
| ], |
| ) |
| self.ScratchFile('BUILD.bazel') |
| self.ScratchFile( |
| 'extension.bzl', |
| [ |
| 'def _repo_rule_impl(ctx):', |
| ' ctx.file("WORKSPACE")', |
| ' ctx.file("BUILD", "filegroup(name=\'lala\')")', |
| 'repo_rule = repository_rule(implementation=_repo_rule_impl)', |
| 'def _ext_a_impl(ctx):', |
| ' repo_rule(name="hello_ext_A")', |
| 'def _ext_b_impl(ctx):', |
| ' repo_rule(name="hello_ext_B")', |
| 'extA = module_extension(implementation=_ext_a_impl)', |
| 'extB = module_extension(implementation=_ext_b_impl)', |
| ], |
| ) |
| |
| self.RunBazel(['build', '@hello_ext_A//:all']) |
| self.RunBazel(['build', '@hello_ext_B//:all']) |
| |
| with open(self.Path('MODULE.bazel.lock'), 'r') as f: |
| lockfile = json.loads(f.read().strip()) |
| self.assertGreater(len(lockfile['moduleDepGraph']), 0) |
| self.assertEqual( |
| list(lockfile['moduleExtensions'].keys()), |
| ['//:extension.bzl%extA', '//:extension.bzl%extB'], |
| ) |
| |
| def testUpdateModuleExtension(self): |
| self.ScratchFile( |
| 'MODULE.bazel', |
| [ |
| 'lockfile_ext = use_extension("extension.bzl", "lockfile_ext")', |
| 'use_repo(lockfile_ext, "hello")', |
| ], |
| ) |
| self.ScratchFile('BUILD.bazel') |
| self.ScratchFile( |
| 'extension.bzl', |
| [ |
| 'def _repo_rule_impl(ctx):', |
| ' ctx.file("WORKSPACE")', |
| ' ctx.file("BUILD", "filegroup(name=\\"lala\\")")', |
| 'repo_rule = repository_rule(implementation = _repo_rule_impl)', |
| 'def _module_ext_impl(ctx):', |
| ' print("Hello from the other side!")', |
| ' repo_rule(name= "hello")', |
| ( |
| 'lockfile_ext = module_extension(implementation =' |
| ' _module_ext_impl)' |
| ), |
| ], |
| ) |
| _, _, stderr = self.RunBazel(['build', '@hello//:all']) |
| self.assertIn('Hello from the other side!', ''.join(stderr)) |
| with open(self.Path('MODULE.bazel.lock'), 'r') as f: |
| lockfile = json.loads(f.read().strip()) |
| old_impl = lockfile['moduleExtensions']['//:extension.bzl%lockfile_ext'][ |
| 'bzlTransitiveDigest' |
| ] |
| |
| # Run again to make sure the resolution value is cached. So even if module |
| # resolution doesn't rerun (its event is null), the lockfile is still |
| # updated with the newest extension eval results |
| self.RunBazel(['build', '@hello//:all']) |
| |
| # Update extension. Make sure that it is executed and updated in the |
| # lockfile without errors (since it's already in the lockfile) |
| self.ScratchFile( |
| 'extension.bzl', |
| [ |
| 'def _repo_rule_impl(ctx):', |
| ' ctx.file("WORKSPACE")', |
| ' ctx.file("BUILD", "filegroup(name=\\"lala\\")")', |
| 'repo_rule = repository_rule(implementation = _repo_rule_impl)', |
| 'def _module_ext_impl(ctx):', |
| ' print("Hello from the other town!")', |
| ' repo_rule(name= "hello")', |
| ( |
| 'lockfile_ext = module_extension(implementation =' |
| ' _module_ext_impl)' |
| ), |
| ], |
| ) |
| _, _, stderr = self.RunBazel(['build', '@hello//:all']) |
| self.assertIn('Hello from the other town!', ''.join(stderr)) |
| with open(self.Path('MODULE.bazel.lock'), 'r') as f: |
| lockfile = json.loads(f.read().strip()) |
| new_impl = lockfile['moduleExtensions']['//:extension.bzl%lockfile_ext'][ |
| 'bzlTransitiveDigest' |
| ] |
| self.assertNotEqual(new_impl, old_impl) |
| |
| def testUpdateModuleExtensionErrorMode(self): |
| self.ScratchFile( |
| 'MODULE.bazel', |
| [ |
| 'lockfile_ext = use_extension("extension.bzl", "lockfile_ext")', |
| 'use_repo(lockfile_ext, "hello")', |
| ], |
| ) |
| self.ScratchFile('BUILD.bazel') |
| self.ScratchFile( |
| 'extension.bzl', |
| [ |
| 'def _repo_rule_impl(ctx):', |
| ' ctx.file("WORKSPACE")', |
| ' ctx.file("BUILD", "filegroup(name=\\"lala\\")")', |
| 'repo_rule = repository_rule(implementation = _repo_rule_impl)', |
| 'def _module_ext_impl(ctx):', |
| ' print("Hello from the other side!")', |
| ' repo_rule(name= "hello")', |
| ( |
| 'lockfile_ext = module_extension(implementation =' |
| ' _module_ext_impl)' |
| ), |
| ], |
| ) |
| _, _, stderr = self.RunBazel(['build', '@hello//:all']) |
| self.assertIn('Hello from the other side!', ''.join(stderr)) |
| self.RunBazel(['shutdown']) |
| |
| # Update extension. |
| self.ScratchFile( |
| 'extension.bzl', |
| [ |
| 'def _repo_rule_impl(ctx):', |
| ' ctx.file("WORKSPACE")', |
| ' ctx.file("BUILD", "filegroup(name=\\"lalo\\")")', |
| 'repo_rule = repository_rule(implementation = _repo_rule_impl)', |
| 'def _module_ext_impl(ctx):', |
| ' print("Hello from the other town!")', |
| ' repo_rule(name= "hello")', |
| ( |
| 'lockfile_ext = module_extension(implementation =' |
| ' _module_ext_impl)' |
| ), |
| ], |
| ) |
| |
| exit_code, _, stderr = self.RunBazel( |
| ['build', '--nobuild', '--lockfile_mode=error', '@hello//:all'], |
| allow_failure=True, |
| ) |
| self.AssertExitCode(exit_code, 48, stderr) |
| self.assertIn( |
| ( |
| 'ERROR: Lock file is no longer up-to-date because: The ' |
| 'implementation of the extension ' |
| "'ModuleExtensionId{bzlFileLabel=//:extension.bzl, " |
| "extensionName=lockfile_ext, isolationKey=Optional.empty}' or one " |
| 'of its transitive .bzl files has changed' |
| ), |
| stderr, |
| ) |
| |
| def testRemoveModuleExtensionsNotUsed(self): |
| # Test that the module extension is removed from the lockfile if it is not |
| # used anymore |
| self.ScratchFile( |
| 'MODULE.bazel', |
| [ |
| 'ext = use_extension("extension.bzl", "ext")', |
| 'use_repo(ext, "hello")', |
| ], |
| ) |
| self.ScratchFile('BUILD.bazel') |
| self.ScratchFile( |
| 'extension.bzl', |
| [ |
| 'def _repo_rule_impl(ctx):', |
| ' ctx.file("WORKSPACE")', |
| ' ctx.file("BUILD", "filegroup(name=\'lala\')")', |
| 'repo_rule = repository_rule(implementation=_repo_rule_impl)', |
| 'def _ext_impl(ctx):', |
| ' repo_rule(name="hello")', |
| 'ext = module_extension(implementation=_ext_impl)', |
| ], |
| ) |
| |
| self.RunBazel(['build', '@hello//:all']) |
| with open(self.Path('MODULE.bazel.lock'), 'r') as f: |
| lockfile = json.loads(f.read().strip()) |
| self.assertEqual( |
| list(lockfile['moduleExtensions'].keys()), ['//:extension.bzl%ext'] |
| ) |
| |
| self.ScratchFile('MODULE.bazel', []) |
| self.RunBazel(['build', '//:all']) |
| with open(self.Path('MODULE.bazel.lock'), 'r') as f: |
| lockfile = json.loads(f.read().strip()) |
| self.assertEqual(len(lockfile['moduleExtensions']), 0) |
| |
| def testNoAbsoluteRootModuleFilePath(self): |
| self.ScratchFile( |
| 'MODULE.bazel', |
| [ |
| 'ext = use_extension("extension.bzl", "ext")', |
| 'ext.dep(generate = True)', |
| 'use_repo(ext, ext_hello = "hello")', |
| 'other_ext = use_extension("extension.bzl", "other_ext")', |
| 'other_ext.dep(generate = False)', |
| 'use_repo(other_ext, other_ext_hello = "hello")', |
| ], |
| ) |
| self.ScratchFile('BUILD.bazel') |
| self.ScratchFile( |
| 'extension.bzl', |
| [ |
| 'def _repo_rule_impl(ctx):', |
| ' ctx.file("WORKSPACE")', |
| ' ctx.file("BUILD", "filegroup(name=\'lala\')")', |
| '', |
| 'repo_rule = repository_rule(implementation=_repo_rule_impl)', |
| '', |
| 'def _module_ext_impl(ctx):', |
| ' for mod in ctx.modules:', |
| ' for dep in mod.tags.dep:', |
| ' if dep.generate:', |
| ' repo_rule(name="hello")', |
| '', |
| '_dep = tag_class(attrs={"generate": attr.bool()})', |
| 'ext = module_extension(', |
| ' implementation=_module_ext_impl,', |
| ' tag_classes={"dep": _dep},', |
| ')', |
| 'other_ext = module_extension(', |
| ' implementation=_module_ext_impl,', |
| ' tag_classes={"dep": _dep},', |
| ')', |
| ], |
| ) |
| |
| # Paths to module files in error message always use forward slashes as |
| # separators, even on Windows. |
| module_file_path = self.Path('MODULE.bazel').replace('\\', '/') |
| |
| self.RunBazel(['build', '--nobuild', '@ext_hello//:all']) |
| with open(self.Path('MODULE.bazel.lock'), 'r') as f: |
| self.assertNotIn(module_file_path, f.read()) |
| |
| self.RunBazel(['shutdown']) |
| exit_code, _, stderr = self.RunBazel( |
| ['build', '--nobuild', '@other_ext_hello//:all'], allow_failure=True |
| ) |
| self.AssertNotExitCode(exit_code, 0, stderr) |
| self.assertIn( |
| ( |
| 'ERROR: module extension "other_ext" from "//:extension.bzl" does ' |
| 'not generate repository "hello", yet it is imported as ' |
| '"other_ext_hello" in the usage at ' |
| + module_file_path |
| + ':4:26' |
| ), |
| stderr, |
| ) |
| |
| |
| if __name__ == '__main__': |
| unittest.main() |