diff --git a/.bazelci/presubmit.yml b/.bazelci/presubmit.yml index 1da4d9fb6b..c9b8bc286d 100644 --- a/.bazelci/presubmit.yml +++ b/.bazelci/presubmit.yml @@ -93,6 +93,36 @@ tasks: <<: *reusable_config name: Default test on Ubuntu platform: ubuntu2004 + ubuntu_bazel_rolling: + <<: *reusable_config + name: "Default test: Ubuntu, Pystar, workspace" + platform: ubuntu2004 + # TODO: Change to "rolling" once + # https://github.com/bazelbuild/bazel/commit/f3aafea59ae021c6a12086cb2cd34c5fa782faf1 + # is available in rolling. + bazel: "last_green" + environment: + RULES_PYTHON_ENABLE_PYSTAR: "1" + test_flags: + # The doc check tests fail because the Starlark implementation makes the + # PyInfo and PyRuntimeInfo symbols become documented. + - "--test_tag_filters=-integration-test,-doc_check_test" + ubuntu_bazel_rolling_bzlmod: + <<: *reusable_config + <<: *common_bzlmod_flags + name: "Default test: Ubuntu, Pystar, bzlmod" + platform: ubuntu2004 + # TODO: Change to "rolling" once + # https://github.com/bazelbuild/bazel/commit/f3aafea59ae021c6a12086cb2cd34c5fa782faf1 + # is available in rolling. + bazel: "last_green" + environment: + RULES_PYTHON_ENABLE_PYSTAR: "1" + test_flags: + # The doc check tests fail because the Starlark implementation makes the + # PyInfo and PyRuntimeInfo symbols become documented. + - "--test_tag_filters=-integration-test,-doc_check_test" + debian: <<: *reusable_config name: Default test on Debian @@ -107,7 +137,6 @@ tasks: platform: windows test_flags: - "--test_tag_filters=-integration-test,-fix-windows" - rbe_min: <<: *minimum_supported_version <<: *reusable_config @@ -262,34 +291,6 @@ tasks: name: multi_python_versions integration tests on Windows working_directory: examples/multi_python_versions platform: windows - - integration_test_pip_install_ubuntu_min: - <<: *minimum_supported_version - <<: *reusable_build_test_all - name: pip_install integration tests on Ubuntu using minimum supported Bazel version - working_directory: examples/pip_install - platform: ubuntu2004 - integration_test_pip_install_ubuntu: - <<: *reusable_build_test_all - name: pip_install integration tests on Ubuntu - working_directory: examples/pip_install - platform: ubuntu2004 - integration_test_pip_install_debian: - <<: *reusable_build_test_all - name: pip_install integration tests on Debian - working_directory: examples/pip_install - platform: debian11 - integration_test_pip_install_macos: - <<: *reusable_build_test_all - name: pip_install integration tests on macOS - working_directory: examples/pip_install - platform: macos - integration_test_pip_install_windows: - <<: *reusable_build_test_all - name: pip_install integration tests on Windows - working_directory: examples/pip_install - platform: windows - integration_test_pip_parse_ubuntu_min: <<: *minimum_supported_version <<: *reusable_build_test_all diff --git a/.bazelignore b/.bazelignore index 135f709824..564eb06195 100644 --- a/.bazelignore +++ b/.bazelignore @@ -6,6 +6,13 @@ bazel-rules_python bazel-bin bazel-out bazel-testlogs +# Prevent the convenience symlinks within the examples from being +# treated as directories with valid BUILD files for the main repo. +# Any directory with a WORKSPACE in it should be added here, with +# an entry like `bazel-{workspacename}` examples/bzlmod/bazel-bzlmod +examples/bzlmod/other_module/bazel-other_module examples/bzlmod_build_file_generation/bazel-bzlmod_build_file_generation +examples/pip_parse/bazel-pip_parse examples/py_proto_library/bazel-py_proto_library +tests/ignore_root_user_error/bazel-ignore_root_user_error diff --git a/.bazelrc b/.bazelrc index 3a5497a071..39b28d12e6 100644 --- a/.bazelrc +++ b/.bazelrc @@ -3,8 +3,8 @@ # This lets us glob() up all the files inside the examples to make them inputs to tests # (Note, we cannot use `common --deleted_packages` because the bazel version command doesn't support it) # To update these lines, run tools/bazel_integration_test/update_deleted_packages.sh -build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_point,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,tests/compile_pip_requirements,tests/compile_pip_requirements_test_from_external_workspace,tests/ignore_root_user_error,tests/pip_repository_entry_points -query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/bzlmod/entry_point,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,tests/compile_pip_requirements,tests/compile_pip_requirements_test_from_external_workspace,tests/ignore_root_user_error,tests/pip_repository_entry_points +build --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,tests/compile_pip_requirements,tests/compile_pip_requirements_test_from_external_workspace,tests/ignore_root_user_error,tests/pip_repository_entry_points +query --deleted_packages=examples/build_file_generation,examples/build_file_generation/random_number_generator,examples/bzlmod,examples/bzlmod/entry_points,examples/bzlmod/entry_points/tests,examples/bzlmod/libs/my_lib,examples/bzlmod/other_module,examples/bzlmod/other_module/other_module/pkg,examples/bzlmod/runfiles,examples/bzlmod/tests,examples/bzlmod/tests/other_module,examples/bzlmod/whl_mods,examples/bzlmod_build_file_generation,examples/bzlmod_build_file_generation/other_module/other_module/pkg,examples/bzlmod_build_file_generation/runfiles,examples/multi_python_versions/libs/my_lib,examples/multi_python_versions/requirements,examples/multi_python_versions/tests,examples/pip_install,examples/pip_parse,examples/pip_parse_vendored,examples/pip_repository_annotations,examples/py_proto_library,tests/compile_pip_requirements,tests/compile_pip_requirements_test_from_external_workspace,tests/ignore_root_user_error,tests/pip_repository_entry_points test --test_output=errors diff --git a/.bcr/config.yml b/.bcr/config.yml index 7bdd70fbaf..7672aa554d 100644 --- a/.bcr/config.yml +++ b/.bcr/config.yml @@ -14,5 +14,5 @@ fixedReleaser: login: f0rmiga - email: thulio@aspect.dev + email: 3149049+f0rmiga@users.noreply.github.com moduleRoots: [".", "gazelle"] diff --git a/BUILD.bazel b/BUILD.bazel index 35a3df892f..4d4d3ec26f 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load(":version.bzl", "BAZEL_VERSION") package(default_visibility = ["//visibility:public"]) @@ -44,6 +45,12 @@ filegroup( ], ) +bzl_library( + name = "version_bzl", + srcs = ["version.bzl"], + visibility = ["//:__subpackages__"], +) + # Reexport of all bzl files used to allow downstream rules to generate docs # without shipping with a dependency on Skylib filegroup( diff --git a/CHANGELOG.md b/CHANGELOG.md index 502545adec..512d820d23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,10 +17,87 @@ A brief description of the categories of changes: * Particular sub-systems are identified using parentheses, e.g. `(bzlmod)` or `(docs)`. +## Unreleased + +### Changed + +* Python version patch level bumps: + * 3.8.15 -> 3.8.18 + * 3.9.17 -> 3.9.18 + * 3.10.12 -> 3.10.13 + * 3.11.4 -> 3.11.6 + +* (deps) Upgrade rules_go 0.39.1 -> 0.41.0; this is so gazelle integration works with upcoming Bazel versions + +* (multi-version) The `distribs` attribute is no longer propagated. This + attribute has been long deprecated by Bazel and shouldn't be used. + +* Calling `//python:repositories.bzl#py_repositories()` is required. It has + always been documented as necessary, but it was possible to omit it in certain + cases. An error about `@rules_python_internal` means the `py_repositories()` + call is missing in `WORKSPACE`. + +* (bzlmod) The `pip.parse` extension will generate os/arch specific lock + file entries on `bazel>=6.4`. + + +### Added + +* (bzlmod, entry_point) Added + [`py_console_script_binary`](./docs/py_console_script_binary.md), which + allows adding custom dependencies to a package's entry points and customizing + the `py_binary` rule used to build it. + +* New Python versions available: `3.8.17`, `3.11.5` using + https://github.com/indygreg/python-build-standalone/releases/tag/20230826. + +* (gazelle) New `# gazelle:python_generation_mode file` directive to support + generating one `py_library` per file. + +* (python_repository) Support `netrc` and `auth_patterns` attributes to enable + authentication against private HTTP hosts serving Python toolchain binaries. + +* `//python:packaging_bzl` added, a `bzl_library` for the Starlark + files `//python:packaging.bzl` requires. +* (py_wheel) Added the `incompatible_normalize_name` feature flag to + normalize the package distribution name according to latest Python + packaging standards. Defaults to `False` for the time being. +* (py_wheel) Added the `incompatible_normalize_version` feature flag + to normalize the package version according to PEP440 standard. This + also adds support for local version specifiers (versions with a `+` + in them), in accordance with PEP440. Defaults to `False` for the + time being. + +* New Python versions available: `3.8.18`, `3.9.18`, `3.10.13`, `3.11.6`, `3.12.0` using + https://github.com/indygreg/python-build-standalone/releases/tag/20231002. + `3.12.0` support is considered beta and may have issues. + +### Removed + +* (bzlmod) The `entry_point` macro is no longer supported and has been removed + in favour of the `py_console_script_binary` macro for `bzlmod` users. + +* (bzlmod) The `pip.parse` no longer generates `{hub_name}_{py_version}` hub repos + as the `entry_point` macro has been superseded by `py_console_script_binary`. + +* (bzlmod) The `pip.parse` no longer generates `{hub_name}_{distribution}` hub repos. + +### Fixed + +* (whl_library) No longer restarts repository rule when fetching external + dependencies improving initial build times involving external dependency + fetching. + +* (gazelle) Improve runfiles lookup hermeticity. + ## [0.25.0] - 2023-08-22 ### Changed +* Python version patch level bumps: + * 3.9.16 -> 3.9.17 + * 3.10.9 -> 3.10.12 + * 3.11.1 -> 3.11.4 * (bzlmod) `pip.parse` can no longer automatically use the default Python version; this was an unreliable and unsafe behavior. The `python_version` arg must always be explicitly specified. @@ -76,5 +153,3 @@ A brief description of the categories of changes: * Expose Python C headers through the toolchain. [0.24.0]: https://github.com/bazelbuild/rules_python/releases/tag/0.24.0 - - diff --git a/MODULE.bazel b/MODULE.bazel index aaa5c86912..e9b06d66ef 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -4,8 +4,9 @@ module( compatibility_level = 1, ) -bazel_dep(name = "platforms", version = "0.0.4") +bazel_dep(name = "bazel_features", version = "1.1.0") bazel_dep(name = "bazel_skylib", version = "1.3.0") +bazel_dep(name = "platforms", version = "0.0.4") # Those are loaded only when using py_proto_library bazel_dep(name = "rules_proto", version = "5.3.0-21.7") @@ -15,6 +16,7 @@ internal_deps = use_extension("@rules_python//python/extensions/private:internal internal_deps.install() use_repo( internal_deps, + "rules_python_internal", # START: maintained by 'bazel run //tools/private:update_pip_deps' "pypi__build", "pypi__click", @@ -26,6 +28,7 @@ use_repo( "pypi__pep517", "pypi__pip", "pypi__pip_tools", + "pypi__pyproject_hooks", "pypi__setuptools", "pypi__tomli", "pypi__wheel", diff --git a/README.md b/README.md index 660e6e20af..10c7d0a4be 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Python Rules for Bazel -[![Build status](https://badge.buildkite.com/1bcfe58b6f5741aacb09b12485969ba7a1205955a45b53e854.svg?branch=main)](https://buildkite.com/bazel/python-rules-python-postsubmit) +[![Build status](https://badge.buildkite.com/0bcfe58b6f5741aacb09b12485969ba7a1205955a45b53e854.svg?branch=main)](https://buildkite.com/bazel/rules-python-python) ## Overview diff --git a/WORKSPACE b/WORKSPACE index 7438bb8257..ad32013816 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -30,11 +30,11 @@ load("//python:versions.bzl", "MINOR_MAPPING") python_register_multi_toolchains( name = "python", - default_version = MINOR_MAPPING.values()[-1], + default_version = MINOR_MAPPING.values()[-2], python_versions = MINOR_MAPPING.values(), ) -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") # Used for Bazel CI http_archive( @@ -72,7 +72,7 @@ _py_gazelle_deps() # Install twine for our own runfiles wheel publishing. # Eventually we might want to install twine automatically for users too, see: # https://github.com/bazelbuild/rules_python/issues/1016. -load("@python//3.11.4:defs.bzl", "interpreter") +load("@python//3.11.6:defs.bzl", "interpreter") load("@rules_python//python:pip.bzl", "pip_parse") pip_parse( @@ -86,3 +86,14 @@ pip_parse( load("@publish_deps//:requirements.bzl", "install_deps") install_deps() + +# This wheel is purely here to validate the wheel extraction code. It's not +# intended for anything else. +http_file( + name = "wheel_for_testing", + downloaded_file_path = "numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + sha256 = "0d60fbae8e0019865fc4784745814cff1c421df5afee233db6d88ab4f14655a2", + urls = [ + "https://files.pythonhosted.org/packages/50/67/3e966d99a07d60a21a21d7ec016e9e4c2642a86fea251ec68677daf71d4d/numpy-1.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", + ], +) diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index 1fb4f81484..0959c35ddd 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -16,7 +16,10 @@ load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load("@bazel_skylib//rules:diff_test.bzl", "diff_test") load("@bazel_skylib//rules:write_file.bzl", "write_file") load("@io_bazel_stardoc//stardoc:stardoc.bzl", "stardoc") +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") # buildifier: disable=bzl-visibility +# NOTE: Only public visibility for historical reasons. +# This package is only for rules_python to generate its own docs. package(default_visibility = ["//visibility:public"]) licenses(["notice"]) # Apache 2.0 @@ -27,71 +30,51 @@ _DOCS = { "pip_repository": "//docs:pip-repository", "py_cc_toolchain": "//docs:py_cc_toolchain-docs", "py_cc_toolchain_info": "//docs:py_cc_toolchain_info-docs", + "py_console_script_binary": "//docs:py-console-script-binary", "python": "//docs:core-docs", } -# We define these bzl_library targets here rather than in the //python package -# because they're only used for doc generation. This way, we avoid requiring -# our users to depend on Skylib. - -bzl_library( - name = "bazel_repo_tools", - srcs = [ - "@bazel_tools//tools:bzl_srcs", - ], +# Temporary compatibility aliases for some other projects depending on the old +# bzl_library targets. +alias( + name = "defs", + actual = "//python:defs_bzl", + deprecation = "Use //python:defs_bzl instead; targets under //docs are internal.", ) -bzl_library( - name = "defs", - srcs = [ - "//python:defs.bzl", - "//python/private:reexports.bzl", - ], - deps = [ - ":bazel_repo_tools", - "//python:defs_bzl", - "//python/private:reexports_bzl", - ], +alias( + name = "bazel_repo_tools", + actual = "//python/private:bazel_tools_bzl", + deprecation = "Use @bazel_tools//tools:bzl_srcs instead; targets under //docs are internal.", ) bzl_library( name = "pip_install_bzl", - srcs = [ - "//python:bzl", - "//python/pip_install:bzl", - ], + deprecation = "Use //python:pip_bzl or //python/pip_install:pip_repository_bzl instead; " + + "targets under //docs are internal.", deps = [ - ":defs", - "//:version.bzl", + "//python:pip_bzl", + "//python/pip_install:pip_repository_bzl", ], ) -bzl_library( +alias( name = "requirements_parser_bzl", - srcs = [ - "//python/pip_install:requirements_parser.bzl", - ], + actual = "//python/pip_install:pip_repository_bzl", + deprecation = "Use //python/pip_install:pip_repository_bzl instead; Both the requirements " + + "parser and targets under //docs are internal", ) -bzl_library( - name = "packaging_bzl", - srcs = [ - "//python:packaging.bzl", - "//python/private:py_package.bzl", - "//python/private:py_wheel.bzl", - "//python/private:stamp.bzl", - "//python/private:util.bzl", - ], - deps = [ - "//python/private:util_bzl", - ], -) +# Empty list means "compatible with all". +# Stardoc+bzlmod doesn't currently work with our docs, so disable trying to +# build it for now. +_COMPATIBLE_PLATFORM = [] if not BZLMOD_ENABLED else ["@platforms//:incompatible"] # TODO: Stardoc does not guarantee consistent outputs accross platforms (Unix/Windows). # As a result we do not build or test docs on Windows. -_NOT_WINDOWS = select({ - "@platforms//os:linux": [], - "@platforms//os:macos": [], +_TARGET_COMPATIBLE_WITH = select({ + "@platforms//os:linux": _COMPATIBLE_PLATFORM, + "@platforms//os:macos": _COMPATIBLE_PLATFORM, "//conditions:default": ["@platforms//:incompatible"], }) @@ -99,19 +82,19 @@ stardoc( name = "core-docs", out = "python.md_", input = "//python:defs.bzl", - target_compatible_with = _NOT_WINDOWS, - deps = [":defs"], + target_compatible_with = _TARGET_COMPATIBLE_WITH, + deps = [ + "//python:defs_bzl", + ], ) stardoc( name = "pip-docs", out = "pip.md_", input = "//python:pip.bzl", - target_compatible_with = _NOT_WINDOWS, + target_compatible_with = _TARGET_COMPATIBLE_WITH, deps = [ - ":bazel_repo_tools", - ":pip_install_bzl", - "@bazel_skylib//lib:versions", + "//python:pip_bzl", ], ) @@ -119,12 +102,19 @@ stardoc( name = "pip-repository", out = "pip_repository.md_", input = "//python/pip_install:pip_repository.bzl", - target_compatible_with = _NOT_WINDOWS, + target_compatible_with = _TARGET_COMPATIBLE_WITH, + deps = [ + "//python/pip_install:pip_repository_bzl", + ], +) + +stardoc( + name = "py-console-script-binary", + out = "py_console_script_binary.md_", + input = "//python/entry_points:py_console_script_binary.bzl", + target_compatible_with = _TARGET_COMPATIBLE_WITH, deps = [ - ":bazel_repo_tools", - ":pip_install_bzl", - ":requirements_parser_bzl", - "@bazel_skylib//lib:versions", + "//python/entry_points:py_console_script_binary_bzl", ], ) @@ -132,8 +122,8 @@ stardoc( name = "packaging-docs", out = "packaging.md_", input = "//python:packaging.bzl", - target_compatible_with = _NOT_WINDOWS, - deps = [":packaging_bzl"], + target_compatible_with = _TARGET_COMPATIBLE_WITH, + deps = ["//python:packaging_bzl"], ) stardoc( @@ -144,7 +134,7 @@ stardoc( # doesn't do anything interesting to users, so bypass it to avoid having to # copy/paste all the rule's doc in the macro. input = "//python/private:py_cc_toolchain_rule.bzl", - target_compatible_with = _NOT_WINDOWS, + target_compatible_with = _TARGET_COMPATIBLE_WITH, deps = ["//python/private:py_cc_toolchain_bzl"], ) @@ -161,7 +151,8 @@ stardoc( failure_message = "Please run: bazel run //docs:update", file1 = k + ".md", file2 = k + ".md_", - target_compatible_with = _NOT_WINDOWS, + tags = ["doc_check_test"], + target_compatible_with = _TARGET_COMPATIBLE_WITH, ) for k in _DOCS.keys() ] @@ -176,12 +167,12 @@ write_file( "cp -fv bazel-bin/docs/{0}.md_ docs/{0}.md".format(k) for k in _DOCS.keys() ], - target_compatible_with = _NOT_WINDOWS, + target_compatible_with = _TARGET_COMPATIBLE_WITH, ) sh_binary( name = "update", srcs = ["update.sh"], data = _DOCS.values(), - target_compatible_with = _NOT_WINDOWS, + target_compatible_with = _TARGET_COMPATIBLE_WITH, ) diff --git a/docs/packaging.md b/docs/packaging.md index 0e8e110ef5..90c66dc1de 100755 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -59,8 +59,9 @@ This also has the advantage that stamping information is included in the wheel's
 py_wheel_rule(name, abi, author, author_email, classifiers, console_scripts, deps,
               description_content_type, description_file, distribution, entry_points,
-              extra_distinfo_files, extra_requires, homepage, license, platform, project_urls,
-              python_requires, python_tag, requires, stamp, strip_path_prefixes, summary, version)
+              extra_distinfo_files, extra_requires, homepage, incompatible_normalize_name,
+              incompatible_normalize_version, license, platform, project_urls, python_requires,
+              python_tag, requires, stamp, strip_path_prefixes, summary, version)
 
Internal rule used by the [py_wheel macro](/docs/packaging.md#py_wheel). @@ -89,6 +90,8 @@ in the way they expect. | extra_distinfo_files | Extra files to add to distinfo directory in the archive. | Dictionary: Label -> String | optional | {} | | extra_requires | List of optional requirements for this package | Dictionary: String -> List of strings | optional | {} | | homepage | A string specifying the URL for the package homepage. | String | optional | "" | +| incompatible_normalize_name | Normalize the package distribution name according to latest Python packaging standards.

See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode and https://packaging.python.org/en/latest/specifications/name-normalization/.

Apart from the valid names according to the above, we also accept '{' and '}', which may be used as placeholders for stamping. | Boolean | optional | False | +| incompatible_normalize_version | Normalize the package version according to PEP440 standard. With this option set to True, if the user wants to pass any stamp variables, they have to be enclosed in '{}', e.g. '{BUILD_TIMESTAMP}'. | Boolean | optional | False | | license | A string specifying the license of the package. | String | optional | "" | | platform | Supported platform. Use 'any' for pure-Python wheel.

If you have included platform-specific data, such as a .pyd or .so extension module, you will need to specify the platform in standard pip format. If you support multiple platforms, you can define platform constraints, then use a select() to specify the appropriate specifier, eg:

platform = select({ "//platforms:windows_x86_64": "win_amd64", "//platforms:macos_x86_64": "macosx_10_7_x86_64", "//platforms:linux_x86_64": "manylinux2014_x86_64", }) | String | optional | "any" | | project_urls | A string dict specifying additional browsable URLs for the project and corresponding labels, where label is the key and url is the value. e.g {{"Bug Tracker": "http://bitbucket.org/tarek/distribute/issues/"}} | Dictionary: String -> String | optional | {} | diff --git a/docs/pip_repository.md b/docs/pip_repository.md index 853605276f..453ca29713 100644 --- a/docs/pip_repository.md +++ b/docs/pip_repository.md @@ -7,7 +7,7 @@ ## pip_hub_repository_bzlmod
-pip_hub_repository_bzlmod(name, repo_mapping, repo_name, whl_library_alias_names)
+pip_hub_repository_bzlmod(name, default_version, repo_mapping, repo_name, whl_map)
 
A rule for bzlmod mulitple pip repository creation. PRIVATE USE ONLY. @@ -18,9 +18,10 @@ A rule for bzlmod mulitple pip repository creation. PRIVATE USE ONLY. | Name | Description | Type | Mandatory | Default | | :------------- | :------------- | :------------- | :------------- | :------------- | | name | A unique name for this repository. | Name | required | | +| default_version | This is the default python version in the format of X.Y.Z. This should match what is setup by the 'python' extension using the 'is_default = True' setting. | String | required | | | repo_mapping | A dictionary from local repository name to global repository name. This allows controls over workspace dependency resolution for dependencies of this repository.<p>For example, an entry "@foo": "@bar" declares that, for any time this repository depends on @foo (such as a dependency on @foo//some:target, it should actually resolve that dependency within globally-declared @bar (@bar//some:target). | Dictionary: String -> String | required | | | repo_name | The apparent name of the repo. This is needed because in bzlmod, the name attribute becomes the canonical name. | String | required | | -| whl_library_alias_names | The list of whl alias that we use to build aliases and the whl names | List of strings | required | | +| whl_map | The wheel map where values are python versions | Dictionary: String -> List of strings | required | | @@ -101,31 +102,6 @@ py_binary( | timeout | Timeout (in seconds) on the rule's execution duration. | Integer | optional | 600 | - - -## pip_repository_bzlmod - -
-pip_repository_bzlmod(name, repo_mapping, repo_name, requirements_darwin, requirements_linux,
-                      requirements_lock, requirements_windows)
-
- -A rule for bzlmod pip_repository creation. Intended for private use only. - -**ATTRIBUTES** - - -| Name | Description | Type | Mandatory | Default | -| :------------- | :------------- | :------------- | :------------- | :------------- | -| name | A unique name for this repository. | Name | required | | -| repo_mapping | A dictionary from local repository name to global repository name. This allows controls over workspace dependency resolution for dependencies of this repository.<p>For example, an entry "@foo": "@bar" declares that, for any time this repository depends on @foo (such as a dependency on @foo//some:target, it should actually resolve that dependency within globally-declared @bar (@bar//some:target). | Dictionary: String -> String | required | | -| repo_name | The apparent name of the repo. This is needed because in bzlmod, the name attribute becomes the canonical name | String | required | | -| requirements_darwin | Override the requirements_lock attribute when the host platform is Mac OS | Label | optional | None | -| requirements_linux | Override the requirements_lock attribute when the host platform is Linux | Label | optional | None | -| requirements_lock | A fully resolved 'requirements.txt' pip requirement file containing the transitive set of your dependencies. If this file is passed instead of 'requirements' no resolve will take place and pip_repository will create individual repositories for each of your dependencies so that wheels are fetched/built only for the targets specified by 'build/run/test'. | Label | optional | None | -| requirements_windows | Override the requirements_lock attribute when the host platform is Windows | Label | optional | None | - - ## whl_library diff --git a/docs/py_console_script_binary.md b/docs/py_console_script_binary.md new file mode 100644 index 0000000000..3d7b5e5bbd --- /dev/null +++ b/docs/py_console_script_binary.md @@ -0,0 +1,87 @@ + + + +Creates an executable (a non-test binary) for console_script entry points. + +Generate a `py_binary` target for a particular console_script `entry_point` +from a PyPI package, e.g. for creating an executable `pylint` target use: +```starlark +load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") + +py_console_script_binary( + name = "pylint", + pkg = "@pip//pylint", +) +``` + +Or for more advanced setups you can also specify extra dependencies and the +exact script name you want to call. It is useful for tools like flake8, pylint, +pytest, which have plugin discovery methods and discover dependencies from the +PyPI packages available in the PYTHONPATH. +```starlark +load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") + +py_console_script_binary( + name = "pylint_with_deps", + pkg = "@pip//pylint", + # Because `pylint` has multiple console_scripts available, we have to + # specify which we want if the name of the target name 'pylint_with_deps' + # cannot be used to guess the entry_point script. + script = "pylint", + deps = [ + # One can add extra dependencies to the entry point. + # This specifically allows us to add plugins to pylint. + "@pip//pylint_print", + ], +) +``` + +A specific Python version can be forced by using the generated version-aware +wrappers, e.g. to force Python 3.9: +```starlark +load("@python_versions//3.9:defs.bzl", "py_console_script_binary") + +py_console_script_binary( + name = "yamllint", + pkg = "@pip//yamllint", +) +``` + +Alternatively, the the `py_console_script_binary.binary_rule` arg can be passed +the version-bound `py_binary` symbol, or any other `py_binary`-compatible rule +of your choosing: +```starlark +load("@python_versions//3.9:defs.bzl", "py_binary") +load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") + +py_console_script_binary( + name = "yamllint", + pkg = "@pip//yamllint:pkg", + binary_rule = py_binary, +) +``` + + + + +## py_console_script_binary + +
+py_console_script_binary(name, pkg, entry_points_txt, script, binary_rule, kwargs)
+
+ +Generate a py_binary for a console_script entry_point. + +**PARAMETERS** + + +| Name | Description | Default Value | +| :------------- | :------------- | :------------- | +| name | str, The name of the resulting target. | none | +| pkg | target, the package for which to generate the script. | none | +| entry_points_txt | optional target, the entry_points.txt file to parse for available console_script values. It may be a single file, or a group of files, but must contain a file named entry_points.txt. If not specified, defaults to the dist_info target in the same package as the pkg Label. | None | +| script | str, The console script name that the py_binary is going to be generated for. Defaults to the normalized name attribute. | None | +| binary_rule | callable, The rule/macro to use to instantiate the target. It's expected to behave like py_binary. Defaults to @rules_python//python:py_binary.bzl#py_binary. | <function py_binary> | +| kwargs | Extra parameters forwarded to binary_rule. | none | + + diff --git a/examples/build_file_generation/BUILD.bazel b/examples/build_file_generation/BUILD.bazel index 928fb128e2..79f62519df 100644 --- a/examples/build_file_generation/BUILD.bazel +++ b/examples/build_file_generation/BUILD.bazel @@ -12,7 +12,6 @@ load("@rules_python_gazelle_plugin//modules_mapping:def.bzl", "modules_mapping") compile_pip_requirements( name = "requirements", - extra_args = ["--allow-unsafe"], requirements_in = "requirements.in", requirements_txt = "requirements_lock.txt", requirements_windows = "requirements_windows.txt", diff --git a/examples/build_file_generation/WORKSPACE b/examples/build_file_generation/WORKSPACE index 7c74835870..03085d86b5 100644 --- a/examples/build_file_generation/WORKSPACE +++ b/examples/build_file_generation/WORKSPACE @@ -20,10 +20,10 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") http_archive( name = "io_bazel_rules_go", - sha256 = "6dc2da7ab4cf5d7bfc7c949776b1b7c733f05e56edc4bcd9022bb249d2e2a996", + sha256 = "278b7ff5a826f3dc10f04feaf0b70d48b68748ccd512d7f98bf442077f043fe3", urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.39.1/rules_go-v0.39.1.zip", - "https://github.com/bazelbuild/rules_go/releases/download/v0.39.1/rules_go-v0.39.1.zip", + "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.41.0/rules_go-v0.41.0.zip", + "https://github.com/bazelbuild/rules_go/releases/download/v0.41.0/rules_go-v0.41.0.zip", ], ) @@ -71,8 +71,11 @@ local_repository( path = "../../gazelle", ) -# Next we load the toolchain from rules_python. -load("@rules_python//python:repositories.bzl", "python_register_toolchains") +# Next we load the setup and toolchain from rules_python. +load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") + +# Perform general setup +py_repositories() # We now register a hermetic Python interpreter rather than relying on a system-installed interpreter. # This toolchain will allow bazel to download a specific python version, and use that version diff --git a/examples/bzlmod/BUILD.bazel b/examples/bzlmod/BUILD.bazel index 3db7751f72..ff14016b85 100644 --- a/examples/bzlmod/BUILD.bazel +++ b/examples/bzlmod/BUILD.bazel @@ -16,7 +16,6 @@ load("@rules_python//python:defs.bzl", "py_binary", "py_library", "py_test") # with pip-compile. compile_pip_requirements_3_9( name = "requirements_3_9", - extra_args = ["--allow-unsafe"], requirements_in = "requirements.in", requirements_txt = "requirements_lock_3_9.txt", requirements_windows = "requirements_windows_3_9.txt", @@ -26,7 +25,6 @@ compile_pip_requirements_3_9( # with pip-compile. compile_pip_requirements_3_10( name = "requirements_3_10", - extra_args = ["--allow-unsafe"], requirements_in = "requirements.in", requirements_txt = "requirements_lock_3_10.txt", requirements_windows = "requirements_windows_3_10.txt", diff --git a/examples/bzlmod/MODULE.bazel b/examples/bzlmod/MODULE.bazel index be9466d883..0d1c7a736b 100644 --- a/examples/bzlmod/MODULE.bazel +++ b/examples/bzlmod/MODULE.bazel @@ -113,10 +113,7 @@ pip.parse( "@whl_mods_hub//:wheel.json": "wheel", }, ) - -# NOTE: The pip_39 repo is only used because the plain `@pip` repo doesn't -# yet support entry points; see https://github.com/bazelbuild/rules_python/issues/1262 -use_repo(pip, "pip", "pip_39") +use_repo(pip, "pip") bazel_dep(name = "other_module", version = "", repo_name = "our_other_module") local_path_override( diff --git a/examples/bzlmod/entry_point/BUILD.bazel b/examples/bzlmod/entry_point/BUILD.bazel deleted file mode 100644 index f68552c3ef..0000000000 --- a/examples/bzlmod/entry_point/BUILD.bazel +++ /dev/null @@ -1,20 +0,0 @@ -load("@pip_39//:requirements.bzl", "entry_point") -load("@rules_python//python:defs.bzl", "py_test") - -alias( - name = "yamllint", - actual = entry_point("yamllint"), -) - -py_test( - name = "entry_point_test", - srcs = ["test_entry_point.py"], - data = [ - ":yamllint", - ], - env = { - "YAMLLINT_ENTRY_POINT": "$(rlocationpath :yamllint)", - }, - main = "test_entry_point.py", - deps = ["@rules_python//python/runfiles"], -) diff --git a/examples/bzlmod/entry_points/BUILD.bazel b/examples/bzlmod/entry_points/BUILD.bazel new file mode 100644 index 0000000000..a0939cb65b --- /dev/null +++ b/examples/bzlmod/entry_points/BUILD.bazel @@ -0,0 +1,33 @@ +load("@python_versions//3.9:defs.bzl", py_console_script_binary_3_9 = "py_console_script_binary") +load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") + +# This is how you can define a `pylint` entrypoint which uses the default python version. +py_console_script_binary( + name = "pylint", + pkg = "@pip//pylint", + visibility = ["//entry_points:__subpackages__"], +) + +# We can also specify extra dependencies for the binary, which is useful for +# tools like flake8, pylint, pytest, which have plugin discovery methods. +py_console_script_binary( + name = "pylint_with_deps", + pkg = "@pip//pylint", + # Because `pylint` has multiple console_scripts available, we have to + # specify which we want if the name of the target name 'pylint_with_deps' + # cannot be used to guess the entry_point script. + script = "pylint", + visibility = ["//entry_points:__subpackages__"], + deps = [ + # One can add extra dependencies to the entry point. + "@pip//pylint_print", + ], +) + +# A specific Python version can be forced by using the generated version-aware +# wrappers, e.g. to force Python 3.9: +py_console_script_binary_3_9( + name = "yamllint", + pkg = "@pip//yamllint:pkg", + visibility = ["//entry_points:__subpackages__"], +) diff --git a/examples/bzlmod/entry_points/tests/BUILD.bazel b/examples/bzlmod/entry_points/tests/BUILD.bazel new file mode 100644 index 0000000000..5a65e9e1a3 --- /dev/null +++ b/examples/bzlmod/entry_points/tests/BUILD.bazel @@ -0,0 +1,63 @@ +load("@bazel_skylib//rules:run_binary.bzl", "run_binary") +load("@rules_python//python:defs.bzl", "py_test") + +# Below are targets for testing the `py_console_script_binary` feature and are +# not part of the example how to use the feature. + +# And a test that we can correctly run `pylint --version` +py_test( + name = "pylint_test", + srcs = ["pylint_test.py"], + data = ["//entry_points:pylint"], + env = { + "ENTRY_POINT": "$(rlocationpath //entry_points:pylint)", + }, + deps = ["@rules_python//python/runfiles"], +) + +# Next run pylint on the file to generate a report. +run_binary( + name = "pylint_report", + srcs = [ + ":file_with_pylint_errors.py", + ], + outs = ["pylint_report.txt"], + args = [ + "--output-format=text:$(location pylint_report.txt)", + "--load-plugins=pylint_print", + # The `exit-zero` ensures that `run_binary` is successful even though there are lint errors. + # We check the generated report in the test below. + "--exit-zero", + "$(location :file_with_pylint_errors.py)", + ], + env = { + # otherwise it may try to create ${HOME}/.cache/pylint + "PYLINTHOME": "./.pylint_home", + }, + tool = "//entry_points:pylint_with_deps", +) + +py_test( + name = "pylint_deps_test", + srcs = ["pylint_deps_test.py"], + data = [ + ":pylint_report", + "//entry_points:pylint_with_deps", + ], + env = { + "ENTRY_POINT": "$(rlocationpath //entry_points:pylint_with_deps)", + "PYLINT_REPORT": "$(rlocationpath :pylint_report)", + }, + deps = ["@rules_python//python/runfiles"], +) + +# And a test to check that yamllint works +py_test( + name = "yamllint_test", + srcs = ["yamllint_test.py"], + data = ["//entry_points:yamllint"], + env = { + "ENTRY_POINT": "$(rlocationpath //entry_points:yamllint)", + }, + deps = ["@rules_python//python/runfiles"], +) diff --git a/examples/bzlmod/entry_points/tests/file_with_pylint_errors.py b/examples/bzlmod/entry_points/tests/file_with_pylint_errors.py new file mode 100644 index 0000000000..bb3dbab660 --- /dev/null +++ b/examples/bzlmod/entry_points/tests/file_with_pylint_errors.py @@ -0,0 +1,6 @@ +""" +A file to demonstrate the pylint-print checker works. +""" + +if __name__ == "__main__": + print("Hello, World!") diff --git a/examples/bzlmod/entry_points/tests/pylint_deps_test.py b/examples/bzlmod/entry_points/tests/pylint_deps_test.py new file mode 100644 index 0000000000..f6743ce9b5 --- /dev/null +++ b/examples/bzlmod/entry_points/tests/pylint_deps_test.py @@ -0,0 +1,72 @@ +# 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. + +import os +import pathlib +import subprocess +import tempfile +import unittest + +from python.runfiles import runfiles + + +class ExampleTest(unittest.TestCase): + def __init__(self, *args, **kwargs): + self.maxDiff = None + + super().__init__(*args, **kwargs) + + def test_pylint_entry_point(self): + rlocation_path = os.environ.get("ENTRY_POINT") + assert ( + rlocation_path is not None + ), "expected 'ENTRY_POINT' env variable to be set to rlocation of the tool" + + entry_point = pathlib.Path(runfiles.Create().Rlocation(rlocation_path)) + self.assertTrue(entry_point.exists(), f"'{entry_point}' does not exist") + + # Let's run the entrypoint and check the tool version. + # + # NOTE @aignas 2023-08-24: the Windows python launcher with Python 3.9 and bazel 6 is not happy if we start + # passing extra files via `subprocess.run` and it starts to fail with an error that the file which is the + # entry_point cannot be found. However, just calling `--version` seems to be fine. + proc = subprocess.run( + [str(entry_point), "--version"], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + self.assertEqual( + "", + proc.stderr.decode("utf-8").strip(), + ) + self.assertRegex(proc.stdout.decode("utf-8").strip(), "^pylint 2\.15\.9") + + def test_pylint_report_has_expected_warnings(self): + rlocation_path = os.environ.get("PYLINT_REPORT") + assert ( + rlocation_path is not None + ), "expected 'PYLINT_REPORT' env variable to be set to rlocation of the report" + + pylint_report = pathlib.Path(runfiles.Create().Rlocation(rlocation_path)) + self.assertTrue(pylint_report.exists(), f"'{pylint_report}' does not exist") + + self.assertRegex( + pylint_report.read_text().strip(), + "W8201: Logging should be used instead of the print\(\) function\. \(print-function\)", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/examples/bzlmod/entry_points/tests/pylint_test.py b/examples/bzlmod/entry_points/tests/pylint_test.py new file mode 100644 index 0000000000..c2532938d8 --- /dev/null +++ b/examples/bzlmod/entry_points/tests/pylint_test.py @@ -0,0 +1,57 @@ +# 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. + +import os +import pathlib +import subprocess +import unittest + +from python.runfiles import runfiles + + +class ExampleTest(unittest.TestCase): + def __init__(self, *args, **kwargs): + self.maxDiff = None + + super().__init__(*args, **kwargs) + + def test_pylint_entry_point(self): + rlocation_path = os.environ.get("ENTRY_POINT") + assert ( + rlocation_path is not None + ), "expected 'ENTRY_POINT' env variable to be set to rlocation of the tool" + + entry_point = pathlib.Path(runfiles.Create().Rlocation(rlocation_path)) + self.assertTrue(entry_point.exists(), f"'{entry_point}' does not exist") + + # Let's run the entrypoint and check the tool version. + # + # NOTE @aignas 2023-08-24: the Windows python launcher with Python 3.9 and bazel 6 is not happy if we start + # passing extra files via `subprocess.run` and it starts to fail with an error that the file which is the + # entry_point cannot be found. However, just calling `--version` seems to be fine. + proc = subprocess.run( + [str(entry_point), "--version"], + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + self.assertEqual( + "", + proc.stderr.decode("utf-8").strip(), + ) + self.assertRegex(proc.stdout.decode("utf-8").strip(), "^pylint 2\.15\.9") + + +if __name__ == "__main__": + unittest.main() diff --git a/examples/bzlmod/entry_point/test_entry_point.py b/examples/bzlmod/entry_points/tests/yamllint_test.py similarity index 64% rename from examples/bzlmod/entry_point/test_entry_point.py rename to examples/bzlmod/entry_points/tests/yamllint_test.py index 5a37458348..0a0235793b 100644 --- a/examples/bzlmod/entry_point/test_entry_point.py +++ b/examples/bzlmod/entry_points/tests/yamllint_test.py @@ -21,15 +21,25 @@ class ExampleTest(unittest.TestCase): - def test_entry_point(self): - rlocation_path = os.environ.get("YAMLLINT_ENTRY_POINT") + def __init__(self, *args, **kwargs): + self.maxDiff = None + + super().__init__(*args, **kwargs) + + def test_yamllint_entry_point(self): + rlocation_path = os.environ.get("ENTRY_POINT") assert ( rlocation_path is not None - ), "expected 'YAMLLINT_ENTRY_POINT' env variable to be set to rlocation of the tool" + ), "expected 'ENTRY_POINT' env variable to be set to rlocation of the tool" entry_point = pathlib.Path(runfiles.Create().Rlocation(rlocation_path)) self.assertTrue(entry_point.exists(), f"'{entry_point}' does not exist") + # Let's run the entrypoint and check the tool version. + # + # NOTE @aignas 2023-08-24: the Windows python launcher with Python 3.9 and bazel 6 is not happy if we start + # passing extra files via `subprocess.run` and it starts to fail with an error that the file which is the + # entry_point cannot be found. However, just calling `--version` seems to be fine. proc = subprocess.run( [str(entry_point), "--version"], check=True, diff --git a/examples/bzlmod/requirements.in b/examples/bzlmod/requirements.in index 47cdcf1ea8..702e15103d 100644 --- a/examples/bzlmod/requirements.in +++ b/examples/bzlmod/requirements.in @@ -7,4 +7,5 @@ s3cmd~=2.1.0 yamllint>=1.28.0 tabulate~=0.9.0 pylint~=2.15.5 +pylint-print python-dateutil>=2.8.2 diff --git a/examples/bzlmod/requirements_lock_3_10.txt b/examples/bzlmod/requirements_lock_3_10.txt index e3a185ac88..7f9bd3ac4a 100644 --- a/examples/bzlmod/requirements_lock_3_10.txt +++ b/examples/bzlmod/requirements_lock_3_10.txt @@ -83,6 +83,12 @@ platformdirs==3.5.1 \ pylint==2.15.10 \ --hash=sha256:9df0d07e8948a1c3ffa3b6e2d7e6e63d9fb457c5da5b961ed63106594780cc7e \ --hash=sha256:b3dc5ef7d33858f297ac0d06cc73862f01e4f2e74025ec3eff347ce0bc60baf5 + # via + # -r requirements.in + # pylint-print +pylint-print==1.0.1 \ + --hash=sha256:30aa207e9718ebf4ceb47fb87012092e6d8743aab932aa07aa14a73e750ad3d0 \ + --hash=sha256:a2b2599e7887b93e551db2624c523c1e6e9e58c3be8416cd98d41e4427e2669b # via -r requirements.in python-dateutil==2.8.2 \ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ diff --git a/examples/bzlmod/requirements_lock_3_9.txt b/examples/bzlmod/requirements_lock_3_9.txt index ba1d4d7148..a3bfba22ad 100644 --- a/examples/bzlmod/requirements_lock_3_9.txt +++ b/examples/bzlmod/requirements_lock_3_9.txt @@ -66,6 +66,12 @@ platformdirs==2.6.0 \ pylint==2.15.9 \ --hash=sha256:18783cca3cfee5b83c6c5d10b3cdb66c6594520ffae61890858fe8d932e1c6b4 \ --hash=sha256:349c8cd36aede4d50a0754a8c0218b43323d13d5d88f4b2952ddfe3e169681eb + # via + # -r requirements.in + # pylint-print +pylint-print==1.0.1 \ + --hash=sha256:30aa207e9718ebf4ceb47fb87012092e6d8743aab932aa07aa14a73e750ad3d0 \ + --hash=sha256:a2b2599e7887b93e551db2624c523c1e6e9e58c3be8416cd98d41e4427e2669b # via -r requirements.in python-dateutil==2.8.2 \ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ @@ -127,10 +133,6 @@ s3cmd==2.1.0 \ --hash=sha256:49cd23d516b17974b22b611a95ce4d93fe326feaa07320bd1d234fed68cbccfa \ --hash=sha256:966b0a494a916fc3b4324de38f089c86c70ee90e8e1cae6d59102103a4c0cc03 # via -r requirements.in -setuptools==65.6.3 \ - --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ - --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 - # via yamllint six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 @@ -303,3 +305,9 @@ yamllint==1.28.0 \ --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b # via -r requirements.in + +# The following packages are considered to be unsafe in a requirements file: +setuptools==65.6.3 \ + --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ + --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 + # via yamllint diff --git a/examples/bzlmod/requirements_windows_3_10.txt b/examples/bzlmod/requirements_windows_3_10.txt index 9a28ae8687..a8f05add6b 100644 --- a/examples/bzlmod/requirements_windows_3_10.txt +++ b/examples/bzlmod/requirements_windows_3_10.txt @@ -87,6 +87,12 @@ platformdirs==3.5.1 \ pylint==2.15.10 \ --hash=sha256:9df0d07e8948a1c3ffa3b6e2d7e6e63d9fb457c5da5b961ed63106594780cc7e \ --hash=sha256:b3dc5ef7d33858f297ac0d06cc73862f01e4f2e74025ec3eff347ce0bc60baf5 + # via + # -r requirements.in + # pylint-print +pylint-print==1.0.1 \ + --hash=sha256:30aa207e9718ebf4ceb47fb87012092e6d8743aab932aa07aa14a73e750ad3d0 \ + --hash=sha256:a2b2599e7887b93e551db2624c523c1e6e9e58c3be8416cd98d41e4427e2669b # via -r requirements.in python-dateutil==2.8.2 \ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ diff --git a/examples/bzlmod/requirements_windows_3_9.txt b/examples/bzlmod/requirements_windows_3_9.txt index 08f0979d52..2681ff2a00 100644 --- a/examples/bzlmod/requirements_windows_3_9.txt +++ b/examples/bzlmod/requirements_windows_3_9.txt @@ -70,6 +70,12 @@ platformdirs==2.6.0 \ pylint==2.15.9 \ --hash=sha256:18783cca3cfee5b83c6c5d10b3cdb66c6594520ffae61890858fe8d932e1c6b4 \ --hash=sha256:349c8cd36aede4d50a0754a8c0218b43323d13d5d88f4b2952ddfe3e169681eb + # via + # -r requirements.in + # pylint-print +pylint-print==1.0.1 \ + --hash=sha256:30aa207e9718ebf4ceb47fb87012092e6d8743aab932aa07aa14a73e750ad3d0 \ + --hash=sha256:a2b2599e7887b93e551db2624c523c1e6e9e58c3be8416cd98d41e4427e2669b # via -r requirements.in python-dateutil==2.8.2 \ --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \ @@ -131,10 +137,6 @@ s3cmd==2.1.0 \ --hash=sha256:49cd23d516b17974b22b611a95ce4d93fe326feaa07320bd1d234fed68cbccfa \ --hash=sha256:966b0a494a916fc3b4324de38f089c86c70ee90e8e1cae6d59102103a4c0cc03 # via -r requirements.in -setuptools==65.6.3 \ - --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ - --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 - # via yamllint six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 @@ -307,3 +309,9 @@ yamllint==1.28.0 \ --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b # via -r requirements.in + +# The following packages are considered to be unsafe in a requirements file: +setuptools==65.6.3 \ + --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ + --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 + # via yamllint diff --git a/examples/bzlmod_build_file_generation/BUILD.bazel b/examples/bzlmod_build_file_generation/BUILD.bazel index c5e27c2d49..9b2e5bdce4 100644 --- a/examples/bzlmod_build_file_generation/BUILD.bazel +++ b/examples/bzlmod_build_file_generation/BUILD.bazel @@ -17,7 +17,6 @@ load("@rules_python_gazelle_plugin//modules_mapping:def.bzl", "modules_mapping") # with pip-compile. compile_pip_requirements( name = "requirements", - extra_args = ["--allow-unsafe"], requirements_in = "requirements.in", requirements_txt = "requirements_lock.txt", requirements_windows = "requirements_windows.txt", @@ -53,6 +52,7 @@ gazelle_python_manifest( "//:requirements_lock.txt", "//:requirements_windows.txt", ], + tags = ["exclusive"], # NOTE: we can use this flag in order to make our setup compatible with # bzlmod. use_pip_repository_aliases = True, diff --git a/examples/bzlmod_build_file_generation/gazelle_python.yaml b/examples/bzlmod_build_file_generation/gazelle_python.yaml index e33021b9c8..0c7f14876e 100644 --- a/examples/bzlmod_build_file_generation/gazelle_python.yaml +++ b/examples/bzlmod_build_file_generation/gazelle_python.yaml @@ -232,7 +232,6 @@ manifest: isort.wrap: isort isort.wrap_modes: isort lazy_object_proxy: lazy_object_proxy - lazy_object_proxy.cext: lazy_object_proxy lazy_object_proxy.compat: lazy_object_proxy lazy_object_proxy.simple: lazy_object_proxy lazy_object_proxy.slots: lazy_object_proxy @@ -588,4 +587,4 @@ manifest: pip_repository: name: pip use_pip_repository_aliases: true -integrity: cee7684391c4a8a1ff219cd354deae61cdcdee70f2076789aabd5249f3c4eca9 +integrity: 369584d55f2168d92c415f4c4ab4bc9d2d21a7fb0b0a6749437fcc771fd2f254 diff --git a/examples/bzlmod_build_file_generation/requirements_lock.txt b/examples/bzlmod_build_file_generation/requirements_lock.txt index 2160fe1163..3fd053f777 100644 --- a/examples/bzlmod_build_file_generation/requirements_lock.txt +++ b/examples/bzlmod_build_file_generation/requirements_lock.txt @@ -125,10 +125,6 @@ s3cmd==2.1.0 \ --hash=sha256:49cd23d516b17974b22b611a95ce4d93fe326feaa07320bd1d234fed68cbccfa \ --hash=sha256:966b0a494a916fc3b4324de38f089c86c70ee90e8e1cae6d59102103a4c0cc03 # via -r requirements.in -setuptools==65.6.3 \ - --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ - --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 - # via yamllint six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 @@ -225,3 +221,9 @@ yamllint==1.28.0 \ --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b # via -r requirements.in + +# The following packages are considered to be unsafe in a requirements file: +setuptools==65.6.3 \ + --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ + --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 + # via yamllint diff --git a/examples/bzlmod_build_file_generation/requirements_windows.txt b/examples/bzlmod_build_file_generation/requirements_windows.txt index 06cfdc332c..15e92288dc 100644 --- a/examples/bzlmod_build_file_generation/requirements_windows.txt +++ b/examples/bzlmod_build_file_generation/requirements_windows.txt @@ -129,10 +129,6 @@ s3cmd==2.1.0 \ --hash=sha256:49cd23d516b17974b22b611a95ce4d93fe326feaa07320bd1d234fed68cbccfa \ --hash=sha256:966b0a494a916fc3b4324de38f089c86c70ee90e8e1cae6d59102103a4c0cc03 # via -r requirements.in -setuptools==65.6.3 \ - --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ - --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 - # via yamllint six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 @@ -229,3 +225,9 @@ yamllint==1.28.0 \ --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b # via -r requirements.in + +# The following packages are considered to be unsafe in a requirements file: +setuptools==65.6.3 \ + --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ + --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 + # via yamllint diff --git a/examples/multi_python_versions/requirements/BUILD.bazel b/examples/multi_python_versions/requirements/BUILD.bazel index e3184c8ac5..e3e821a68d 100644 --- a/examples/multi_python_versions/requirements/BUILD.bazel +++ b/examples/multi_python_versions/requirements/BUILD.bazel @@ -5,28 +5,24 @@ load("@python//3.9:defs.bzl", compile_pip_requirements_3_9 = "compile_pip_requir compile_pip_requirements_3_8( name = "requirements_3_8", - extra_args = ["--allow-unsafe"], requirements_in = "requirements.in", requirements_txt = "requirements_lock_3_8.txt", ) compile_pip_requirements_3_9( name = "requirements_3_9", - extra_args = ["--allow-unsafe"], requirements_in = "requirements.in", requirements_txt = "requirements_lock_3_9.txt", ) compile_pip_requirements_3_10( name = "requirements_3_10", - extra_args = ["--allow-unsafe"], requirements_in = "requirements.in", requirements_txt = "requirements_lock_3_10.txt", ) compile_pip_requirements_3_11( name = "requirements_3_11", - extra_args = ["--allow-unsafe"], requirements_in = "requirements.in", requirements_txt = "requirements_lock_3_11.txt", ) diff --git a/examples/multi_python_versions/requirements/requirements_lock_3_10.txt b/examples/multi_python_versions/requirements/requirements_lock_3_10.txt index 6bee4e0030..4910d13844 100644 --- a/examples/multi_python_versions/requirements/requirements_lock_3_10.txt +++ b/examples/multi_python_versions/requirements/requirements_lock_3_10.txt @@ -4,53 +4,75 @@ # # bazel run //requirements:requirements_3_10.update # -websockets==10.3 \ - --hash=sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af \ - --hash=sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c \ - --hash=sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76 \ - --hash=sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47 \ - --hash=sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69 \ - --hash=sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079 \ - --hash=sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c \ - --hash=sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55 \ - --hash=sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02 \ - --hash=sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559 \ - --hash=sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3 \ - --hash=sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e \ - --hash=sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978 \ - --hash=sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98 \ - --hash=sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae \ - --hash=sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755 \ - --hash=sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d \ - --hash=sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991 \ - --hash=sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1 \ - --hash=sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680 \ - --hash=sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247 \ - --hash=sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f \ - --hash=sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2 \ - --hash=sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7 \ - --hash=sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4 \ - --hash=sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667 \ - --hash=sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb \ - --hash=sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094 \ - --hash=sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36 \ - --hash=sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79 \ - --hash=sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500 \ - --hash=sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e \ - --hash=sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582 \ - --hash=sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442 \ - --hash=sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd \ - --hash=sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6 \ - --hash=sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731 \ - --hash=sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4 \ - --hash=sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d \ - --hash=sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8 \ - --hash=sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f \ - --hash=sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677 \ - --hash=sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8 \ - --hash=sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9 \ - --hash=sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e \ - --hash=sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b \ - --hash=sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916 \ - --hash=sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4 +websockets==11.0.3 \ + --hash=sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd \ + --hash=sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f \ + --hash=sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998 \ + --hash=sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82 \ + --hash=sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788 \ + --hash=sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa \ + --hash=sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f \ + --hash=sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4 \ + --hash=sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7 \ + --hash=sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f \ + --hash=sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd \ + --hash=sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69 \ + --hash=sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb \ + --hash=sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b \ + --hash=sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016 \ + --hash=sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac \ + --hash=sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4 \ + --hash=sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb \ + --hash=sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99 \ + --hash=sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e \ + --hash=sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54 \ + --hash=sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf \ + --hash=sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007 \ + --hash=sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3 \ + --hash=sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6 \ + --hash=sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86 \ + --hash=sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1 \ + --hash=sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61 \ + --hash=sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11 \ + --hash=sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8 \ + --hash=sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f \ + --hash=sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931 \ + --hash=sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526 \ + --hash=sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016 \ + --hash=sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae \ + --hash=sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd \ + --hash=sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b \ + --hash=sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311 \ + --hash=sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af \ + --hash=sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152 \ + --hash=sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288 \ + --hash=sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de \ + --hash=sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97 \ + --hash=sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d \ + --hash=sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d \ + --hash=sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca \ + --hash=sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0 \ + --hash=sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9 \ + --hash=sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b \ + --hash=sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e \ + --hash=sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128 \ + --hash=sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d \ + --hash=sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c \ + --hash=sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5 \ + --hash=sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6 \ + --hash=sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b \ + --hash=sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b \ + --hash=sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280 \ + --hash=sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c \ + --hash=sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c \ + --hash=sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f \ + --hash=sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20 \ + --hash=sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8 \ + --hash=sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb \ + --hash=sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602 \ + --hash=sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf \ + --hash=sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0 \ + --hash=sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74 \ + --hash=sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0 \ + --hash=sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564 # via -r requirements/requirements.in diff --git a/examples/multi_python_versions/requirements/requirements_lock_3_11.txt b/examples/multi_python_versions/requirements/requirements_lock_3_11.txt index a437a397d0..35666b54b1 100644 --- a/examples/multi_python_versions/requirements/requirements_lock_3_11.txt +++ b/examples/multi_python_versions/requirements/requirements_lock_3_11.txt @@ -4,53 +4,75 @@ # # bazel run //requirements:requirements_3_11.update # -websockets==10.3 \ - --hash=sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af \ - --hash=sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c \ - --hash=sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76 \ - --hash=sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47 \ - --hash=sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69 \ - --hash=sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079 \ - --hash=sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c \ - --hash=sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55 \ - --hash=sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02 \ - --hash=sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559 \ - --hash=sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3 \ - --hash=sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e \ - --hash=sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978 \ - --hash=sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98 \ - --hash=sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae \ - --hash=sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755 \ - --hash=sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d \ - --hash=sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991 \ - --hash=sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1 \ - --hash=sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680 \ - --hash=sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247 \ - --hash=sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f \ - --hash=sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2 \ - --hash=sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7 \ - --hash=sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4 \ - --hash=sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667 \ - --hash=sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb \ - --hash=sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094 \ - --hash=sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36 \ - --hash=sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79 \ - --hash=sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500 \ - --hash=sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e \ - --hash=sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582 \ - --hash=sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442 \ - --hash=sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd \ - --hash=sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6 \ - --hash=sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731 \ - --hash=sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4 \ - --hash=sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d \ - --hash=sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8 \ - --hash=sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f \ - --hash=sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677 \ - --hash=sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8 \ - --hash=sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9 \ - --hash=sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e \ - --hash=sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b \ - --hash=sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916 \ - --hash=sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4 +websockets==11.0.3 \ + --hash=sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd \ + --hash=sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f \ + --hash=sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998 \ + --hash=sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82 \ + --hash=sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788 \ + --hash=sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa \ + --hash=sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f \ + --hash=sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4 \ + --hash=sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7 \ + --hash=sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f \ + --hash=sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd \ + --hash=sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69 \ + --hash=sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb \ + --hash=sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b \ + --hash=sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016 \ + --hash=sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac \ + --hash=sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4 \ + --hash=sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb \ + --hash=sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99 \ + --hash=sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e \ + --hash=sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54 \ + --hash=sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf \ + --hash=sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007 \ + --hash=sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3 \ + --hash=sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6 \ + --hash=sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86 \ + --hash=sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1 \ + --hash=sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61 \ + --hash=sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11 \ + --hash=sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8 \ + --hash=sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f \ + --hash=sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931 \ + --hash=sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526 \ + --hash=sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016 \ + --hash=sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae \ + --hash=sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd \ + --hash=sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b \ + --hash=sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311 \ + --hash=sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af \ + --hash=sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152 \ + --hash=sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288 \ + --hash=sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de \ + --hash=sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97 \ + --hash=sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d \ + --hash=sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d \ + --hash=sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca \ + --hash=sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0 \ + --hash=sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9 \ + --hash=sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b \ + --hash=sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e \ + --hash=sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128 \ + --hash=sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d \ + --hash=sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c \ + --hash=sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5 \ + --hash=sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6 \ + --hash=sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b \ + --hash=sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b \ + --hash=sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280 \ + --hash=sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c \ + --hash=sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c \ + --hash=sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f \ + --hash=sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20 \ + --hash=sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8 \ + --hash=sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb \ + --hash=sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602 \ + --hash=sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf \ + --hash=sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0 \ + --hash=sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74 \ + --hash=sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0 \ + --hash=sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564 # via -r requirements/requirements.in diff --git a/examples/multi_python_versions/requirements/requirements_lock_3_8.txt b/examples/multi_python_versions/requirements/requirements_lock_3_8.txt index 19303f8eff..10b5df4830 100644 --- a/examples/multi_python_versions/requirements/requirements_lock_3_8.txt +++ b/examples/multi_python_versions/requirements/requirements_lock_3_8.txt @@ -4,53 +4,75 @@ # # bazel run //requirements:requirements_3_8.update # -websockets==10.3 \ - --hash=sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af \ - --hash=sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c \ - --hash=sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76 \ - --hash=sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47 \ - --hash=sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69 \ - --hash=sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079 \ - --hash=sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c \ - --hash=sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55 \ - --hash=sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02 \ - --hash=sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559 \ - --hash=sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3 \ - --hash=sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e \ - --hash=sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978 \ - --hash=sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98 \ - --hash=sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae \ - --hash=sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755 \ - --hash=sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d \ - --hash=sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991 \ - --hash=sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1 \ - --hash=sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680 \ - --hash=sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247 \ - --hash=sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f \ - --hash=sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2 \ - --hash=sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7 \ - --hash=sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4 \ - --hash=sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667 \ - --hash=sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb \ - --hash=sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094 \ - --hash=sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36 \ - --hash=sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79 \ - --hash=sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500 \ - --hash=sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e \ - --hash=sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582 \ - --hash=sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442 \ - --hash=sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd \ - --hash=sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6 \ - --hash=sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731 \ - --hash=sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4 \ - --hash=sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d \ - --hash=sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8 \ - --hash=sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f \ - --hash=sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677 \ - --hash=sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8 \ - --hash=sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9 \ - --hash=sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e \ - --hash=sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b \ - --hash=sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916 \ - --hash=sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4 +websockets==11.0.3 \ + --hash=sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd \ + --hash=sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f \ + --hash=sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998 \ + --hash=sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82 \ + --hash=sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788 \ + --hash=sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa \ + --hash=sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f \ + --hash=sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4 \ + --hash=sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7 \ + --hash=sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f \ + --hash=sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd \ + --hash=sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69 \ + --hash=sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb \ + --hash=sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b \ + --hash=sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016 \ + --hash=sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac \ + --hash=sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4 \ + --hash=sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb \ + --hash=sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99 \ + --hash=sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e \ + --hash=sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54 \ + --hash=sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf \ + --hash=sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007 \ + --hash=sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3 \ + --hash=sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6 \ + --hash=sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86 \ + --hash=sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1 \ + --hash=sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61 \ + --hash=sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11 \ + --hash=sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8 \ + --hash=sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f \ + --hash=sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931 \ + --hash=sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526 \ + --hash=sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016 \ + --hash=sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae \ + --hash=sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd \ + --hash=sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b \ + --hash=sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311 \ + --hash=sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af \ + --hash=sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152 \ + --hash=sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288 \ + --hash=sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de \ + --hash=sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97 \ + --hash=sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d \ + --hash=sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d \ + --hash=sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca \ + --hash=sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0 \ + --hash=sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9 \ + --hash=sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b \ + --hash=sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e \ + --hash=sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128 \ + --hash=sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d \ + --hash=sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c \ + --hash=sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5 \ + --hash=sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6 \ + --hash=sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b \ + --hash=sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b \ + --hash=sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280 \ + --hash=sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c \ + --hash=sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c \ + --hash=sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f \ + --hash=sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20 \ + --hash=sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8 \ + --hash=sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb \ + --hash=sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602 \ + --hash=sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf \ + --hash=sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0 \ + --hash=sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74 \ + --hash=sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0 \ + --hash=sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564 # via -r requirements/requirements.in diff --git a/examples/multi_python_versions/requirements/requirements_lock_3_9.txt b/examples/multi_python_versions/requirements/requirements_lock_3_9.txt index 4af42ca277..0001f88d48 100644 --- a/examples/multi_python_versions/requirements/requirements_lock_3_9.txt +++ b/examples/multi_python_versions/requirements/requirements_lock_3_9.txt @@ -4,53 +4,75 @@ # # bazel run //requirements:requirements_3_9.update # -websockets==10.3 \ - --hash=sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af \ - --hash=sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c \ - --hash=sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76 \ - --hash=sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47 \ - --hash=sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69 \ - --hash=sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079 \ - --hash=sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c \ - --hash=sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55 \ - --hash=sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02 \ - --hash=sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559 \ - --hash=sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3 \ - --hash=sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e \ - --hash=sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978 \ - --hash=sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98 \ - --hash=sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae \ - --hash=sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755 \ - --hash=sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d \ - --hash=sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991 \ - --hash=sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1 \ - --hash=sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680 \ - --hash=sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247 \ - --hash=sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f \ - --hash=sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2 \ - --hash=sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7 \ - --hash=sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4 \ - --hash=sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667 \ - --hash=sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb \ - --hash=sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094 \ - --hash=sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36 \ - --hash=sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79 \ - --hash=sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500 \ - --hash=sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e \ - --hash=sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582 \ - --hash=sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442 \ - --hash=sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd \ - --hash=sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6 \ - --hash=sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731 \ - --hash=sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4 \ - --hash=sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d \ - --hash=sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8 \ - --hash=sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f \ - --hash=sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677 \ - --hash=sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8 \ - --hash=sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9 \ - --hash=sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e \ - --hash=sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b \ - --hash=sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916 \ - --hash=sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4 +websockets==11.0.3 \ + --hash=sha256:01f5567d9cf6f502d655151645d4e8b72b453413d3819d2b6f1185abc23e82dd \ + --hash=sha256:03aae4edc0b1c68498f41a6772d80ac7c1e33c06c6ffa2ac1c27a07653e79d6f \ + --hash=sha256:0ac56b661e60edd453585f4bd68eb6a29ae25b5184fd5ba51e97652580458998 \ + --hash=sha256:0ee68fe502f9031f19d495dae2c268830df2760c0524cbac5d759921ba8c8e82 \ + --hash=sha256:1553cb82942b2a74dd9b15a018dce645d4e68674de2ca31ff13ebc2d9f283788 \ + --hash=sha256:1a073fc9ab1c8aff37c99f11f1641e16da517770e31a37265d2755282a5d28aa \ + --hash=sha256:1d2256283fa4b7f4c7d7d3e84dc2ece74d341bce57d5b9bf385df109c2a1a82f \ + --hash=sha256:1d5023a4b6a5b183dc838808087033ec5df77580485fc533e7dab2567851b0a4 \ + --hash=sha256:1fdf26fa8a6a592f8f9235285b8affa72748dc12e964a5518c6c5e8f916716f7 \ + --hash=sha256:2529338a6ff0eb0b50c7be33dc3d0e456381157a31eefc561771ee431134a97f \ + --hash=sha256:279e5de4671e79a9ac877427f4ac4ce93751b8823f276b681d04b2156713b9dd \ + --hash=sha256:2d903ad4419f5b472de90cd2d40384573b25da71e33519a67797de17ef849b69 \ + --hash=sha256:332d126167ddddec94597c2365537baf9ff62dfcc9db4266f263d455f2f031cb \ + --hash=sha256:34fd59a4ac42dff6d4681d8843217137f6bc85ed29722f2f7222bd619d15e95b \ + --hash=sha256:3580dd9c1ad0701169e4d6fc41e878ffe05e6bdcaf3c412f9d559389d0c9e016 \ + --hash=sha256:3ccc8a0c387629aec40f2fc9fdcb4b9d5431954f934da3eaf16cdc94f67dbfac \ + --hash=sha256:41f696ba95cd92dc047e46b41b26dd24518384749ed0d99bea0a941ca87404c4 \ + --hash=sha256:42cc5452a54a8e46a032521d7365da775823e21bfba2895fb7b77633cce031bb \ + --hash=sha256:4841ed00f1026dfbced6fca7d963c4e7043aa832648671b5138008dc5a8f6d99 \ + --hash=sha256:4b253869ea05a5a073ebfdcb5cb3b0266a57c3764cf6fe114e4cd90f4bfa5f5e \ + --hash=sha256:54c6e5b3d3a8936a4ab6870d46bdd6ec500ad62bde9e44462c32d18f1e9a8e54 \ + --hash=sha256:619d9f06372b3a42bc29d0cd0354c9bb9fb39c2cbc1a9c5025b4538738dbffaf \ + --hash=sha256:6505c1b31274723ccaf5f515c1824a4ad2f0d191cec942666b3d0f3aa4cb4007 \ + --hash=sha256:660e2d9068d2bedc0912af508f30bbeb505bbbf9774d98def45f68278cea20d3 \ + --hash=sha256:6681ba9e7f8f3b19440921e99efbb40fc89f26cd71bf539e45d8c8a25c976dc6 \ + --hash=sha256:68b977f21ce443d6d378dbd5ca38621755f2063d6fdb3335bda981d552cfff86 \ + --hash=sha256:69269f3a0b472e91125b503d3c0b3566bda26da0a3261c49f0027eb6075086d1 \ + --hash=sha256:6f1a3f10f836fab6ca6efa97bb952300b20ae56b409414ca85bff2ad241d2a61 \ + --hash=sha256:7622a89d696fc87af8e8d280d9b421db5133ef5b29d3f7a1ce9f1a7bf7fcfa11 \ + --hash=sha256:777354ee16f02f643a4c7f2b3eff8027a33c9861edc691a2003531f5da4f6bc8 \ + --hash=sha256:84d27a4832cc1a0ee07cdcf2b0629a8a72db73f4cf6de6f0904f6661227f256f \ + --hash=sha256:8531fdcad636d82c517b26a448dcfe62f720e1922b33c81ce695d0edb91eb931 \ + --hash=sha256:86d2a77fd490ae3ff6fae1c6ceaecad063d3cc2320b44377efdde79880e11526 \ + --hash=sha256:88fc51d9a26b10fc331be344f1781224a375b78488fc343620184e95a4b27016 \ + --hash=sha256:8a34e13a62a59c871064dfd8ffb150867e54291e46d4a7cf11d02c94a5275bae \ + --hash=sha256:8c82f11964f010053e13daafdc7154ce7385ecc538989a354ccc7067fd7028fd \ + --hash=sha256:92b2065d642bf8c0a82d59e59053dd2fdde64d4ed44efe4870fa816c1232647b \ + --hash=sha256:97b52894d948d2f6ea480171a27122d77af14ced35f62e5c892ca2fae9344311 \ + --hash=sha256:9d9acd80072abcc98bd2c86c3c9cd4ac2347b5a5a0cae7ed5c0ee5675f86d9af \ + --hash=sha256:9f59a3c656fef341a99e3d63189852be7084c0e54b75734cde571182c087b152 \ + --hash=sha256:aa5003845cdd21ac0dc6c9bf661c5beddd01116f6eb9eb3c8e272353d45b3288 \ + --hash=sha256:b16fff62b45eccb9c7abb18e60e7e446998093cdcb50fed33134b9b6878836de \ + --hash=sha256:b30c6590146e53149f04e85a6e4fcae068df4289e31e4aee1fdf56a0dead8f97 \ + --hash=sha256:b58cbf0697721120866820b89f93659abc31c1e876bf20d0b3d03cef14faf84d \ + --hash=sha256:b67c6f5e5a401fc56394f191f00f9b3811fe843ee93f4a70df3c389d1adf857d \ + --hash=sha256:bceab846bac555aff6427d060f2fcfff71042dba6f5fca7dc4f75cac815e57ca \ + --hash=sha256:bee9fcb41db2a23bed96c6b6ead6489702c12334ea20a297aa095ce6d31370d0 \ + --hash=sha256:c114e8da9b475739dde229fd3bc6b05a6537a88a578358bc8eb29b4030fac9c9 \ + --hash=sha256:c1f0524f203e3bd35149f12157438f406eff2e4fb30f71221c8a5eceb3617b6b \ + --hash=sha256:c792ea4eabc0159535608fc5658a74d1a81020eb35195dd63214dcf07556f67e \ + --hash=sha256:c7f3cb904cce8e1be667c7e6fef4516b98d1a6a0635a58a57528d577ac18a128 \ + --hash=sha256:d67ac60a307f760c6e65dad586f556dde58e683fab03323221a4e530ead6f74d \ + --hash=sha256:dcacf2c7a6c3a84e720d1bb2b543c675bf6c40e460300b628bab1b1efc7c034c \ + --hash=sha256:de36fe9c02995c7e6ae6efe2e205816f5f00c22fd1fbf343d4d18c3d5ceac2f5 \ + --hash=sha256:def07915168ac8f7853812cc593c71185a16216e9e4fa886358a17ed0fd9fcf6 \ + --hash=sha256:df41b9bc27c2c25b486bae7cf42fccdc52ff181c8c387bfd026624a491c2671b \ + --hash=sha256:e052b8467dd07d4943936009f46ae5ce7b908ddcac3fda581656b1b19c083d9b \ + --hash=sha256:e063b1865974611313a3849d43f2c3f5368093691349cf3c7c8f8f75ad7cb280 \ + --hash=sha256:e1459677e5d12be8bbc7584c35b992eea142911a6236a3278b9b5ce3326f282c \ + --hash=sha256:e1a99a7a71631f0efe727c10edfba09ea6bee4166a6f9c19aafb6c0b5917d09c \ + --hash=sha256:e590228200fcfc7e9109509e4d9125eace2042fd52b595dd22bbc34bb282307f \ + --hash=sha256:e6316827e3e79b7b8e7d8e3b08f4e331af91a48e794d5d8b099928b6f0b85f20 \ + --hash=sha256:e7837cb169eca3b3ae94cc5787c4fed99eef74c0ab9506756eea335e0d6f3ed8 \ + --hash=sha256:e848f46a58b9fcf3d06061d17be388caf70ea5b8cc3466251963c8345e13f7eb \ + --hash=sha256:ed058398f55163a79bb9f06a90ef9ccc063b204bb346c4de78efc5d15abfe602 \ + --hash=sha256:f2e58f2c36cc52d41f2659e4c0cbf7353e28c8c9e63e30d8c6d3494dc9fdedcf \ + --hash=sha256:f467ba0050b7de85016b43f5a22b46383ef004c4f672148a8abf32bc999a87f0 \ + --hash=sha256:f61bdb1df43dc9c131791fbc2355535f9024b9a04398d3bd0684fc16ab07df74 \ + --hash=sha256:fb06eea71a00a7af0ae6aefbb932fb8a7df3cb390cc217d51a9ad7343de1b8d0 \ + --hash=sha256:ffd7dcaf744f25f82190856bc26ed81721508fc5cbf2a330751e135ff1283564 # via -r requirements/requirements.in diff --git a/examples/pip_install/BUILD.bazel b/examples/pip_install/BUILD.bazel index 35f5a9338a..87c5aa7f8c 100644 --- a/examples/pip_install/BUILD.bazel +++ b/examples/pip_install/BUILD.bazel @@ -61,7 +61,6 @@ alias( # Check that our compiled requirements are up-to-date compile_pip_requirements( name = "requirements", - extra_args = ["--allow-unsafe"], requirements_windows = ":requirements_windows.txt", ) @@ -88,7 +87,7 @@ py_test( genquery( name = "yamllint_lib_by_version", expression = """ - attr("tags", "\\bpypi_version=1.26.3\\b", "@pip_yamllint//:pkg") + attr("tags", "\\bpypi_version=1.28.0\\b", "@pip_yamllint//:pkg") intersect attr("tags", "\\bpypi_name=yamllint\\b", "@pip_yamllint//:pkg") """, diff --git a/examples/pip_install/pip_install_test.py b/examples/pip_install/pip_install_test.py index 3e1b085ed4..f49422bb83 100644 --- a/examples/pip_install/pip_install_test.py +++ b/examples/pip_install/pip_install_test.py @@ -43,7 +43,7 @@ def test_entry_point(self): stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) - self.assertEqual(proc.stdout.decode("utf-8").strip(), "yamllint 1.26.3") + self.assertEqual(proc.stdout.decode("utf-8").strip(), "yamllint 1.28.0") def test_data(self): env = os.environ.get("WHEEL_DATA_CONTENTS") diff --git a/examples/pip_install/requirements.in b/examples/pip_install/requirements.in index 11ede3c44a..3480175020 100644 --- a/examples/pip_install/requirements.in +++ b/examples/pip_install/requirements.in @@ -1,4 +1,4 @@ boto3~=1.14.51 s3cmd~=2.1.0 -yamllint~=1.26.3 +yamllint~=1.28.0 tree-sitter==0.20.0 ; sys_platform != "win32" diff --git a/examples/pip_install/requirements.txt b/examples/pip_install/requirements.txt index 495a32a637..00fe860169 100644 --- a/examples/pip_install/requirements.txt +++ b/examples/pip_install/requirements.txt @@ -89,10 +89,6 @@ s3transfer==0.3.7 \ --hash=sha256:35627b86af8ff97e7ac27975fe0a98a312814b46c6333d8a6b889627bcd80994 \ --hash=sha256:efa5bd92a897b6a8d5c1383828dca3d52d0790e0756d49740563a3fb6ed03246 # via boto3 -setuptools==65.6.3 \ - --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ - --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 - # via yamllint six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 @@ -105,6 +101,13 @@ urllib3==1.25.11 \ --hash=sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2 \ --hash=sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e # via botocore -yamllint==1.26.3 \ - --hash=sha256:3934dcde484374596d6b52d8db412929a169f6d9e52e20f9ade5bf3523d9b96e +yamllint==1.28.0 \ + --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ + --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b # via -r requirements.in + +# The following packages are considered to be unsafe in a requirements file: +setuptools==65.6.3 \ + --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ + --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 + # via yamllint diff --git a/examples/pip_install/requirements_windows.txt b/examples/pip_install/requirements_windows.txt index b87192f9d0..c74eeacb06 100644 --- a/examples/pip_install/requirements_windows.txt +++ b/examples/pip_install/requirements_windows.txt @@ -89,18 +89,21 @@ s3transfer==0.3.7 \ --hash=sha256:35627b86af8ff97e7ac27975fe0a98a312814b46c6333d8a6b889627bcd80994 \ --hash=sha256:efa5bd92a897b6a8d5c1383828dca3d52d0790e0756d49740563a3fb6ed03246 # via boto3 -setuptools==65.6.3 \ - --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ - --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 - # via yamllint six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 # via python-dateutil -urllib3==1.25.11 \ - --hash=sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2 \ - --hash=sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e +urllib3==1.26.17 \ + --hash=sha256:24d6a242c28d29af46c3fae832c36db3bbebcc533dd1bb549172cd739c82df21 \ + --hash=sha256:94a757d178c9be92ef5539b8840d48dc9cf1b2709c9d6b588232a055c524458b # via botocore -yamllint==1.26.3 \ - --hash=sha256:3934dcde484374596d6b52d8db412929a169f6d9e52e20f9ade5bf3523d9b96e +yamllint==1.28.0 \ + --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ + --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b # via -r requirements.in + +# The following packages are considered to be unsafe in a requirements file: +setuptools==65.6.3 \ + --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ + --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 + # via yamllint diff --git a/examples/pip_parse/BUILD.bazel b/examples/pip_parse/BUILD.bazel index 653f75ce2b..b7aa5b172b 100644 --- a/examples/pip_parse/BUILD.bazel +++ b/examples/pip_parse/BUILD.bazel @@ -58,7 +58,6 @@ alias( # This rule adds a convenient way to update the requirements file. compile_pip_requirements( name = "requirements", - extra_args = ["--allow-unsafe"], requirements_in = "requirements.in", requirements_txt = "requirements_lock.txt", ) diff --git a/examples/pip_parse/pip_parse_test.py b/examples/pip_parse/pip_parse_test.py index f319cb898f..199879065c 100644 --- a/examples/pip_parse/pip_parse_test.py +++ b/examples/pip_parse/pip_parse_test.py @@ -41,7 +41,7 @@ def test_entry_point(self): stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) - self.assertEqual(proc.stdout.decode("utf-8").strip(), "yamllint 1.26.3") + self.assertEqual(proc.stdout.decode("utf-8").strip(), "yamllint 1.28.0") def test_data(self): env = os.environ.get("WHEEL_DATA_CONTENTS") diff --git a/examples/pip_parse/requirements.in b/examples/pip_parse/requirements.in index ec2102fdda..279dd6068e 100644 --- a/examples/pip_parse/requirements.in +++ b/examples/pip_parse/requirements.in @@ -1,3 +1,3 @@ requests~=2.25.1 s3cmd~=2.1.0 -yamllint~=1.26.3 +yamllint~=1.28.0 diff --git a/examples/pip_parse/requirements_lock.txt b/examples/pip_parse/requirements_lock.txt index 3cbe57f28c..8b68356b29 100644 --- a/examples/pip_parse/requirements_lock.txt +++ b/examples/pip_parse/requirements_lock.txt @@ -78,10 +78,6 @@ s3cmd==2.1.0 \ --hash=sha256:49cd23d516b17974b22b611a95ce4d93fe326feaa07320bd1d234fed68cbccfa \ --hash=sha256:966b0a494a916fc3b4324de38f089c86c70ee90e8e1cae6d59102103a4c0cc03 # via -r requirements.in -setuptools==65.6.3 \ - --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ - --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 - # via yamllint six==1.16.0 \ --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \ --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254 @@ -90,6 +86,13 @@ urllib3==1.26.13 \ --hash=sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc \ --hash=sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8 # via requests -yamllint==1.26.3 \ - --hash=sha256:3934dcde484374596d6b52d8db412929a169f6d9e52e20f9ade5bf3523d9b96e +yamllint==1.28.0 \ + --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ + --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b # via -r requirements.in + +# The following packages are considered to be unsafe in a requirements file: +setuptools==65.6.3 \ + --hash=sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54 \ + --hash=sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75 + # via yamllint diff --git a/examples/pip_parse_vendored/BUILD.bazel b/examples/pip_parse_vendored/BUILD.bazel index 56630e513d..b87b2aa812 100644 --- a/examples/pip_parse_vendored/BUILD.bazel +++ b/examples/pip_parse_vendored/BUILD.bazel @@ -16,7 +16,7 @@ genrule( cmd = " | ".join([ "cat $<", # Insert our load statement after the existing one so we don't produce a file with buildifier warnings - """sed -e '/^load.*/i\\'$$'\\n''load("@python39//:defs.bzl", "interpreter")'""", + """sed -e '/^load.*.whl_library/i\\'$$'\\n''load("@python39//:defs.bzl", "interpreter")'""", # Replace the bazel 6.0.0 specific comment with something that bazel 5.4.0 would produce. # This enables this example to be run as a test under bazel 5.4.0. """sed -e 's#@//#//#'""", diff --git a/examples/pip_parse_vendored/requirements.bzl b/examples/pip_parse_vendored/requirements.bzl index 7bf5170120..4e83555d6c 100644 --- a/examples/pip_parse_vendored/requirements.bzl +++ b/examples/pip_parse_vendored/requirements.bzl @@ -13,7 +13,7 @@ all_whl_requirements = ["@pip_certifi//:whl", "@pip_charset_normalizer//:whl", " all_data_requirements = ["@pip_certifi//:data", "@pip_charset_normalizer//:data", "@pip_idna//:data", "@pip_requests//:data", "@pip_urllib3//:data"] -_packages = [("pip_certifi", "certifi==2022.12.7 --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"), ("pip_charset_normalizer", "charset-normalizer==2.1.1 --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"), ("pip_idna", "idna==3.4 --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"), ("pip_requests", "requests==2.28.1 --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"), ("pip_urllib3", "urllib3==1.26.13 --hash=sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc --hash=sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8")] +_packages = [("pip_certifi", "certifi==2023.7.22 --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"), ("pip_charset_normalizer", "charset-normalizer==2.1.1 --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"), ("pip_idna", "idna==3.4 --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"), ("pip_requests", "requests==2.28.1 --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"), ("pip_urllib3", "urllib3==1.26.13 --hash=sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc --hash=sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8")] _config = {"download_only": False, "enable_implicit_namespace_pkgs": False, "environment": {}, "extra_pip_args": [], "isolated": True, "pip_data_exclude": [], "python_interpreter": "python3", "python_interpreter_target": interpreter, "quiet": True, "repo": "pip", "repo_prefix": "pip_", "timeout": 600} _annotations = {} diff --git a/examples/pip_parse_vendored/requirements.in b/examples/pip_parse_vendored/requirements.in index f2293605cf..7ec4233fa4 100644 --- a/examples/pip_parse_vendored/requirements.in +++ b/examples/pip_parse_vendored/requirements.in @@ -1 +1,2 @@ requests +certifi>=2023.7.22 # https://security.snyk.io/vuln/SNYK-PYTHON-CERTIFI-5805047 diff --git a/examples/pip_parse_vendored/requirements.txt b/examples/pip_parse_vendored/requirements.txt index ff1a3633a2..75b45a1ce3 100644 --- a/examples/pip_parse_vendored/requirements.txt +++ b/examples/pip_parse_vendored/requirements.txt @@ -4,10 +4,12 @@ # # bazel run //:requirements.update # -certifi==2022.12.7 \ - --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \ - --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18 - # via requests +certifi==2023.7.22 \ + --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ + --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 + # via + # -r requirements.in + # requests charset-normalizer==2.1.1 \ --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \ --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f diff --git a/examples/pip_repository_annotations/BUILD.bazel b/examples/pip_repository_annotations/BUILD.bazel index 84089f77d0..77b5ab0698 100644 --- a/examples/pip_repository_annotations/BUILD.bazel +++ b/examples/pip_repository_annotations/BUILD.bazel @@ -10,7 +10,6 @@ exports_files( # This rule adds a convenient way to update the requirements file. compile_pip_requirements( name = "requirements", - extra_args = ["--allow-unsafe"], ) py_test( diff --git a/examples/pip_repository_annotations/requirements.in b/examples/pip_repository_annotations/requirements.in index fd3f75c888..29419a216c 100644 --- a/examples/pip_repository_annotations/requirements.in +++ b/examples/pip_repository_annotations/requirements.in @@ -2,5 +2,6 @@ # `pip_repository` rules. --extra-index-url https://pypi.python.org/simple/ +certifi>=2023.7.22 # https://security.snyk.io/vuln/SNYK-PYTHON-CERTIFI-5805047 wheel requests[security]>=2.8.1 diff --git a/examples/pip_repository_annotations/requirements.txt b/examples/pip_repository_annotations/requirements.txt index 9fde0a922f..9063fa7b1c 100644 --- a/examples/pip_repository_annotations/requirements.txt +++ b/examples/pip_repository_annotations/requirements.txt @@ -6,10 +6,12 @@ # --extra-index-url https://pypi.python.org/simple/ -certifi==2022.12.7 \ - --hash=sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3 \ - --hash=sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18 - # via requests +certifi==2023.7.22 \ + --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ + --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 + # via + # -r requirements.in + # requests charset-normalizer==2.1.1 \ --hash=sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845 \ --hash=sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f @@ -22,9 +24,9 @@ requests[security]==2.28.1 \ --hash=sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983 \ --hash=sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349 # via -r requirements.in -urllib3==1.26.13 \ - --hash=sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc \ - --hash=sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8 +urllib3==1.26.17 \ + --hash=sha256:24d6a242c28d29af46c3fae832c36db3bbebcc533dd1bb549172cd739c82df21 \ + --hash=sha256:94a757d178c9be92ef5539b8840d48dc9cf1b2709c9d6b588232a055c524458b # via requests wheel==0.38.4 \ --hash=sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac \ diff --git a/examples/wheel/BUILD.bazel b/examples/wheel/BUILD.bazel index f56a41b370..81422d37c3 100644 --- a/examples/wheel/BUILD.bazel +++ b/examples/wheel/BUILD.bazel @@ -54,6 +54,8 @@ py_wheel( testonly = True, # Set this to verify the generated .dist target doesn't break things # Package data. We're building "example_minimal_library-0.0.1-py3-none-any.whl" distribution = "example_minimal_library", + incompatible_normalize_name = True, + incompatible_normalize_version = True, python_tag = "py3", version = "0.0.1", deps = [ @@ -76,6 +78,8 @@ py_wheel( testonly = True, abi = "$(ABI)", distribution = "example_minimal_library", + incompatible_normalize_name = True, + incompatible_normalize_version = True, python_tag = "$(PYTHON_TAG)", toolchains = ["//examples/wheel:make_variable_tags"], version = "$(VERSION)", @@ -95,6 +99,8 @@ py_wheel( name = "minimal_with_py_library_with_stamp", # Package data. We're building "example_minimal_library-0.0.1-py3-none-any.whl" distribution = "example_minimal_library{BUILD_USER}", + incompatible_normalize_name = False, + incompatible_normalize_version = False, python_tag = "py3", stamp = 1, version = "0.1.{BUILD_TIMESTAMP}", @@ -123,6 +129,8 @@ py_wheel( name = "minimal_with_py_package", # Package data. We're building "example_minimal_package-0.0.1-py3-none-any.whl" distribution = "example_minimal_package", + incompatible_normalize_name = True, + incompatible_normalize_version = True, python_tag = "py3", version = "0.0.1", deps = [":example_pkg"], @@ -156,6 +164,8 @@ py_wheel( "//examples/wheel:README.md": "README", }, homepage = "www.example.com", + incompatible_normalize_name = True, + incompatible_normalize_version = True, license = "Apache 2.0", project_urls = { "Bug Tracker": "www.example.com/issues", @@ -177,6 +187,8 @@ py_wheel( entry_points = { "console_scripts": ["main = foo.bar:baz"], }, + incompatible_normalize_name = True, + incompatible_normalize_version = True, python_tag = "py3", strip_path_prefixes = [ "examples", @@ -191,6 +203,8 @@ py_wheel( name = "custom_package_root_multi_prefix", # Package data. We're building "custom_custom_package_root_multi_prefix-0.0.1-py3-none-any.whl" distribution = "example_custom_package_root_multi_prefix", + incompatible_normalize_name = True, + incompatible_normalize_version = True, python_tag = "py3", strip_path_prefixes = [ "examples/wheel/lib", @@ -206,6 +220,8 @@ py_wheel( name = "custom_package_root_multi_prefix_reverse_order", # Package data. We're building "custom_custom_package_root_multi_prefix_reverse_order-0.0.1-py3-none-any.whl" distribution = "example_custom_package_root_multi_prefix_reverse_order", + incompatible_normalize_name = True, + incompatible_normalize_version = True, python_tag = "py3", strip_path_prefixes = [ "examples/wheel", @@ -220,6 +236,8 @@ py_wheel( py_wheel( name = "python_requires_in_a_package", distribution = "example_python_requires_in_a_package", + incompatible_normalize_name = True, + incompatible_normalize_version = True, python_requires = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", python_tag = "py3", version = "0.0.1", @@ -231,6 +249,8 @@ py_wheel( py_wheel( name = "use_rule_with_dir_in_outs", distribution = "use_rule_with_dir_in_outs", + incompatible_normalize_name = True, + incompatible_normalize_version = True, python_tag = "py3", version = "0.0.1", deps = [ @@ -244,6 +264,8 @@ py_wheel( name = "python_abi3_binary_wheel", abi = "abi3", distribution = "example_python_abi3_binary_wheel", + incompatible_normalize_name = True, + incompatible_normalize_version = True, # these platform strings must line up with test_python_abi3_binary_wheel() in wheel_test.py platform = select({ ":aarch64-apple-darwin": "macosx_11_0_arm64", @@ -258,16 +280,32 @@ py_wheel( ) py_wheel( - name = "filename_escaping", + name = "legacy_filename_escaping", # Per https://www.python.org/dev/peps/pep-0427/#escaping-and-unicode # runs of non-alphanumeric, non-digit symbols should be replaced with a single underscore. # Unicode non-ascii letters should *not* be replaced with underscore. distribution = "file~~name-escaping", + incompatible_normalize_name = False, + incompatible_normalize_version = False, python_tag = "py3", version = "0.0.1-r7", deps = [":example_pkg"], ) +py_wheel( + name = "filename_escaping", + # Per https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode + # runs of "-", "_" and "." should be replaced with a single underscore. + # Unicode non-ascii letters aren't allowed according to + # https://packaging.python.org/en/latest/specifications/name-normalization/. + distribution = "File--Name-Escaping", + incompatible_normalize_name = True, + incompatible_normalize_version = True, + python_tag = "py3", + version = "v0.0.1.RC1+ubuntu-r7", + deps = [":example_pkg"], +) + py_test( name = "wheel_test", srcs = ["wheel_test.py"], @@ -277,6 +315,7 @@ py_test( ":custom_package_root_multi_prefix_reverse_order", ":customized", ":filename_escaping", + ":legacy_filename_escaping", ":minimal_with_py_library", ":minimal_with_py_library_with_stamp", ":minimal_with_py_package", diff --git a/examples/wheel/wheel_test.py b/examples/wheel/wheel_test.py index f51a0ecedc..671bd8ad84 100644 --- a/examples/wheel/wheel_test.py +++ b/examples/wheel/wheel_test.py @@ -153,13 +153,51 @@ def test_customized_wheel(self): second = second.main:s""", ) + def test_legacy_filename_escaping(self): + filename = os.path.join( + os.environ['TEST_SRCDIR'], + 'rules_python', + 'examples', + 'wheel', + 'file_name_escaping-0.0.1_r7-py3-none-any.whl', + ) + with zipfile.ZipFile(filename) as zf: + self.assertEquals( + zf.namelist(), + [ + 'examples/wheel/lib/data.txt', + 'examples/wheel/lib/module_with_data.py', + 'examples/wheel/lib/simple_module.py', + 'examples/wheel/main.py', + # PEP calls for replacing only in the archive filename. + # Alas setuptools also escapes in the dist-info directory + # name, so let's be compatible. + 'file_name_escaping-0.0.1_r7.dist-info/WHEEL', + 'file_name_escaping-0.0.1_r7.dist-info/METADATA', + 'file_name_escaping-0.0.1_r7.dist-info/RECORD', + ], + ) + metadata_contents = zf.read( + 'file_name_escaping-0.0.1_r7.dist-info/METADATA' + ) + self.assertEquals( + metadata_contents, + b"""\ +Metadata-Version: 2.1 +Name: file~~name-escaping +Version: 0.0.1-r7 + +UNKNOWN +""", + ) + def test_filename_escaping(self): filename = os.path.join( os.environ["TEST_SRCDIR"], "rules_python", "examples", "wheel", - "file_name_escaping-0.0.1_r7-py3-none-any.whl", + "file_name_escaping-0.0.1rc1+ubuntu.r7-py3-none-any.whl", ) with zipfile.ZipFile(filename) as zf: self.assertEqual( @@ -172,20 +210,20 @@ def test_filename_escaping(self): # PEP calls for replacing only in the archive filename. # Alas setuptools also escapes in the dist-info directory # name, so let's be compatible. - "file_name_escaping-0.0.1_r7.dist-info/WHEEL", - "file_name_escaping-0.0.1_r7.dist-info/METADATA", - "file_name_escaping-0.0.1_r7.dist-info/RECORD", + "file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/WHEEL", + "file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/METADATA", + "file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/RECORD", ], ) metadata_contents = zf.read( - "file_name_escaping-0.0.1_r7.dist-info/METADATA" + "file_name_escaping-0.0.1rc1+ubuntu.r7.dist-info/METADATA" ) self.assertEqual( metadata_contents, b"""\ Metadata-Version: 2.1 -Name: file~~name-escaping -Version: 0.0.1-r7 +Name: File--Name-Escaping +Version: 0.0.1rc1+ubuntu.r7 UNKNOWN """, diff --git a/gazelle/MODULE.bazel b/gazelle/MODULE.bazel index ae94a5f863..8c6ad19c7b 100644 --- a/gazelle/MODULE.bazel +++ b/gazelle/MODULE.bazel @@ -5,8 +5,8 @@ module( ) bazel_dep(name = "rules_python", version = "0.18.0") -bazel_dep(name = "rules_go", version = "0.38.1", repo_name = "io_bazel_rules_go") -bazel_dep(name = "gazelle", version = "0.31.0", repo_name = "bazel_gazelle") +bazel_dep(name = "rules_go", version = "0.41.0", repo_name = "io_bazel_rules_go") +bazel_dep(name = "gazelle", version = "0.33.0", repo_name = "bazel_gazelle") go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps") go_deps.from_file(go_mod = "//:go.mod") diff --git a/gazelle/README.md b/gazelle/README.md index ba8520d36b..4728e4c429 100644 --- a/gazelle/README.md +++ b/gazelle/README.md @@ -189,9 +189,9 @@ Python-specific directives are as follows: | `# gazelle:python_validate_import_statements`| `true` | | Controls whether the Python import statements should be validated. Can be "true" or "false" | | | `# gazelle:python_generation_mode`| `package` | -| Controls the target generation mode. Can be "package" or "project" | | +| Controls the target generation mode. Can be "file", "package", or "project" | | | `# gazelle:python_library_naming_convention`| `$package_name$` | -| Controls the `py_library` naming convention. It interpolates $package_name$ with the Bazel package name. E.g. if the Bazel package name is `foo`, setting this to `$package_name$_my_lib` would result in a generated target named `foo_my_lib`. | | +| Controls the `py_library` naming convention. It interpolates \$package_name\$ with the Bazel package name. E.g. if the Bazel package name is `foo`, setting this to `$package_name$_my_lib` would result in a generated target named `foo_my_lib`. | | | `# gazelle:python_binary_naming_convention` | `$package_name$_bin` | | Controls the `py_binary` naming convention. Follows the same interpolation rules as `python_library_naming_convention`. | | | `# gazelle:python_test_naming_convention` | `$package_name$_test` | @@ -206,11 +206,15 @@ Python source files are those ending in `.py` but not ending in `_test.py`. First, we look for the nearest ancestor BUILD file starting from the folder containing the Python source file. -If there is no `py_library` in this BUILD file, one is created, using the -package name as the target's name. This makes it the default target in the -package. +In package generation mode, if there is no `py_library` in this BUILD file, one +is created using the package name as the target's name. This makes it the +default target in the package. Next, all source files are collected into the +`srcs` of the `py_library`. -Next, all source files are collected into the `srcs` of the `py_library`. +In project generation mode, all source files in subdirectories (that don't have +BUILD files) are also collected. + +In file generation mode, each file is given its own target. Finally, the `import` statements in the source files are parsed, and dependencies are added to the `deps` attribute. diff --git a/gazelle/WORKSPACE b/gazelle/WORKSPACE index eef16e924d..fe7ac3ec53 100644 --- a/gazelle/WORKSPACE +++ b/gazelle/WORKSPACE @@ -4,10 +4,10 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") http_archive( name = "io_bazel_rules_go", - sha256 = "099a9fb96a376ccbbb7d291ed4ecbdfd42f6bc822ab77ae6f1b5cb9e914e94fa", + sha256 = "278b7ff5a826f3dc10f04feaf0b70d48b68748ccd512d7f98bf442077f043fe3", urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.35.0/rules_go-v0.35.0.zip", - "https://github.com/bazelbuild/rules_go/releases/download/v0.35.0/rules_go-v0.35.0.zip", + "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.41.0/rules_go-v0.41.0.zip", + "https://github.com/bazelbuild/rules_go/releases/download/v0.41.0/rules_go-v0.41.0.zip", ], ) @@ -34,7 +34,9 @@ local_repository( path = "..", ) -load("@rules_python//python:repositories.bzl", "python_register_toolchains") +load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") + +py_repositories() python_register_toolchains( name = "python39", diff --git a/gazelle/go.mod b/gazelle/go.mod index 1d1cee75f5..6789aa152b 100644 --- a/gazelle/go.mod +++ b/gazelle/go.mod @@ -5,7 +5,7 @@ go 1.19 require ( github.com/bazelbuild/bazel-gazelle v0.31.1 github.com/bazelbuild/buildtools v0.0.0-20230510134650-37bd1811516d - github.com/bazelbuild/rules_go v0.39.1 + github.com/bazelbuild/rules_go v0.41.0 github.com/bmatcuk/doublestar v1.3.4 github.com/emirpasic/gods v1.18.1 github.com/ghodss/yaml v1.0.0 diff --git a/gazelle/go.sum b/gazelle/go.sum index ba2c8bf688..5617f9b822 100644 --- a/gazelle/go.sum +++ b/gazelle/go.sum @@ -4,8 +4,8 @@ github.com/bazelbuild/bazel-gazelle v0.31.1 h1:ROyUyUHzoEdvoOs1e0haxJx1l5EjZX6AO github.com/bazelbuild/bazel-gazelle v0.31.1/go.mod h1:Ul0pqz50f5wxz0QNzsZ+mrEu4AVAVJZEB5xLnHgIG9c= github.com/bazelbuild/buildtools v0.0.0-20230510134650-37bd1811516d h1:Fl1FfItZp34QIQmmDTbZXHB5XA6JfbNNfH7tRRGWvQo= github.com/bazelbuild/buildtools v0.0.0-20230510134650-37bd1811516d/go.mod h1:689QdV3hBP7Vo9dJMmzhoYIyo/9iMhEmHkJcnaPRCbo= -github.com/bazelbuild/rules_go v0.39.1 h1:wkJLUDx59dntWMghuL8++GteoU1To6sRoKJXuyFtmf8= -github.com/bazelbuild/rules_go v0.39.1/go.mod h1:TMHmtfpvyfsxaqfL9WnahCsXMWDMICTw7XeK9yVb+YU= +github.com/bazelbuild/rules_go v0.41.0 h1:JzlRxsFNhlX+g4drDRPhIaU5H5LnI978wdMJ0vK4I+k= +github.com/bazelbuild/rules_go v0.41.0/go.mod h1:TMHmtfpvyfsxaqfL9WnahCsXMWDMICTw7XeK9yVb+YU= github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0= github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/gazelle/manifest/defs.bzl b/gazelle/manifest/defs.bzl index f1266a0f46..d5afe7c143 100644 --- a/gazelle/manifest/defs.bzl +++ b/gazelle/manifest/defs.bzl @@ -25,7 +25,8 @@ def gazelle_python_manifest( pip_repository_name = "", pip_deps_repository_name = "", manifest = ":gazelle_python.yaml", - use_pip_repository_aliases = False): + use_pip_repository_aliases = False, + **kwargs): """A macro for defining the updating and testing targets for the Gazelle manifest file. Args: @@ -39,6 +40,8 @@ def gazelle_python_manifest( pip_deps_repository_name: deprecated - the old pip_install target name. modules_mapping: the target for the generated modules_mapping.json file. manifest: the target for the Gazelle manifest file. + **kwargs: other bazel attributes passed to the target target generated by + this macro. """ if pip_deps_repository_name != "": # buildifier: disable=print @@ -102,6 +105,14 @@ def gazelle_python_manifest( tags = ["manual"], ) + attrs = { + "env": { + "_TEST_MANIFEST": "$(rootpath {})".format(manifest), + "_TEST_MANIFEST_GENERATOR_HASH": "$(rootpath {})".format(manifest_generator_hash), + "_TEST_REQUIREMENTS": "$(rootpath {})".format(requirements), + }, + "size": "small", + } go_test( name = "{}.test".format(name), srcs = [Label("//manifest/test:test.go")], @@ -110,14 +121,10 @@ def gazelle_python_manifest( requirements, manifest_generator_hash, ], - env = { - "_TEST_MANIFEST": "$(rootpath {})".format(manifest), - "_TEST_MANIFEST_GENERATOR_HASH": "$(rootpath {})".format(manifest_generator_hash), - "_TEST_REQUIREMENTS": "$(rootpath {})".format(requirements), - }, rundir = ".", deps = [Label("//manifest")], - size = "small", + # kwargs could contain test-specific attributes like size or timeout + **dict(attrs, **kwargs) ) native.filegroup( diff --git a/gazelle/python/BUILD.bazel b/gazelle/python/BUILD.bazel index fcfe81bd61..4cb755de25 100644 --- a/gazelle/python/BUILD.bazel +++ b/gazelle/python/BUILD.bazel @@ -36,7 +36,7 @@ go_library( "@com_github_emirpasic_gods//lists/singlylinkedlist", "@com_github_emirpasic_gods//sets/treeset", "@com_github_emirpasic_gods//utils", - "@io_bazel_rules_go//go/tools/bazel:go_default_library", + "@io_bazel_rules_go//go/runfiles", ], ) diff --git a/gazelle/python/configure.go b/gazelle/python/configure.go index 32f9ab0a11..2d3880571c 100644 --- a/gazelle/python/configure.go +++ b/gazelle/python/configure.go @@ -137,8 +137,13 @@ func (py *Configurer) Configure(c *config.Config, rel string, f *rule.File) { switch pythonconfig.GenerationModeType(strings.TrimSpace(d.Value)) { case pythonconfig.GenerationModePackage: config.SetCoarseGrainedGeneration(false) + config.SetPerFileGeneration(false) + case pythonconfig.GenerationModeFile: + config.SetCoarseGrainedGeneration(false) + config.SetPerFileGeneration(true) case pythonconfig.GenerationModeProject: config.SetCoarseGrainedGeneration(true) + config.SetPerFileGeneration(false) default: err := fmt.Errorf("invalid value for directive %q: %s", pythonconfig.GenerationMode, d.Value) diff --git a/gazelle/python/generate.go b/gazelle/python/generate.go index fb41324fd6..ede4d2a222 100644 --- a/gazelle/python/generate.go +++ b/gazelle/python/generate.go @@ -153,12 +153,17 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes if entry.IsDir() { // If we are visiting a directory, we determine if we should // halt digging the tree based on a few criterias: - // 1. The directory has a BUILD or BUILD.bazel files. Then + // 1. We are using per-file generation. + // 2. The directory has a BUILD or BUILD.bazel files. Then // it doesn't matter at all what it has since it's a // separate Bazel package. - // 2. (only for fine-grained generation) The directory has - // an __init__.py, __main__.py or __test__.py, meaning - // a BUILD file will be generated. + // 3. (only for package generation) The directory has an + // __init__.py, __main__.py or __test__.py, meaning a + // BUILD file will be generated. + if cfg.PerFileGeneration() { + return fs.SkipDir + } + if isBazelPackage(path) { boundaryPackages[path] = struct{}{} return nil @@ -213,15 +218,12 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes collisionErrors := singlylinkedlist.New() - var pyLibrary *rule.Rule - if !pyLibraryFilenames.Empty() { - deps, err := parser.parse(pyLibraryFilenames) + appendPyLibrary := func(srcs *treeset.Set, pyLibraryTargetName string) { + deps, err := parser.parse(srcs) if err != nil { log.Fatalf("ERROR: %v\n", err) } - pyLibraryTargetName := cfg.RenderLibraryName(packageName) - // Check if a target with the same name we are generating already // exists, and if it is of a different kind from the one we are // generating. If so, we have to throw an error since Gazelle won't @@ -239,9 +241,9 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes } } - pyLibrary = newTargetBuilder(pyLibraryKind, pyLibraryTargetName, pythonProjectRoot, args.Rel, pyFileNames). + pyLibrary := newTargetBuilder(pyLibraryKind, pyLibraryTargetName, pythonProjectRoot, args.Rel, pyFileNames). addVisibility(visibility). - addSrcs(pyLibraryFilenames). + addSrcs(srcs). addModuleDependencies(deps). generateImportsAttribute(). build() @@ -249,6 +251,24 @@ func (py *Python) GenerateRules(args language.GenerateArgs) language.GenerateRes result.Gen = append(result.Gen, pyLibrary) result.Imports = append(result.Imports, pyLibrary.PrivateAttr(config.GazelleImportsKey)) } + if cfg.PerFileGeneration() { + pyLibraryFilenames.Each(func(index int, filename interface{}) { + if filename == pyLibraryEntrypointFilename { + stat, err := os.Stat(filepath.Join(args.Dir, filename.(string))) + if err != nil { + log.Fatalf("ERROR: %v\n", err) + } + if stat.Size() == 0 { + return // ignore empty __init__.py + } + } + srcs := treeset.NewWith(godsutils.StringComparator, filename) + pyLibraryTargetName := strings.TrimSuffix(filepath.Base(filename.(string)), ".py") + appendPyLibrary(srcs, pyLibraryTargetName) + }) + } else if !pyLibraryFilenames.Empty() { + appendPyLibrary(pyLibraryFilenames, cfg.RenderLibraryName(packageName)) + } if hasPyBinary { deps, err := parser.parseSingle(pyBinaryEntrypointFilename) diff --git a/gazelle/python/kinds.go b/gazelle/python/kinds.go index ab1afb7d55..941b45b5c6 100644 --- a/gazelle/python/kinds.go +++ b/gazelle/python/kinds.go @@ -49,7 +49,8 @@ var pyKinds = map[string]rule.KindInfo{ }, }, pyLibraryKind: { - MatchAny: true, + MatchAny: false, + MatchAttrs: []string{"srcs"}, NonEmptyAttrs: map[string]bool{ "deps": true, "srcs": true, diff --git a/gazelle/python/parser.go b/gazelle/python/parser.go index 7f10a754bf..60a3c24269 100644 --- a/gazelle/python/parser.go +++ b/gazelle/python/parser.go @@ -26,7 +26,7 @@ import ( "strings" "sync" - "github.com/bazelbuild/rules_go/go/tools/bazel" + "github.com/bazelbuild/rules_go/go/runfiles" "github.com/emirpasic/gods/sets/treeset" godsutils "github.com/emirpasic/gods/utils" ) @@ -38,13 +38,20 @@ var ( ) func startParserProcess(ctx context.Context) { - parseScriptRunfile, err := bazel.Runfile("python/parse") + rfiles, err := runfiles.New() + if err != nil { + log.Printf("failed to create a runfiles object: %v\n", err) + os.Exit(1) + } + + parseScriptRunfile, err := rfiles.Rlocation("rules_python_gazelle_plugin/python/parse") if err != nil { log.Printf("failed to initialize parser: %v\n", err) os.Exit(1) } cmd := exec.CommandContext(ctx, parseScriptRunfile) + cmd.Env = append(os.Environ(), rfiles.Env()...) cmd.Stderr = os.Stderr diff --git a/gazelle/python/resolve.go b/gazelle/python/resolve.go index 46014e50ec..87eed76ec3 100644 --- a/gazelle/python/resolve.go +++ b/gazelle/python/resolve.go @@ -151,10 +151,10 @@ func (py *Resolver) Resolve( for len(moduleParts) > 1 { // Iterate back through the possible imports until // a match is found. - // For example, "from foo.bar import baz" where bar is a variable, we should try - // `foo.bar.baz` first, then `foo.bar`, then `foo`. In the first case, the import could be file `baz.py` - // in the directory `foo/bar`. - // Or, the import could be variable `bar` in file `foo/bar.py`. + // For example, "from foo.bar import baz" where baz is a module, we should try `foo.bar.baz` first, then + // `foo.bar`, then `foo`. + // In the first case, the import could be file `baz.py` in the directory `foo/bar`. + // Or, the import could be variable `baz` in file `foo/bar.py`. // The import could also be from a standard module, e.g. `six.moves`, where // the dependency is actually `six`. moduleParts = moduleParts[:len(moduleParts)-1] diff --git a/gazelle/python/std_modules.go b/gazelle/python/std_modules.go index c537184c74..a87deec366 100644 --- a/gazelle/python/std_modules.go +++ b/gazelle/python/std_modules.go @@ -26,7 +26,7 @@ import ( "strings" "sync" - "github.com/bazelbuild/rules_go/go/tools/bazel" + "github.com/bazelbuild/rules_go/go/runfiles" ) var ( @@ -39,7 +39,13 @@ var ( func startStdModuleProcess(ctx context.Context) { stdModulesSeen = make(map[string]struct{}) - stdModulesScriptRunfile, err := bazel.Runfile("python/std_modules") + rfiles, err := runfiles.New() + if err != nil { + log.Printf("failed to create a runfiles object: %v\n", err) + os.Exit(1) + } + + stdModulesScriptRunfile, err := rfiles.Rlocation("rules_python_gazelle_plugin/python/std_modules") if err != nil { log.Printf("failed to initialize std_modules: %v\n", err) os.Exit(1) @@ -49,7 +55,8 @@ func startStdModuleProcess(ctx context.Context) { cmd.Stderr = os.Stderr // All userland site-packages should be ignored. - cmd.Env = []string{"PYTHONNOUSERSITE=1"} + cmd.Env = append([]string{"PYTHONNOUSERSITE=1"}, rfiles.Env()...) + stdin, err := cmd.StdinPipe() if err != nil { log.Printf("failed to initialize std_modules: %v\n", err) diff --git a/gazelle/python/testdata/dont_rename_target/BUILD.in b/gazelle/python/testdata/dont_rename_target/BUILD.in index 33e8ec25cb..e9bc0e6e29 100644 --- a/gazelle/python/testdata/dont_rename_target/BUILD.in +++ b/gazelle/python/testdata/dont_rename_target/BUILD.in @@ -2,4 +2,5 @@ load("@rules_python//python:defs.bzl", "py_library") py_library( name = "my_custom_target", + srcs = ["__init__.py"], ) diff --git a/gazelle/python/testdata/per_file/BUILD.in b/gazelle/python/testdata/per_file/BUILD.in new file mode 100644 index 0000000000..01b0904d50 --- /dev/null +++ b/gazelle/python/testdata/per_file/BUILD.in @@ -0,0 +1,11 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file + +# This target should be kept unmodified by Gazelle. +py_library( + name = "custom", + srcs = ["bar.py"], + visibility = ["//visibility:private"], + tags = ["cant_touch_this"], +) diff --git a/gazelle/python/testdata/per_file/BUILD.out b/gazelle/python/testdata/per_file/BUILD.out new file mode 100644 index 0000000000..2ec825b207 --- /dev/null +++ b/gazelle/python/testdata/per_file/BUILD.out @@ -0,0 +1,24 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file + +# This target should be kept unmodified by Gazelle. +py_library( + name = "custom", + srcs = ["bar.py"], + tags = ["cant_touch_this"], + visibility = ["//visibility:private"], +) + +py_library( + name = "baz", + srcs = ["baz.py"], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "foo", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], + deps = [":custom"], +) diff --git a/gazelle/python/testdata/per_file/README.md b/gazelle/python/testdata/per_file/README.md new file mode 100644 index 0000000000..3ddeb213fc --- /dev/null +++ b/gazelle/python/testdata/per_file/README.md @@ -0,0 +1,5 @@ +# Per-file generation + +This test case generates one `py_library` per file. + +`__init__.py` is left empty so no target is generated for it. diff --git a/gazelle/python/testdata/per_file/WORKSPACE b/gazelle/python/testdata/per_file/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/per_file/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/per_file/__init__.py b/gazelle/python/testdata/per_file/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/per_file/bar.py b/gazelle/python/testdata/per_file/bar.py new file mode 100644 index 0000000000..730755995d --- /dev/null +++ b/gazelle/python/testdata/per_file/bar.py @@ -0,0 +1,15 @@ +# 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. + +# For test purposes only. diff --git a/gazelle/python/testdata/per_file/baz.py b/gazelle/python/testdata/per_file/baz.py new file mode 100644 index 0000000000..730755995d --- /dev/null +++ b/gazelle/python/testdata/per_file/baz.py @@ -0,0 +1,15 @@ +# 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. + +# For test purposes only. diff --git a/gazelle/python/testdata/per_file/foo.py b/gazelle/python/testdata/per_file/foo.py new file mode 100644 index 0000000000..c000990002 --- /dev/null +++ b/gazelle/python/testdata/per_file/foo.py @@ -0,0 +1,15 @@ +# 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. + +import bar diff --git a/gazelle/python/testdata/per_file/test.yaml b/gazelle/python/testdata/per_file/test.yaml new file mode 100644 index 0000000000..fcea77710f --- /dev/null +++ b/gazelle/python/testdata/per_file/test.yaml @@ -0,0 +1,15 @@ +# 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. + +--- diff --git a/gazelle/python/testdata/per_file_non_empty_init/BUILD.in b/gazelle/python/testdata/per_file_non_empty_init/BUILD.in new file mode 100644 index 0000000000..a5853f6c5c --- /dev/null +++ b/gazelle/python/testdata/per_file_non_empty_init/BUILD.in @@ -0,0 +1,3 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file diff --git a/gazelle/python/testdata/per_file_non_empty_init/BUILD.out b/gazelle/python/testdata/per_file_non_empty_init/BUILD.out new file mode 100644 index 0000000000..8733dbd971 --- /dev/null +++ b/gazelle/python/testdata/per_file_non_empty_init/BUILD.out @@ -0,0 +1,16 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file + +py_library( + name = "__init__", + srcs = ["__init__.py"], + visibility = ["//:__subpackages__"], + deps = [":foo"], +) + +py_library( + name = "foo", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/per_file_non_empty_init/README.md b/gazelle/python/testdata/per_file_non_empty_init/README.md new file mode 100644 index 0000000000..6e6e9e245d --- /dev/null +++ b/gazelle/python/testdata/per_file_non_empty_init/README.md @@ -0,0 +1,3 @@ +# Per-file generation + +This test case generates one `py_library` per file, including `__init__.py`. diff --git a/gazelle/python/testdata/per_file_non_empty_init/WORKSPACE b/gazelle/python/testdata/per_file_non_empty_init/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/per_file_non_empty_init/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/per_file_non_empty_init/__init__.py b/gazelle/python/testdata/per_file_non_empty_init/__init__.py new file mode 100644 index 0000000000..492cbc0260 --- /dev/null +++ b/gazelle/python/testdata/per_file_non_empty_init/__init__.py @@ -0,0 +1,15 @@ +# 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. + +import foo diff --git a/gazelle/python/testdata/per_file_non_empty_init/foo.py b/gazelle/python/testdata/per_file_non_empty_init/foo.py new file mode 100644 index 0000000000..730755995d --- /dev/null +++ b/gazelle/python/testdata/per_file_non_empty_init/foo.py @@ -0,0 +1,15 @@ +# 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. + +# For test purposes only. diff --git a/gazelle/python/testdata/per_file_non_empty_init/test.yaml b/gazelle/python/testdata/per_file_non_empty_init/test.yaml new file mode 100644 index 0000000000..fcea77710f --- /dev/null +++ b/gazelle/python/testdata/per_file_non_empty_init/test.yaml @@ -0,0 +1,15 @@ +# 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. + +--- diff --git a/gazelle/python/testdata/per_file_subdirs/BUILD.in b/gazelle/python/testdata/per_file_subdirs/BUILD.in new file mode 100644 index 0000000000..a5853f6c5c --- /dev/null +++ b/gazelle/python/testdata/per_file_subdirs/BUILD.in @@ -0,0 +1,3 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file diff --git a/gazelle/python/testdata/per_file_subdirs/BUILD.out b/gazelle/python/testdata/per_file_subdirs/BUILD.out new file mode 100644 index 0000000000..69c42e01a9 --- /dev/null +++ b/gazelle/python/testdata/per_file_subdirs/BUILD.out @@ -0,0 +1,10 @@ +load("@rules_python//python:defs.bzl", "py_library") + +# gazelle:python_generation_mode file + +py_library( + name = "foo", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], + deps = ["//bar:__init__"], +) diff --git a/gazelle/python/testdata/per_file_subdirs/README.md b/gazelle/python/testdata/per_file_subdirs/README.md new file mode 100644 index 0000000000..9eda2fac28 --- /dev/null +++ b/gazelle/python/testdata/per_file_subdirs/README.md @@ -0,0 +1,3 @@ +# Per-file generation + +This test case generates one `py_library` per file in subdirectories. diff --git a/gazelle/python/testdata/per_file_subdirs/WORKSPACE b/gazelle/python/testdata/per_file_subdirs/WORKSPACE new file mode 100644 index 0000000000..faff6af87a --- /dev/null +++ b/gazelle/python/testdata/per_file_subdirs/WORKSPACE @@ -0,0 +1 @@ +# This is a Bazel workspace for the Gazelle test data. diff --git a/gazelle/python/testdata/per_file_subdirs/bar/BUILD.in b/gazelle/python/testdata/per_file_subdirs/bar/BUILD.in new file mode 100644 index 0000000000..e69de29bb2 diff --git a/gazelle/python/testdata/per_file_subdirs/bar/BUILD.out b/gazelle/python/testdata/per_file_subdirs/bar/BUILD.out new file mode 100644 index 0000000000..7258d27524 --- /dev/null +++ b/gazelle/python/testdata/per_file_subdirs/bar/BUILD.out @@ -0,0 +1,13 @@ +load("@rules_python//python:defs.bzl", "py_library") + +py_library( + name = "__init__", + srcs = ["__init__.py"], + visibility = ["//:__subpackages__"], +) + +py_library( + name = "foo", + srcs = ["foo.py"], + visibility = ["//:__subpackages__"], +) diff --git a/gazelle/python/testdata/per_file_subdirs/bar/__init__.py b/gazelle/python/testdata/per_file_subdirs/bar/__init__.py new file mode 100644 index 0000000000..579915261d --- /dev/null +++ b/gazelle/python/testdata/per_file_subdirs/bar/__init__.py @@ -0,0 +1,15 @@ +# 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. + +from .foo import func diff --git a/gazelle/python/testdata/per_file_subdirs/bar/foo.py b/gazelle/python/testdata/per_file_subdirs/bar/foo.py new file mode 100644 index 0000000000..59eb08c42f --- /dev/null +++ b/gazelle/python/testdata/per_file_subdirs/bar/foo.py @@ -0,0 +1,16 @@ +# 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. + +def func(): + pass diff --git a/gazelle/python/testdata/per_file_subdirs/baz/baz.py b/gazelle/python/testdata/per_file_subdirs/baz/baz.py new file mode 100644 index 0000000000..5256394021 --- /dev/null +++ b/gazelle/python/testdata/per_file_subdirs/baz/baz.py @@ -0,0 +1,15 @@ +# 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. + +from bar.foo import func diff --git a/gazelle/python/testdata/per_file_subdirs/foo.py b/gazelle/python/testdata/per_file_subdirs/foo.py new file mode 100644 index 0000000000..b5e6cff5c6 --- /dev/null +++ b/gazelle/python/testdata/per_file_subdirs/foo.py @@ -0,0 +1,15 @@ +# 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. + +from bar import func diff --git a/gazelle/python/testdata/per_file_subdirs/test.yaml b/gazelle/python/testdata/per_file_subdirs/test.yaml new file mode 100644 index 0000000000..fcea77710f --- /dev/null +++ b/gazelle/python/testdata/per_file_subdirs/test.yaml @@ -0,0 +1,15 @@ +# 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. + +--- diff --git a/gazelle/pythonconfig/pythonconfig.go b/gazelle/pythonconfig/pythonconfig.go index c7cd7c1a28..a266804fab 100644 --- a/gazelle/pythonconfig/pythonconfig.go +++ b/gazelle/pythonconfig/pythonconfig.go @@ -78,6 +78,7 @@ const ( // GenerationModeProject defines the mode in which a coarse-grained target will // be generated englobing sub-directories containing Python files. GenerationModeProject GenerationModeType = "project" + GenerationModeFile GenerationModeType = "file" ) const ( @@ -126,6 +127,7 @@ type Config struct { ignoreDependencies map[string]struct{} validateImportStatements bool coarseGrainedGeneration bool + perFileGeneration bool libraryNamingConvention string binaryNamingConvention string testNamingConvention string @@ -145,6 +147,7 @@ func New( ignoreDependencies: make(map[string]struct{}), validateImportStatements: true, coarseGrainedGeneration: false, + perFileGeneration: false, libraryNamingConvention: packageNameNamingConventionSubstitution, binaryNamingConvention: fmt.Sprintf("%s_bin", packageNameNamingConventionSubstitution), testNamingConvention: fmt.Sprintf("%s_test", packageNameNamingConventionSubstitution), @@ -169,6 +172,7 @@ func (c *Config) NewChild() *Config { ignoreDependencies: make(map[string]struct{}), validateImportStatements: c.validateImportStatements, coarseGrainedGeneration: c.coarseGrainedGeneration, + perFileGeneration: c.perFileGeneration, libraryNamingConvention: c.libraryNamingConvention, binaryNamingConvention: c.binaryNamingConvention, testNamingConvention: c.testNamingConvention, @@ -327,6 +331,18 @@ func (c *Config) CoarseGrainedGeneration() bool { return c.coarseGrainedGeneration } +// SetPerFileGneration sets whether a separate py_library target should be +// generated for each file. +func (c *Config) SetPerFileGeneration(perFile bool) { + c.perFileGeneration = perFile +} + +// PerFileGeneration returns whether a separate py_library target should be +// generated for each file. +func (c *Config) PerFileGeneration() bool { + return c.perFileGeneration +} + // SetLibraryNamingConvention sets the py_library target naming convention. func (c *Config) SetLibraryNamingConvention(libraryNamingConvention string) { c.libraryNamingConvention = libraryNamingConvention diff --git a/internal_deps.bzl b/internal_deps.bzl index f50d2bfae1..fd2d91edc1 100644 --- a/internal_deps.bzl +++ b/internal_deps.bzl @@ -74,10 +74,10 @@ def rules_python_internal_deps(): maybe( http_archive, name = "io_bazel_rules_go", - sha256 = "6dc2da7ab4cf5d7bfc7c949776b1b7c733f05e56edc4bcd9022bb249d2e2a996", + sha256 = "278b7ff5a826f3dc10f04feaf0b70d48b68748ccd512d7f98bf442077f043fe3", urls = [ - "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.39.1/rules_go-v0.39.1.zip", - "https://github.com/bazelbuild/rules_go/releases/download/v0.39.1/rules_go-v0.39.1.zip", + "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.41.0/rules_go-v0.41.0.zip", + "https://github.com/bazelbuild/rules_go/releases/download/v0.41.0/rules_go-v0.41.0.zip", ], ) diff --git a/internal_setup.bzl b/internal_setup.bzl index c3a7ad452d..0c9d6c48a6 100644 --- a/internal_setup.bzl +++ b/internal_setup.bzl @@ -20,10 +20,13 @@ load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") load("@rules_proto//proto:repositories.bzl", "rules_proto_dependencies", "rules_proto_toolchains") load("//:version.bzl", "SUPPORTED_BAZEL_VERSIONS") load("//python/pip_install:repositories.bzl", "pip_install_dependencies") +load("//python/private:internal_config_repo.bzl", "internal_config_repo") # buildifier: disable=bzl-visibility def rules_python_internal_setup(): """Setup for rules_python tests and tools.""" + internal_config_repo(name = "rules_python_internal") + # Because we don't use the pip_install rule, we have to call this to fetch its deps pip_install_dependencies() diff --git a/python/BUILD.bazel b/python/BUILD.bazel index c5f25803c7..5ff752e13f 100644 --- a/python/BUILD.bazel +++ b/python/BUILD.bazel @@ -36,6 +36,7 @@ filegroup( "//python/cc:distribution", "//python/config_settings:distribution", "//python/constraints:distribution", + "//python/entry_points:distribution", "//python/private:distribution", "//python/runfiles:distribution", ], @@ -69,6 +70,32 @@ bzl_library( ], ) +bzl_library( + name = "packaging_bzl", + srcs = ["packaging.bzl"], + deps = [ + ":py_binary_bzl", + "//python/private:py_package.bzl", + "//python/private:py_wheel_bzl", + "//python/private:py_wheel_normalize_pep440.bzl", + "//python/private:stamp_bzl", + "//python/private:util_bzl", + ], +) + +bzl_library( + name = "pip_bzl", + srcs = ["pip.bzl"], + deps = [ + "//python/pip_install:pip_repository_bzl", + "//python/pip_install:repositories_bzl", + "//python/pip_install:requirements_bzl", + "//python/private:bzlmod_enabled_bzl", + "//python/private:full_version_bzl", + "//python/private:render_pkg_aliases_bzl", + ], +) + bzl_library( name = "proto_bzl", srcs = [ @@ -83,7 +110,11 @@ bzl_library( bzl_library( name = "py_binary_bzl", srcs = ["py_binary.bzl"], - deps = ["//python/private:util_bzl"], + deps = [ + "//python/private:util_bzl", + "//python/private/common:py_binary_macro_bazel_bzl", + "@rules_python_internal//:rules_python_config_bzl", + ], ) bzl_library( @@ -100,37 +131,82 @@ bzl_library( bzl_library( name = "py_info_bzl", srcs = ["py_info.bzl"], - deps = ["//python/private:reexports_bzl"], + deps = [ + "//python/private:reexports_bzl", + "//python/private/common:providers_bzl", + "@rules_python_internal//:rules_python_config_bzl", + ], ) bzl_library( name = "py_library_bzl", srcs = ["py_library.bzl"], - deps = ["//python/private:util_bzl"], + deps = [ + "//python/private:util_bzl", + "//python/private/common:py_library_macro_bazel_bzl", + "@rules_python_internal//:rules_python_config_bzl", + ], ) bzl_library( name = "py_runtime_bzl", srcs = ["py_runtime.bzl"], - deps = ["//python/private:util_bzl"], + deps = [ + "//python/private:util_bzl", + "//python/private/common:py_runtime_macro_bzl", + "@rules_python_internal//:rules_python_config_bzl", + ], ) bzl_library( name = "py_runtime_pair_bzl", srcs = ["py_runtime_pair.bzl"], - deps = ["//python/private:bazel_tools_bzl"], + deps = [ + "//python/private:bazel_tools_bzl", + "//python/private:py_runtime_pair_macro_bzl", + "@rules_python_internal//:rules_python_config_bzl", + ], ) bzl_library( name = "py_runtime_info_bzl", srcs = ["py_runtime_info.bzl"], - deps = ["//python/private:reexports_bzl"], + deps = [ + "//python/private:reexports_bzl", + "//python/private/common:providers_bzl", + "@rules_python_internal//:rules_python_config_bzl", + ], ) bzl_library( name = "py_test_bzl", srcs = ["py_test.bzl"], - deps = ["//python/private:util_bzl"], + deps = [ + "//python/private:util_bzl", + "//python/private/common:py_test_macro_bazel_bzl", + "@rules_python_internal//:rules_python_config_bzl", + ], +) + +bzl_library( + name = "repositories_bzl", + srcs = ["repositories.bzl"], + deps = [ + ":versions_bzl", + "//python/private:bazel_tools_bzl", + "//python/private:bzlmod_enabled_bzl", + "//python/private:coverage_deps_bzl", + "//python/private:full_version_bzl", + "//python/private:internal_config_repo_bzl", + "//python/private:toolchains_repo_bzl", + "//python/private:which_bzl", + ], +) + +bzl_library( + name = "versions_bzl", + srcs = ["versions.bzl"], + visibility = ["//:__subpackages__"], ) # NOTE: Remember to add bzl_library targets to //tests:bzl_libraries diff --git a/python/config_settings/BUILD.bazel b/python/config_settings/BUILD.bazel index 272ba78f1f..ab4ee8d880 100644 --- a/python/config_settings/BUILD.bazel +++ b/python/config_settings/BUILD.bazel @@ -5,6 +5,7 @@ filegroup( name = "distribution", srcs = glob(["*.bzl"]) + [ "BUILD.bazel", + "//python/config_settings/private:distribution", ], visibility = ["//python:__pkg__"], ) diff --git a/python/config_settings/private/BUILD.bazel b/python/config_settings/private/BUILD.bazel index e69de29bb2..aa68c6508c 100644 --- a/python/config_settings/private/BUILD.bazel +++ b/python/config_settings/private/BUILD.bazel @@ -0,0 +1,7 @@ +filegroup( + name = "distribution", + srcs = glob(["*.bzl"]) + [ + "BUILD.bazel", + ], + visibility = ["//python/config_settings:__pkg__"], +) diff --git a/python/config_settings/transition.bzl b/python/config_settings/transition.bzl index 20e03dc21d..cb25965f76 100644 --- a/python/config_settings/transition.bzl +++ b/python/config_settings/transition.bzl @@ -17,7 +17,8 @@ them to the desired target platform. """ load("@bazel_skylib//lib:dicts.bzl", "dicts") -load("//python:defs.bzl", _py_binary = "py_binary", _py_test = "py_test") +load("//python:py_binary.bzl", _py_binary = "py_binary") +load("//python:py_test.bzl", _py_test = "py_test") load("//python/config_settings/private:py_args.bzl", "py_args") def _transition_python_version_impl(_, attr): @@ -151,7 +152,6 @@ def _py_rule(rule_impl, transition_rule, name, python_version, **kwargs): # https://bazel.build/reference/be/common-definitions#common-attributes compatible_with = kwargs.pop("compatible_with", None) deprecation = kwargs.pop("deprecation", None) - distribs = kwargs.pop("distribs", None) exec_compatible_with = kwargs.pop("exec_compatible_with", None) exec_properties = kwargs.pop("exec_properties", None) features = kwargs.pop("features", None) @@ -165,7 +165,6 @@ def _py_rule(rule_impl, transition_rule, name, python_version, **kwargs): common_attrs = { "compatible_with": compatible_with, "deprecation": deprecation, - "distribs": distribs, "exec_compatible_with": exec_compatible_with, "exec_properties": exec_properties, "features": features, diff --git a/python/entry_points/BUILD.bazel b/python/entry_points/BUILD.bazel new file mode 100644 index 0000000000..981a1ccf69 --- /dev/null +++ b/python/entry_points/BUILD.bazel @@ -0,0 +1,39 @@ +# 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. + +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +exports_files( + [ + "py_console_script_binary.bzl", + ], + visibility = ["//docs:__subpackages__"], +) + +bzl_library( + name = "py_console_script_binary_bzl", + srcs = [":py_console_script_binary.bzl"], + visibility = ["//visibility:public"], + deps = [ + "//python/private:py_console_script_binary_bzl", + ], +) + +filegroup( + name = "distribution", + srcs = glob([ + "*.bzl", + ]), + visibility = ["//python:__subpackages__"], +) diff --git a/python/entry_points/py_console_script_binary.bzl b/python/entry_points/py_console_script_binary.bzl new file mode 100644 index 0000000000..60e74f579d --- /dev/null +++ b/python/entry_points/py_console_script_binary.bzl @@ -0,0 +1,79 @@ +# 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. + +""" +Creates an executable (a non-test binary) for console_script entry points. + +Generate a `py_binary` target for a particular console_script `entry_point` +from a PyPI package, e.g. for creating an executable `pylint` target use: +```starlark +load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") + +py_console_script_binary( + name = "pylint", + pkg = "@pip//pylint", +) +``` + +Or for more advanced setups you can also specify extra dependencies and the +exact script name you want to call. It is useful for tools like flake8, pylint, +pytest, which have plugin discovery methods and discover dependencies from the +PyPI packages available in the PYTHONPATH. +```starlark +load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") + +py_console_script_binary( + name = "pylint_with_deps", + pkg = "@pip//pylint", + # Because `pylint` has multiple console_scripts available, we have to + # specify which we want if the name of the target name 'pylint_with_deps' + # cannot be used to guess the entry_point script. + script = "pylint", + deps = [ + # One can add extra dependencies to the entry point. + # This specifically allows us to add plugins to pylint. + "@pip//pylint_print", + ], +) +``` + +A specific Python version can be forced by using the generated version-aware +wrappers, e.g. to force Python 3.9: +```starlark +load("@python_versions//3.9:defs.bzl", "py_console_script_binary") + +py_console_script_binary( + name = "yamllint", + pkg = "@pip//yamllint", +) +``` + +Alternatively, the the `py_console_script_binary.binary_rule` arg can be passed +the version-bound `py_binary` symbol, or any other `py_binary`-compatible rule +of your choosing: +```starlark +load("@python_versions//3.9:defs.bzl", "py_binary") +load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") + +py_console_script_binary( + name = "yamllint", + pkg = "@pip//yamllint:pkg", + binary_rule = py_binary, +) +``` +""" + +load("//python/private:py_console_script_binary.bzl", _py_console_script_binary = "py_console_script_binary") + +py_console_script_binary = _py_console_script_binary diff --git a/python/extensions/pip.bzl b/python/extensions/pip.bzl index 3ba0d3eb58..a0559ffe97 100644 --- a/python/extensions/pip.bzl +++ b/python/extensions/pip.bzl @@ -14,18 +14,18 @@ "pip module extension for use with bzlmod" +load("@bazel_features//:features.bzl", "bazel_features") load("@pythons_hub//:interpreters.bzl", "DEFAULT_PYTHON_VERSION", "INTERPRETER_LABELS") -load("//python:pip.bzl", "whl_library_alias") load( "//python/pip_install:pip_repository.bzl", "locked_requirements_label", "pip_hub_repository_bzlmod", "pip_repository_attrs", - "pip_repository_bzlmod", "use_isolated", "whl_library", ) load("//python/pip_install:requirements_parser.bzl", parse_requirements = "parse") +load("//python/private:full_version.bzl", "full_version") load("//python/private:normalize_name.bzl", "normalize_name") load("//python/private:version_label.bzl", "version_label") @@ -78,11 +78,11 @@ You cannot use both the additive_build_content and additive_build_content_file a whl_mods = whl_mods, ) -def _create_versioned_pip_and_whl_repos(module_ctx, pip_attr, whl_map): +def _create_whl_repos(module_ctx, pip_attr, whl_map): python_interpreter_target = pip_attr.python_interpreter_target # if we do not have the python_interpreter set in the attributes - # we programtically find it. + # we programmatically find it. hub_name = pip_attr.hub_name if python_interpreter_target == None: python_name = "python_" + version_label(pip_attr.python_version, sep = "_") @@ -104,23 +104,12 @@ def _create_versioned_pip_and_whl_repos(module_ctx, pip_attr, whl_map): requrements_lock = locked_requirements_label(module_ctx, pip_attr) # Parse the requirements file directly in starlark to get the information - # needed for the whl_libary declarations below. This is needed to contain - # the pip_repository logic to a single module extension. + # needed for the whl_libary declarations below. requirements_lock_content = module_ctx.read(requrements_lock) parse_result = parse_requirements(requirements_lock_content) requirements = parse_result.requirements extra_pip_args = pip_attr.extra_pip_args + parse_result.options - # Create the repository where users load the `requirement` macro. Under bzlmod - # this does not create the install_deps() macro. - # TODO: we may not need this repository once we have entry points - # supported. For now a user can access this repository and use - # the entrypoint functionality. - pip_repository_bzlmod( - name = pip_name, - repo_name = pip_name, - requirements_lock = pip_attr.requirements_lock, - ) if hub_name not in whl_map: whl_map[hub_name] = {} @@ -157,12 +146,12 @@ def _create_versioned_pip_and_whl_repos(module_ctx, pip_attr, whl_map): if whl_name not in whl_map[hub_name]: whl_map[hub_name][whl_name] = {} - whl_map[hub_name][whl_name][pip_attr.python_version] = pip_name + "_" + whl_map[hub_name][whl_name][full_version(pip_attr.python_version)] = pip_name + "_" def _pip_impl(module_ctx): - """Implementation of a class tag that creates the pip hub(s) and corresponding pip spoke, alias and whl repositories. + """Implementation of a class tag that creates the pip hub and corresponding pip spoke whl repositories. - This implmentation iterates through all of the `pip.parse` calls and creates + This implementation iterates through all of the `pip.parse` calls and creates different pip hub repositories based on the "hub_name". Each of the pip calls create spoke repos that uses a specific Python interpreter. @@ -196,52 +185,33 @@ def _pip_impl(module_ctx): Both of these pip spokes contain requirements files that includes websocket and its dependencies. - Two different repositories are created for the two spokes: - - - @@rules_python~override~pip~pip_39 - - @@rules_python~override~pip~pip_310 - - The different spoke names are a combination of the hub_name and the Python version. - In the future we may remove this repository, but we do not support entry points. - yet, and that functionality exists in these repos. - We also need repositories for the wheels that the different pip spokes contain. For each Python version a different wheel repository is created. In our example - each pip spoke had a requirments file that contained websockets. We + each pip spoke had a requirements file that contained websockets. We then create two different wheel repositories that are named the following. - @@rules_python~override~pip~pip_39_websockets - @@rules_python~override~pip~pip_310_websockets - And if the wheel has any other dependies subsequest wheels are created in the same fashion. - - We also create a repository for the wheel alias. We want to just use the syntax - 'requirement("websockets")' we need to have an alias repository that is named: - - - @@rules_python~override~pip~pip_websockets + And if the wheel has any other dependencies subsequent wheels are created in the same fashion. - This repository contains alias statements for the different wheel components (pkg, data, etc). - Each of those aliases has a select that resolves to a spoke repository depending on - the Python version. + The hub repository has aliases for `pkg`, `data`, etc, which have a select that resolves to + a spoke repository depending on the Python version. Also we may have more than one hub as defined in a MODULES.bazel file. So we could have multiple hubs pointing to various different pip spokes. - Some other business rules notes. A hub can only have one spoke per Python version. We cannot + Some other business rules notes. A hub can only have one spoke per Python version. We cannot have a hub named "pip" that has two spokes that use the Python 3.9 interpreter. Second - we cannot have the same hub name used in submodules. The hub name has to be globally + we cannot have the same hub name used in sub-modules. The hub name has to be globally unique. - This implementation reuses elements of non-bzlmod code and also reuses the first implementation - of pip bzlmod, but adds the capability to have multiple pip.parse calls. - This implementation also handles the creation of whl_modification JSON files that are used - during the creation of wheel libraries. These JSON files used via the annotations argument + during the creation of wheel libraries. These JSON files used via the annotations argument when calling wheel_installer.py. Args: module_ctx: module contents - """ # Build all of the wheel modifications if the tag class is called. @@ -259,63 +229,46 @@ def _pip_impl(module_ctx): for mod in module_ctx.modules: for pip_attr in mod.tags.parse: hub_name = pip_attr.hub_name - if hub_name in pip_hub_map: - # We cannot have two hubs with the same name in different - # modules. - if pip_hub_map[hub_name].module_name != mod.name: - fail(( - "Duplicate cross-module pip hub named '{hub}': pip hub " + - "names must be unique across modules. First defined " + - "by module '{first_module}', second attempted by " + - "module '{second_module}'" - ).format( - hub = hub_name, - first_module = pip_hub_map[hub_name].module_name, - second_module = mod.name, - )) - - if pip_attr.python_version in pip_hub_map[hub_name].python_versions: - fail(( - "Duplicate pip python version '{version}' for hub " + - "'{hub}' in module '{module}': the Python versions " + - "used for a hub must be unique" - ).format( - hub = hub_name, - module = mod.name, - version = pip_attr.python_version, - )) - else: - pip_hub_map[pip_attr.hub_name].python_versions.append(pip_attr.python_version) - else: + if hub_name not in pip_hub_map: pip_hub_map[pip_attr.hub_name] = struct( module_name = mod.name, python_versions = [pip_attr.python_version], ) + elif pip_hub_map[hub_name].module_name != mod.name: + # We cannot have two hubs with the same name in different + # modules. + fail(( + "Duplicate cross-module pip hub named '{hub}': pip hub " + + "names must be unique across modules. First defined " + + "by module '{first_module}', second attempted by " + + "module '{second_module}'" + ).format( + hub = hub_name, + first_module = pip_hub_map[hub_name].module_name, + second_module = mod.name, + )) + + elif pip_attr.python_version in pip_hub_map[hub_name].python_versions: + fail(( + "Duplicate pip python version '{version}' for hub " + + "'{hub}' in module '{module}': the Python versions " + + "used for a hub must be unique" + ).format( + hub = hub_name, + module = mod.name, + version = pip_attr.python_version, + )) + else: + pip_hub_map[pip_attr.hub_name].python_versions.append(pip_attr.python_version) - _create_versioned_pip_and_whl_repos(module_ctx, pip_attr, hub_whl_map) + _create_whl_repos(module_ctx, pip_attr, hub_whl_map) for hub_name, whl_map in hub_whl_map.items(): - for whl_name, version_map in whl_map.items(): - if DEFAULT_PYTHON_VERSION in version_map: - whl_default_version = DEFAULT_PYTHON_VERSION - else: - whl_default_version = None - - # Create the alias repositories which contains different select - # statements These select statements point to the different pip - # whls that are based on a specific version of Python. - whl_library_alias( - name = hub_name + "_" + whl_name, - wheel_name = whl_name, - default_version = whl_default_version, - version_map = version_map, - ) - - # Create the hub repository for pip. pip_hub_repository_bzlmod( name = hub_name, repo_name = hub_name, - whl_library_alias_names = whl_map.keys(), + whl_map = whl_map, + default_version = full_version(DEFAULT_PYTHON_VERSION), ) def _pip_parse_ext_attrs(): @@ -428,6 +381,17 @@ cannot have a child module that uses the same `hub_name`. } return attrs +def _extension_extra_args(): + args = {} + + if bazel_features.external_deps.module_extension_has_os_arch_dependent: + args = args | { + "arch_dependent": True, + "os_dependent": True, + } + + return args + pip = module_extension( doc = """\ This extension is used to make dependencies from pip available. @@ -470,6 +434,7 @@ extension. """, ), }, + **_extension_extra_args() ) def _whl_mods_repo_impl(rctx): diff --git a/python/extensions/private/internal_deps.bzl b/python/extensions/private/internal_deps.bzl index 8a98b82827..aadf2cc997 100644 --- a/python/extensions/private/internal_deps.bzl +++ b/python/extensions/private/internal_deps.bzl @@ -9,9 +9,11 @@ "Python toolchain module extension for internal rule use" load("//python/pip_install:repositories.bzl", "pip_install_dependencies") +load("//python/private:internal_config_repo.bzl", "internal_config_repo") # buildifier: disable=unused-variable def _internal_deps_impl(module_ctx): + internal_config_repo(name = "rules_python_internal") pip_install_dependencies() internal_deps = module_extension( diff --git a/python/extensions/private/pythons_hub.bzl b/python/extensions/private/pythons_hub.bzl index a64f203bd6..f36ce45521 100644 --- a/python/extensions/private/pythons_hub.bzl +++ b/python/extensions/private/pythons_hub.bzl @@ -14,7 +14,8 @@ "Repo rule used by bzlmod extension to create a repo that has a map of Python interpreters and their labels" -load("//python:versions.bzl", "MINOR_MAPPING", "WINDOWS_NAME") +load("//python:versions.bzl", "WINDOWS_NAME") +load("//python/private:full_version.bzl", "full_version") load( "//python/private:toolchains_repo.bzl", "get_host_os_arch", @@ -28,12 +29,6 @@ def _have_same_length(*lists): fail("expected at least one list") return len({len(length): None for length in lists}) == 1 -def _get_version(python_version): - # we need to get the MINOR_MAPPING or use the full version - if python_version in MINOR_MAPPING: - python_version = MINOR_MAPPING[python_version] - return python_version - def _python_toolchain_build_file_content( prefixes, python_versions, @@ -55,7 +50,7 @@ def _python_toolchain_build_file_content( # build the toolchain content by calling python_toolchain_build_file_content return "\n".join([python_toolchain_build_file_content( prefix = prefixes[i], - python_version = _get_version(python_versions[i]), + python_version = full_version(python_versions[i]), set_python_version_constraint = set_python_version_constraints[i], user_repository_name = user_repository_names[i], rules_python = rules_python, diff --git a/python/extensions/python.bzl b/python/extensions/python.bzl index 2d007267b1..c7c2c82c05 100644 --- a/python/extensions/python.bzl +++ b/python/extensions/python.bzl @@ -86,16 +86,22 @@ def _python_impl(module_ctx): # much else that can be done. Modules don't know and can't control # what other modules do, so the first in the dependency graph wins. if toolchain_version in global_toolchain_versions: - _warn_duplicate_global_toolchain_version( - toolchain_version, - first = global_toolchain_versions[toolchain_version], - second_toolchain_name = toolchain_name, - second_module_name = mod.name, - ) + # If the python version is explicitly provided by the root + # module, they should not be warned for choosing the same + # version that rules_python provides as default. + first = global_toolchain_versions[toolchain_version] + if mod.name != "rules_python" or not first.is_root: + _warn_duplicate_global_toolchain_version( + toolchain_version, + first = first, + second_toolchain_name = toolchain_name, + second_module_name = mod.name, + ) continue global_toolchain_versions[toolchain_version] = struct( toolchain_name = toolchain_name, module_name = mod.name, + is_root = mod.is_root, ) # Only the root module and rules_python are allowed to specify the default diff --git a/python/packaging.bzl b/python/packaging.bzl index d9b9d02711..48423e307f 100644 --- a/python/packaging.bzl +++ b/python/packaging.bzl @@ -14,6 +14,7 @@ """Public API for for building wheels.""" +load("//python:py_binary.bzl", "py_binary") load("//python/private:py_package.bzl", "py_package_lib") load("//python/private:py_wheel.bzl", _PyWheelInfo = "PyWheelInfo", _py_wheel = "py_wheel") load("//python/private:util.bzl", "copy_propagating_kwargs") @@ -167,7 +168,7 @@ def py_wheel(name, twine = None, publish_args = [], **kwargs): # TODO: use py_binary from //python:defs.bzl after our stardoc setup is less brittle # buildifier: disable=native-py - native.py_binary( + py_binary( name = "{}.publish".format(name), srcs = [twine_main], args = twine_args, diff --git a/python/pip.bzl b/python/pip.bzl index 0c6e90f577..fb842cc4ce 100644 --- a/python/pip.bzl +++ b/python/pip.bzl @@ -17,8 +17,8 @@ load("//python/pip_install:pip_repository.bzl", "pip_repository", _package_annot load("//python/pip_install:repositories.bzl", "pip_install_dependencies") load("//python/pip_install:requirements.bzl", _compile_pip_requirements = "compile_pip_requirements") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") +load("//python/private:full_version.bzl", "full_version") load("//python/private:render_pkg_aliases.bzl", "NO_MATCH_ERROR_MESSAGE_TEMPLATE") -load(":versions.bzl", "MINOR_MAPPING") compile_pip_requirements = _compile_pip_requirements package_annotation = _package_annotation @@ -295,7 +295,7 @@ alias( for [python_version, repo_prefix] in version_map: alias.append("""\ "@{rules_python}//python/config_settings:is_python_{full_python_version}": "{actual}",""".format( - full_python_version = MINOR_MAPPING[python_version] if python_version in MINOR_MAPPING else python_version, + full_python_version = full_version(python_version), actual = "@{repo_prefix}{wheel_name}//:{alias_name}".format( repo_prefix = repo_prefix, wheel_name = wheel_name, diff --git a/python/pip_install/BUILD.bazel b/python/pip_install/BUILD.bazel index 4e4fbb4a1c..c071033384 100644 --- a/python/pip_install/BUILD.bazel +++ b/python/pip_install/BUILD.bazel @@ -1,3 +1,70 @@ +# 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. + +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +package( + default_visibility = ["//:__subpackages__"], +) + +bzl_library( + name = "pip_repository_bzl", + srcs = ["pip_repository.bzl"], + # Semi-public: What is intended to be public and what is intended to be + # internal is unclear. Some symbols are clearly public (e.g. + # package_annotations), some are clearly internal (e.g. + # pip_hub_repository_bzlmod), and many are unknown. + visibility = ["//visibility:public"], + deps = [ + ":repositories_bzl", + ":requirements_parser_bzl", + "//python:repositories_bzl", + "//python:versions_bzl", + "//python/pip_install/private:generate_whl_library_build_bazel_bzl", + "//python/pip_install/private:srcs_bzl", + "//python/private:bzlmod_enabled_bzl", + "//python/private:normalize_name_bzl", + "//python/private:render_pkg_aliases_bzl", + "//python/private:toolchains_repo_bzl", + "//python/private:which_bzl", + ], +) + +bzl_library( + name = "requirements_bzl", + srcs = ["requirements.bzl"], + deps = [ + ":repositories_bzl", + "//python:defs_bzl", + ], +) + +bzl_library( + name = "requirements_parser_bzl", + srcs = ["requirements_parser.bzl"], +) + +bzl_library( + name = "repositories_bzl", + srcs = ["repositories.bzl"], + deps = [ + "//:version_bzl", + "//python/private:bazel_tools_bzl", + "@bazel_skylib//lib:versions", + ], +) + filegroup( name = "distribution", srcs = glob(["*.bzl"]) + [ diff --git a/python/pip_install/pip_hub_repository_requirements_bzlmod.bzl.tmpl b/python/pip_install/pip_hub_repository_requirements_bzlmod.bzl.tmpl deleted file mode 100644 index 4a3d512ae7..0000000000 --- a/python/pip_install/pip_hub_repository_requirements_bzlmod.bzl.tmpl +++ /dev/null @@ -1,35 +0,0 @@ -"""Starlark representation of locked requirements. - -@generated by rules_python pip_parse repository rule -from %%REQUIREMENTS_LOCK%%. - -This file is different from the other bzlmod template -because we do not support entry_point yet. -""" - -all_requirements = %%ALL_REQUIREMENTS%% - -all_whl_requirements = %%ALL_WHL_REQUIREMENTS%% - -all_data_requirements = %%ALL_DATA_REQUIREMENTS%% - -def _clean_name(name): - return name.replace("-", "_").replace(".", "_").lower() - -def requirement(name): - return "%%MACRO_TMPL%%".format(_clean_name(name), "pkg") - -def whl_requirement(name): - return "%%MACRO_TMPL%%".format(_clean_name(name), "whl") - -def data_requirement(name): - return "%%MACRO_TMPL%%".format(_clean_name(name), "data") - -def dist_info_requirement(name): - return "%%MACRO_TMPL%%".format(_clean_name(name), "dist_info") - -def entry_point(pkg, script = None): - """entry_point returns the target of the canonical label of the package entrypoints. - """ - # TODO: https://github.com/bazelbuild/rules_python/issues/1262 - print("not implemented") diff --git a/python/pip_install/pip_repository.bzl b/python/pip_install/pip_repository.bzl index 87c7f6b77a..ea8b9eb5ac 100644 --- a/python/pip_install/pip_repository.bzl +++ b/python/pip_install/pip_repository.bzl @@ -14,7 +14,7 @@ "" -load("//python:repositories.bzl", "get_interpreter_dirname", "is_standalone_interpreter") +load("//python:repositories.bzl", "is_standalone_interpreter") load("//python:versions.bzl", "WINDOWS_NAME") load("//python/pip_install:repositories.bzl", "all_requirements") load("//python/pip_install:requirements_parser.bzl", parse_requirements = "parse") @@ -43,15 +43,11 @@ def _construct_pypath(rctx): Returns: String of the PYTHONPATH. """ - # Get the root directory of these rules - rules_root = rctx.path(Label("//:BUILD.bazel")).dirname - thirdparty_roots = [ - # Includes all the external dependencies from repositories.bzl - rctx.path(Label("@" + repo + "//:BUILD.bazel")).dirname - for repo in all_requirements - ] separator = ":" if not "windows" in rctx.os.name.lower() else ";" - pypath = separator.join([str(p) for p in [rules_root] + thirdparty_roots]) + pypath = separator.join([ + str(rctx.path(entry).dirname) + for entry in rctx.attr._python_path_entries + ]) return pypath def _get_python_interpreter_attr(rctx): @@ -123,7 +119,7 @@ def _get_xcode_location_cflags(rctx): "-isysroot {}/SDKs/MacOSX.sdk".format(xcode_root), ] -def _get_toolchain_unix_cflags(rctx): +def _get_toolchain_unix_cflags(rctx, python_interpreter): """Gather cflags from a standalone toolchain for unix systems. Pip won't be able to compile c extensions from sdists with the pre built python distributions from indygreg @@ -135,11 +131,11 @@ def _get_toolchain_unix_cflags(rctx): return [] # Only update the location when using a standalone toolchain. - if not is_standalone_interpreter(rctx, rctx.attr.python_interpreter_target): + if not is_standalone_interpreter(rctx, python_interpreter): return [] er = rctx.execute([ - rctx.path(rctx.attr.python_interpreter_target).realpath, + python_interpreter, "-c", "import sys; print(f'{sys.version_info[0]}.{sys.version_info[1]}', end='')", ]) @@ -147,7 +143,7 @@ def _get_toolchain_unix_cflags(rctx): fail("could not get python version from interpreter (status {}): {}".format(er.return_code, er.stderr)) _python_version = er.stdout include_path = "{}/include/python{}".format( - get_interpreter_dirname(rctx, rctx.attr.python_interpreter_target), + python_interpreter.dirname, _python_version, ) @@ -218,11 +214,12 @@ def _parse_optional_attrs(rctx, args): return args -def _create_repository_execution_environment(rctx): +def _create_repository_execution_environment(rctx, python_interpreter): """Create a environment dictionary for processes we spawn with rctx.execute. Args: - rctx: The repository context. + rctx (repository_ctx): The repository context. + python_interpreter (path): The resolved python interpreter. Returns: Dictionary of environment variable suitable to pass to rctx.execute. """ @@ -230,7 +227,7 @@ def _create_repository_execution_environment(rctx): # Gather any available CPPFLAGS values cppflags = [] cppflags.extend(_get_xcode_location_cflags(rctx)) - cppflags.extend(_get_toolchain_unix_cflags(rctx)) + cppflags.extend(_get_toolchain_unix_cflags(rctx, python_interpreter)) env = { "PYTHONPATH": _construct_pypath(rctx), @@ -270,10 +267,14 @@ A requirements_lock attribute must be specified, or a platform-specific lockfile """) return requirements_txt -def _create_pip_repository_bzlmod(rctx, bzl_packages, requirements): - repo_name = rctx.attr.repo_name - build_contents = _BUILD_FILE_CONTENTS - aliases = render_pkg_aliases(repo_name = repo_name, bzl_packages = bzl_packages) +def _pip_hub_repository_bzlmod_impl(rctx): + bzl_packages = rctx.attr.whl_map.keys() + aliases = render_pkg_aliases( + repo_name = rctx.attr.repo_name, + rules_python = rctx.attr._template.workspace_name, + default_version = rctx.attr.default_version, + whl_map = rctx.attr.whl_map, + ) for path, contents in aliases.items(): rctx.file(path, contents) @@ -283,7 +284,7 @@ def _create_pip_repository_bzlmod(rctx, bzl_packages, requirements): # `requirement`, et al. macros. macro_tmpl = "@@{name}//{{}}:{{}}".format(name = rctx.attr.name) - rctx.file("BUILD.bazel", build_contents) + rctx.file("BUILD.bazel", _BUILD_FILE_CONTENTS) rctx.template("requirements.bzl", rctx.attr._template, substitutions = { "%%ALL_DATA_REQUIREMENTS%%": _format_repr_list([ macro_tmpl.format(p, "data") @@ -299,24 +300,26 @@ def _create_pip_repository_bzlmod(rctx, bzl_packages, requirements): ]), "%%MACRO_TMPL%%": macro_tmpl, "%%NAME%%": rctx.attr.name, - "%%REQUIREMENTS_LOCK%%": requirements, }) -def _pip_hub_repository_bzlmod_impl(rctx): - bzl_packages = rctx.attr.whl_library_alias_names - _create_pip_repository_bzlmod(rctx, bzl_packages, "") - pip_hub_repository_bzlmod_attrs = { + "default_version": attr.string( + mandatory = True, + doc = """\ +This is the default python version in the format of X.Y.Z. This should match +what is setup by the 'python' extension using the 'is_default = True' +setting.""", + ), "repo_name": attr.string( mandatory = True, doc = "The apparent name of the repo. This is needed because in bzlmod, the name attribute becomes the canonical name.", ), - "whl_library_alias_names": attr.string_list( + "whl_map": attr.string_list_dict( mandatory = True, - doc = "The list of whl alias that we use to build aliases and the whl names", + doc = "The wheel map where values are python versions", ), "_template": attr.label( - default = ":pip_hub_repository_requirements_bzlmod.bzl.tmpl", + default = ":pip_repository_requirements_bzlmod.bzl.tmpl", ), } @@ -326,52 +329,6 @@ pip_hub_repository_bzlmod = repository_rule( implementation = _pip_hub_repository_bzlmod_impl, ) -def _pip_repository_bzlmod_impl(rctx): - requirements_txt = locked_requirements_label(rctx, rctx.attr) - content = rctx.read(requirements_txt) - parsed_requirements_txt = parse_requirements(content) - - packages = [(normalize_name(name), requirement) for name, requirement in parsed_requirements_txt.requirements] - - bzl_packages = sorted([name for name, _ in packages]) - _create_pip_repository_bzlmod(rctx, bzl_packages, str(requirements_txt)) - -pip_repository_bzlmod_attrs = { - "repo_name": attr.string( - mandatory = True, - doc = "The apparent name of the repo. This is needed because in bzlmod, the name attribute becomes the canonical name", - ), - "requirements_darwin": attr.label( - allow_single_file = True, - doc = "Override the requirements_lock attribute when the host platform is Mac OS", - ), - "requirements_linux": attr.label( - allow_single_file = True, - doc = "Override the requirements_lock attribute when the host platform is Linux", - ), - "requirements_lock": attr.label( - allow_single_file = True, - doc = """ -A fully resolved 'requirements.txt' pip requirement file containing the transitive set of your dependencies. If this file is passed instead -of 'requirements' no resolve will take place and pip_repository will create individual repositories for each of your dependencies so that -wheels are fetched/built only for the targets specified by 'build/run/test'. -""", - ), - "requirements_windows": attr.label( - allow_single_file = True, - doc = "Override the requirements_lock attribute when the host platform is Windows", - ), - "_template": attr.label( - default = ":pip_repository_requirements_bzlmod.bzl.tmpl", - ), -} - -pip_repository_bzlmod = repository_rule( - attrs = pip_repository_bzlmod_attrs, - doc = """A rule for bzlmod pip_repository creation. Intended for private use only.""", - implementation = _pip_repository_bzlmod_impl, -) - def _pip_repository_impl(rctx): requirements_txt = locked_requirements_label(rctx, rctx.attr) content = rctx.read(requirements_txt) @@ -630,7 +587,7 @@ def _whl_library_impl(rctx): result = rctx.execute( args, # Manually construct the PYTHONPATH since we cannot use the toolchain here - environment = _create_repository_execution_environment(rctx), + environment = _create_repository_execution_environment(rctx, python_interpreter), quiet = rctx.attr.quiet, timeout = rctx.attr.timeout, ) @@ -720,6 +677,19 @@ whl_library_attrs = { mandatory = True, doc = "Python requirement string describing the package to make available", ), + "_python_path_entries": attr.label_list( + # Get the root directory of these rules and keep them as a default attribute + # in order to avoid unnecessary repository fetching restarts. + # + # This is very similar to what was done in https://github.com/bazelbuild/rules_go/pull/3478 + default = [ + Label("//:BUILD.bazel"), + ] + [ + # Includes all the external dependencies from repositories.bzl + Label("@" + repo + "//:BUILD.bazel") + for repo in all_requirements + ], + ), } whl_library_attrs.update(**common_attrs) diff --git a/python/pip_install/pip_repository_requirements_bzlmod.bzl.tmpl b/python/pip_install/pip_repository_requirements_bzlmod.bzl.tmpl index 2df60b0b52..c72187c7ee 100644 --- a/python/pip_install/pip_repository_requirements_bzlmod.bzl.tmpl +++ b/python/pip_install/pip_repository_requirements_bzlmod.bzl.tmpl @@ -1,7 +1,6 @@ """Starlark representation of locked requirements. -@generated by rules_python pip_parse repository rule -from %%REQUIREMENTS_LOCK%%. +@generated by rules_python pip.parse bzlmod extension. """ all_requirements = %%ALL_REQUIREMENTS%% @@ -30,4 +29,19 @@ def entry_point(pkg, script = None): """ if not script: script = pkg - return "@@%%NAME%%_{}//:rules_python_wheel_entry_point_{}".format(_clean_name(pkg), script) + fail("""Please replace this instance of entry_point with the following: + +``` +load("@rules_python//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") + +py_console_script_binary( + name = "{pkg}", + pkg = "@%%{pkg_label}", + script = "{script}", +) +``` +""".format( + pkg = _clean_name(pkg), + pkg_label = "%%MACRO_TMPL%%".format(_clean_name(pkg), "pkg"), + script = script, + )) diff --git a/python/pip_install/private/BUILD.bazel b/python/pip_install/private/BUILD.bazel index 86b4b3d22c..2cc4cbd70d 100644 --- a/python/pip_install/private/BUILD.bazel +++ b/python/pip_install/private/BUILD.bazel @@ -1,3 +1,4 @@ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") load(":pip_install_utils.bzl", "srcs_module") package(default_visibility = ["//:__subpackages__"]) @@ -22,3 +23,16 @@ srcs_module( srcs = "//python/pip_install:py_srcs", dest = ":srcs.bzl", ) + +bzl_library( + name = "generate_whl_library_build_bazel_bzl", + srcs = ["generate_whl_library_build_bazel.bzl"], + deps = [ + "//python/private:normalize_name_bzl", + ], +) + +bzl_library( + name = "srcs_bzl", + srcs = ["srcs.bzl"], +) diff --git a/python/pip_install/repositories.bzl b/python/pip_install/repositories.bzl index 4b209b304c..b322a7007e 100644 --- a/python/pip_install/repositories.bzl +++ b/python/pip_install/repositories.bzl @@ -23,13 +23,13 @@ _RULE_DEPS = [ # START: maintained by 'bazel run //tools/private:update_pip_deps' ( "pypi__build", - "https://files.pythonhosted.org/packages/03/97/f58c723ff036a8d8b4d3115377c0a37ed05c1f68dd9a0d66dab5e82c5c1c/build-0.9.0-py3-none-any.whl", - "38a7a2b7a0bdc61a42a0a67509d88c71ecfc37b393baba770fae34e20929ff69", + "https://files.pythonhosted.org/packages/58/91/17b00d5fac63d3dca605f1b8269ba3c65e98059e1fd99d00283e42a454f0/build-0.10.0-py3-none-any.whl", + "af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171", ), ( "pypi__click", - "https://files.pythonhosted.org/packages/76/0a/b6c5f311e32aeb3b406e03c079ade51e905ea630fc19d1262a46249c1c86/click-8.0.1-py3-none-any.whl", - "fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6", + "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", + "ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", ), ( "pypi__colorama", @@ -38,8 +38,8 @@ _RULE_DEPS = [ ), ( "pypi__importlib_metadata", - "https://files.pythonhosted.org/packages/d7/31/74dcb59a601b95fce3b0334e8fc9db758f78e43075f22aeb3677dfb19f4c/importlib_metadata-1.4.0-py2.py3-none-any.whl", - "bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359", + "https://files.pythonhosted.org/packages/cc/37/db7ba97e676af155f5fcb1a35466f446eadc9104e25b83366e8088c9c926/importlib_metadata-6.8.0-py3-none-any.whl", + "3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb", ), ( "pypi__installer", @@ -48,13 +48,13 @@ _RULE_DEPS = [ ), ( "pypi__more_itertools", - "https://files.pythonhosted.org/packages/bd/3f/c4b3dbd315e248f84c388bd4a72b131a29f123ecacc37ffb2b3834546e42/more_itertools-8.13.0-py3-none-any.whl", - "c5122bffc5f104d37c1626b8615b511f3427aa5389b94d61e5ef8236bfbc3ddb", + "https://files.pythonhosted.org/packages/5a/cb/6dce742ea14e47d6f565589e859ad225f2a5de576d7696e0623b784e226b/more_itertools-10.1.0-py3-none-any.whl", + "64e0735fcfdc6f3464ea133afe8ea4483b1c5fe3a3d69852e6503b43a0b222e6", ), ( "pypi__packaging", - "https://files.pythonhosted.org/packages/8f/7b/42582927d281d7cb035609cd3a543ffac89b74f3f4ee8e1c50914bcb57eb/packaging-22.0-py3-none-any.whl", - "957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3", + "https://files.pythonhosted.org/packages/ab/c3/57f0601a2d4fe15de7a553c00adbc901425661bf048f2a22dfc500caf121/packaging-23.1-py3-none-any.whl", + "994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61", ), ( "pypi__pep517", @@ -63,18 +63,23 @@ _RULE_DEPS = [ ), ( "pypi__pip", - "https://files.pythonhosted.org/packages/09/bd/2410905c76ee14c62baf69e3f4aa780226c1bbfc9485731ad018e35b0cb5/pip-22.3.1-py3-none-any.whl", - "908c78e6bc29b676ede1c4d57981d490cb892eb45cd8c214ab6298125119e077", + "https://files.pythonhosted.org/packages/50/c2/e06851e8cc28dcad7c155f4753da8833ac06a5c704c109313b8d5a62968a/pip-23.2.1-py3-none-any.whl", + "7ccf472345f20d35bdc9d1841ff5f313260c2c33fe417f48c30ac46cccabf5be", ), ( "pypi__pip_tools", - "https://files.pythonhosted.org/packages/5e/e8/f6d7d1847c7351048da870417724ace5c4506e816b38db02f4d7c675c189/pip_tools-6.12.1-py3-none-any.whl", - "f0c0c0ec57b58250afce458e2e6058b1f30a4263db895b7d72fd6311bf1dc6f7", + "https://files.pythonhosted.org/packages/e8/df/47e6267c6b5cdae867adbdd84b437393e6202ce4322de0a5e0b92960e1d6/pip_tools-7.3.0-py3-none-any.whl", + "8717693288720a8c6ebd07149c93ab0be1fced0b5191df9e9decd3263e20d85e", + ), + ( + "pypi__pyproject_hooks", + "https://files.pythonhosted.org/packages/d5/ea/9ae603de7fbb3df820b23a70f6aff92bf8c7770043254ad8d2dc9d6bcba4/pyproject_hooks-1.0.0-py3-none-any.whl", + "283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8", ), ( "pypi__setuptools", - "https://files.pythonhosted.org/packages/7c/5b/3d92b9f0f7ca1645cba48c080b54fe7d8b1033a4e5720091d1631c4266db/setuptools-60.10.0-py3-none-any.whl", - "782ef48d58982ddb49920c11a0c5c9c0b02e7d7d1c2ad0aa44e1a1e133051c96", + "https://files.pythonhosted.org/packages/4f/ab/0bcfebdfc3bfa8554b2b2c97a555569c4c1ebc74ea288741ea8326c51906/setuptools-68.1.2-py3-none-any.whl", + "3d8083eed2d13afc9426f227b24fd1659489ec107c0e86cec2ffdde5c92e790b", ), ( "pypi__tomli", @@ -83,13 +88,13 @@ _RULE_DEPS = [ ), ( "pypi__wheel", - "https://files.pythonhosted.org/packages/bd/7c/d38a0b30ce22fc26ed7dbc087c6d00851fb3395e9d0dac40bec1f905030c/wheel-0.38.4-py3-none-any.whl", - "b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8", + "https://files.pythonhosted.org/packages/b8/8b/31273bf66016be6ad22bb7345c37ff350276cfd46e389a0c2ac5da9d9073/wheel-0.41.2-py3-none-any.whl", + "75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8", ), ( "pypi__zipp", - "https://files.pythonhosted.org/packages/f4/50/cc72c5bcd48f6e98219fc4a88a5227e9e28b81637a99c49feba1d51f4d50/zipp-1.0.0-py2.py3-none-any.whl", - "8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656", + "https://files.pythonhosted.org/packages/8c/08/d3006317aefe25ea79d3b76c9650afabaf6d63d1c8443b236e7405447503/zipp-3.16.2-py3-none-any.whl", + "679e51dd4403591b2d6838a48de3d283f3d188412a9782faadf845f298736ba0", ), # END: maintained by 'bazel run //tools/private:update_pip_deps' ] diff --git a/python/pip_install/requirements.bzl b/python/pip_install/requirements.bzl index 84ee203ffd..a48718151f 100644 --- a/python/pip_install/requirements.bzl +++ b/python/pip_install/requirements.bzl @@ -90,20 +90,23 @@ def compile_pip_requirements( loc.format(requirements_darwin) if requirements_darwin else "None", loc.format(requirements_windows) if requirements_windows else "None", "//%s:%s.update" % (native.package_name(), name), + "--resolver=backtracking", + "--allow-unsafe", ] + (["--generate-hashes"] if generate_hashes else []) + extra_args deps = [ requirement("build"), requirement("click"), requirement("colorama"), + requirement("importlib_metadata"), + requirement("more_itertools"), requirement("pep517"), requirement("pip"), requirement("pip_tools"), + requirement("pyproject_hooks"), requirement("setuptools"), requirement("tomli"), - requirement("importlib_metadata"), requirement("zipp"), - requirement("more_itertools"), Label("//python/runfiles:runfiles"), ] + extra_deps diff --git a/python/pip_install/tools/requirements.txt b/python/pip_install/tools/requirements.txt index e8de11216e..bf9fe46afd 100755 --- a/python/pip_install/tools/requirements.txt +++ b/python/pip_install/tools/requirements.txt @@ -1,14 +1,14 @@ -build==0.9 -click==8.0.1 +build +click colorama -importlib_metadata==1.4.0 +importlib_metadata installer -more_itertools==8.13.0 -packaging==22.0 +more_itertools +packaging pep517 -pip==22.3.1 -pip_tools==6.12.1 -setuptools==60.10 +pip +pip_tools +setuptools tomli -wheel==0.38.4 -zipp==1.0.0 +wheel +zipp diff --git a/python/pip_install/tools/wheel_installer/BUILD.bazel b/python/pip_install/tools/wheel_installer/BUILD.bazel index 6360ca5c70..0eadcc25f6 100644 --- a/python/pip_install/tools/wheel_installer/BUILD.bazel +++ b/python/pip_install/tools/wheel_installer/BUILD.bazel @@ -9,6 +9,7 @@ py_library( "wheel.py", "wheel_installer.py", ], + visibility = ["//third_party/rules_pycross/pycross/private:__subpackages__"], deps = [ requirement("installer"), requirement("pip"), diff --git a/python/private/BUILD.bazel b/python/private/BUILD.bazel index 29b5a6c885..f6e3012edd 100644 --- a/python/private/BUILD.bazel +++ b/python/private/BUILD.bazel @@ -13,14 +13,24 @@ # limitations under the License. load("@bazel_skylib//:bzl_library.bzl", "bzl_library") +load("//python:py_binary.bzl", "py_binary") +load("//python:py_library.bzl", "py_library") load("//python:versions.bzl", "print_toolchains_checksums") load(":stamp.bzl", "stamp_build_setting") +package( + default_visibility = ["//:__subpackages__"], +) + licenses(["notice"]) filegroup( name = "distribution", - srcs = glob(["**"]) + ["//python/private/proto:distribution"], + srcs = glob(["**"]) + [ + "//python/private/common:distribution", + "//python/private/proto:distribution", + "//tools/build_defs/python/private:distribution", + ], visibility = ["//python:__pkg__"], ) @@ -38,34 +48,45 @@ filegroup( ) bzl_library( - name = "reexports_bzl", - srcs = ["reexports.bzl"], - visibility = [ - "//docs:__pkg__", - "//python:__pkg__", + name = "autodetecting_toolchain_bzl", + srcs = ["autodetecting_toolchain.bzl"], + deps = [ + "//python:py_runtime_bzl", + "//python:py_runtime_pair_bzl", ], - deps = [":bazel_tools_bzl"], ) bzl_library( - name = "util_bzl", - srcs = ["util.bzl"], - visibility = [ - "//docs:__subpackages__", - "//python:__subpackages__", - ], - deps = ["@bazel_skylib//lib:types"], + name = "bzlmod_enabled_bzl", + srcs = ["bzlmod_enabled.bzl"], ) bzl_library( - name = "which_bzl", - srcs = ["which.bzl"], - visibility = [ - "//docs:__subpackages__", - "//python:__subpackages__", + name = "coverage_deps_bzl", + srcs = ["coverage_deps.bzl"], + deps = [ + ":bazel_tools_bzl", + ":version_label_bzl", ], ) +bzl_library( + name = "full_version_bzl", + srcs = ["full_version.bzl"], + deps = ["//python:versions_bzl"], +) + +bzl_library( + name = "internal_config_repo_bzl", + srcs = ["internal_config_repo.bzl"], + deps = [":bzlmod_enabled_bzl"], +) + +bzl_library( + name = "normalize_name_bzl", + srcs = ["normalize_name.bzl"], +) + bzl_library( name = "py_cc_toolchain_bzl", srcs = [ @@ -88,15 +109,123 @@ bzl_library( visibility = ["//python/cc:__pkg__"], ) +bzl_library( + name = "py_console_script_binary_bzl", + srcs = [ + "py_console_script_binary.bzl", + "py_console_script_gen.bzl", + ], + visibility = ["//python/entry_points:__pkg__"], + deps = [ + "//python:py_binary_bzl", + ], +) + +bzl_library( + name = "py_package_bzl", + srcs = ["py_package.bzl"], + visibility = ["//:__subpackages__"], +) + +bzl_library( + name = "py_runtime_pair_macro_bzl", + srcs = ["py_runtime_pair_macro.bzl"], + visibility = ["//:__subpackages__"], + deps = [":py_runtime_pair_rule_bzl"], +) + +bzl_library( + name = "py_runtime_pair_rule_bzl", + srcs = ["py_runtime_pair_rule.bzl"], + deps = [ + "//python:py_runtime_bzl", + "//python:py_runtime_info_bzl", + ], +) + +bzl_library( + name = "py_wheel_bzl", + srcs = ["py_wheel.bzl"], + visibility = ["//:__subpackages__"], + deps = [ + ":py_package_bzl", + ":stamp_bzl", + ], +) + +bzl_library( + name = "reexports_bzl", + srcs = ["reexports.bzl"], + visibility = [ + "//docs:__pkg__", + "//python:__pkg__", + ], + deps = [":bazel_tools_bzl"], +) + +bzl_library( + name = "render_pkg_aliases_bzl", + srcs = ["render_pkg_aliases.bzl"], + deps = [ + ":normalize_name_bzl", + ":text_util_bzl", + ":version_label_bzl", + ], +) + +bzl_library( + name = "stamp_bzl", + srcs = ["stamp.bzl"], + visibility = ["//:__subpackages__"], +) + +bzl_library( + name = "text_util_bzl", + srcs = ["text_util.bzl"], +) + +bzl_library( + name = "toolchains_repo_bzl", + srcs = ["toolchains_repo.bzl"], + deps = [ + ":which_bzl", + "//python:versions_bzl", + ], +) + +bzl_library( + name = "util_bzl", + srcs = ["util.bzl"], + visibility = [ + "//docs:__subpackages__", + "//python:__subpackages__", + ], + deps = ["@bazel_skylib//lib:types"], +) + +bzl_library( + name = "version_label_bzl", + srcs = ["version_label.bzl"], +) + +bzl_library( + name = "which_bzl", + srcs = ["which.bzl"], + visibility = [ + "//docs:__subpackages__", + "//python:__subpackages__", + ], +) + # @bazel_tools can't define bzl_library itself, so we just put a wrapper around it. bzl_library( name = "bazel_tools_bzl", srcs = [ - "@bazel_tools//tools/python:srcs_version.bzl", - "@bazel_tools//tools/python:toolchain.bzl", - "@bazel_tools//tools/python:utils.bzl", + # This set of sources is overly broad, but it's the only public + # target available across Bazel versions that has all the necessary + # sources. + "@bazel_tools//tools:bzl_srcs", ], - visibility = ["//python:__pkg__"], ) # Needed to define bzl_library targets for docgen. (We don't define the @@ -107,15 +236,35 @@ exports_files( "coverage.patch", "py_package.bzl", "py_wheel.bzl", + "py_wheel_normalize_pep440.bzl", "reexports.bzl", "stamp.bzl", "util.bzl", "py_cc_toolchain_rule.bzl", ], - visibility = ["//docs:__pkg__"], + visibility = ["//:__subpackages__"], ) # Used to determine the use of `--stamp` in Starlark rules stamp_build_setting(name = "stamp") print_toolchains_checksums(name = "print_toolchains_checksums") + +# Used for py_console_script_gen rule +py_binary( + name = "py_console_script_gen_py", + srcs = ["py_console_script_gen.py"], + main = "py_console_script_gen.py", + visibility = [ + "//visibility:public", + ], +) + +py_library( + name = "py_console_script_gen_lib", + srcs = ["py_console_script_gen.py"], + imports = ["../.."], + visibility = [ + "//tests/entry_points:__pkg__", + ], +) diff --git a/python/private/autodetecting_toolchain.bzl b/python/private/autodetecting_toolchain.bzl new file mode 100644 index 0000000000..3caa5aa8ca --- /dev/null +++ b/python/private/autodetecting_toolchain.bzl @@ -0,0 +1,70 @@ +# Copyright 2019 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. + +"""Definitions related to the Python toolchain.""" + +load("//python:py_runtime.bzl", "py_runtime") +load("//python:py_runtime_pair.bzl", "py_runtime_pair") + +def define_autodetecting_toolchain(name): + """Defines the autodetecting Python toolchain. + + Args: + name: The name of the toolchain to introduce. Must have value + "autodetecting_toolchain". This param is present only to make the + BUILD file more readable. + """ + if name != "autodetecting_toolchain": + fail("Python autodetecting toolchain must be named " + + "'autodetecting_toolchain'") + + # buildifier: disable=native-py + py_runtime( + name = "_autodetecting_py3_runtime", + interpreter = ":py3wrapper.sh", + python_version = "PY3", + stub_shebang = "#!/usr/bin/env python3", + visibility = ["//visibility:private"], + ) + + # This is a dummy runtime whose interpreter_path triggers the native rule + # logic to use the legacy behavior on Windows. + # TODO(#7844): Remove this target. + # buildifier: disable=native-py + py_runtime( + name = "_magic_sentinel_runtime", + interpreter_path = "/_magic_pyruntime_sentinel_do_not_use", + python_version = "PY3", + visibility = ["//visibility:private"], + ) + + py_runtime_pair( + name = "_autodetecting_py_runtime_pair", + py3_runtime = select({ + # If we're on windows, inject the sentinel to tell native rule logic + # that we attempted to use the autodetecting toolchain and need to + # switch back to legacy behavior. + # TODO(#7844): Remove this hack. + "@platforms//os:windows": ":_magic_sentinel_runtime", + "//conditions:default": ":_autodetecting_py3_runtime", + }), + visibility = ["//visibility:public"], + ) + + native.toolchain( + name = name, + toolchain = ":_autodetecting_py_runtime_pair", + toolchain_type = ":toolchain_type", + visibility = ["//visibility:public"], + ) diff --git a/python/private/autodetecting_toolchain_interpreter.sh b/python/private/autodetecting_toolchain_interpreter.sh new file mode 100644 index 0000000000..5c8a10d601 --- /dev/null +++ b/python/private/autodetecting_toolchain_interpreter.sh @@ -0,0 +1,57 @@ +#!/bin/sh + +# Don't set -e because we don't have robust trapping and printing of errors. +set -u + +# We use /bin/sh rather than /bin/bash for portability. See discussion here: +# https://groups.google.com/forum/?nomobile=true#!topic/bazel-dev/4Ql_7eDcLC0 +# We do lose the ability to set -o pipefail. + +FAILURE_HEADER="\ +Error occurred while attempting to use the default Python toolchain \ +(@rules_python//python:autodetecting_toolchain)." + +die() { + echo "$FAILURE_HEADER" 1>&2 + echo "$1" 1>&2 + exit 1 +} + +# We use `which` to locate the Python interpreter command on PATH. `command -v` +# is another option, but it doesn't check whether the file it finds has the +# executable bit. +# +# A tricky situation happens when this wrapper is invoked as part of running a +# tool, e.g. passing a py_binary target to `ctx.actions.run()`. Bazel will unset +# the PATH variable. Then the shell will see there's no PATH and initialize its +# own, sometimes without exporting it. This causes `which` to fail and this +# script to think there's no Python interpreter installed. To avoid this we +# explicitly pass PATH to each `which` invocation. We can't just export PATH +# because that would modify the environment seen by the final user Python +# program. +# +# See also: +# +# https://github.com/bazelbuild/continuous-integration/issues/578 +# https://github.com/bazelbuild/bazel/issues/8414 +# https://github.com/bazelbuild/bazel/issues/8415 + +# Try the "python3" command name first, then fall back on "python". +PYTHON_BIN="$(PATH="$PATH" which python3 2> /dev/null)" +if [ -z "${PYTHON_BIN:-}" ]; then + PYTHON_BIN="$(PATH="$PATH" which python 2>/dev/null)" +fi +if [ -z "${PYTHON_BIN:-}" ]; then + die "Neither 'python3' nor 'python' were found on the target \ +platform's PATH, which is: + +$PATH + +Please ensure an interpreter is available on this platform (and marked \ +executable), or else register an appropriate Python toolchain as per the \ +documentation for py_runtime_pair \ +(https://github.com/bazelbuild/rules_python/blob/master/docs/python.md#py_runtime_pair)." +fi + +exec "$PYTHON_BIN" "$@" + diff --git a/python/private/common/BUILD.bazel b/python/private/common/BUILD.bazel new file mode 100644 index 0000000000..f20e682e26 --- /dev/null +++ b/python/private/common/BUILD.bazel @@ -0,0 +1,204 @@ +# 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. + +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +package( + default_visibility = ["//python:__subpackages__"], +) + +bzl_library( + name = "attributes_bazel_bzl", + srcs = ["attributes_bazel.bzl"], +) + +bzl_library( + name = "attributes_bzl", + srcs = ["attributes.bzl"], + deps = [ + ":common_bzl", + ":providers_bzl", + ":py_internal_bzl", + ":semantics_bzl", + ], +) + +bzl_library( + name = "cc_helper_bzl", + srcs = ["cc_helper.bzl"], + deps = [":py_internal_bzl"], +) + +bzl_library( + name = "common_bazel_bzl", + srcs = ["common_bazel.bzl"], + deps = [ + ":common_bzl", + ":providers_bzl", + ":py_internal_bzl", + "@bazel_skylib//lib:paths", + ], +) + +bzl_library( + name = "common_bzl", + srcs = ["common.bzl"], + deps = [ + ":cc_helper_bzl", + ":providers_bzl", + ":py_internal_bzl", + ":semantics_bzl", + ], +) + +filegroup( + name = "distribution", + srcs = glob(["**"]), +) + +bzl_library( + name = "providers_bzl", + srcs = ["providers.bzl"], + deps = [ + ":semantics_bzl", + "@rules_python_internal//:rules_python_config_bzl", + ], +) + +bzl_library( + name = "py_binary_macro_bazel_bzl", + srcs = ["py_binary_macro_bazel.bzl"], + deps = [ + ":common_bzl", + ":py_binary_rule_bazel_bzl", + ], +) + +bzl_library( + name = "py_binary_rule_bazel_bzl", + srcs = ["py_binary_rule_bazel.bzl"], + deps = [ + ":attributes_bzl", + ":py_executable_bazel_bzl", + ":semantics_bzl", + "@bazel_skylib//lib:dicts", + ], +) + +bzl_library( + name = "py_executable_bazel_bzl", + srcs = ["py_executable_bazel.bzl"], + deps = [ + ":attributes_bazel_bzl", + ":common_bazel_bzl", + ":common_bzl", + ":providers_bzl", + ":py_executable_bzl", + ":py_internal_bzl", + ":semantics_bzl", + ], +) + +bzl_library( + name = "py_executable_bzl", + srcs = ["py_executable.bzl"], + deps = [ + ":attributes_bzl", + ":cc_helper_bzl", + ":common_bzl", + ":providers_bzl", + ":py_internal_bzl", + "@bazel_skylib//lib:dicts", + ], +) + +bzl_library( + name = "py_internal_bzl", + srcs = ["py_internal.bzl"], + deps = ["@rules_python_internal//:py_internal_bzl"], +) + +bzl_library( + name = "py_library_bzl", + srcs = ["py_library.bzl"], + deps = [ + ":attributes_bzl", + ":common_bzl", + ":providers_bzl", + ":py_internal_bzl", + "@bazel_skylib//lib:dicts", + ], +) + +bzl_library( + name = "py_library_macro_bazel_bzl", + srcs = ["py_library_macro_bazel.bzl"], + deps = [":py_library_rule_bazel_bzl"], +) + +bzl_library( + name = "py_library_rule_bazel_bzl", + srcs = ["py_library_rule_bazel.bzl"], + deps = [ + ":attributes_bazel_bzl", + ":common_bazel_bzl", + ":common_bzl", + ":py_library_bzl", + ], +) + +bzl_library( + name = "py_runtime_macro_bzl", + srcs = ["py_runtime_macro.bzl"], + deps = [":py_runtime_rule_bzl"], +) + +bzl_library( + name = "py_runtime_rule_bzl", + srcs = ["py_runtime_rule.bzl"], + deps = [ + ":attributes_bzl", + ":common_bzl", + ":providers_bzl", + ":py_internal_bzl", + "@bazel_skylib//lib:dicts", + "@bazel_skylib//lib:paths", + ], +) + +bzl_library( + name = "py_test_macro_bazel_bzl", + srcs = ["py_test_macro_bazel.bzl"], + deps = [ + ":common_bazel_bzl", + ":py_test_rule_bazel_bzl", + ], +) + +bzl_library( + name = "py_test_rule_bazel_bzl", + srcs = ["py_test_rule_bazel.bzl"], + deps = [ + ":attributes_bzl", + ":common_bzl", + ":py_executable_bazel_bzl", + ":semantics_bzl", + "@bazel_skylib//lib:dicts", + ], +) + +bzl_library( + name = "semantics_bzl", + srcs = ["semantics.bzl"], +) diff --git a/python/private/common/attributes.bzl b/python/private/common/attributes.bzl new file mode 100644 index 0000000000..6e184c0c8f --- /dev/null +++ b/python/private/common/attributes.bzl @@ -0,0 +1,242 @@ +# Copyright 2022 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. +"""Attributes for Python rules.""" + +load(":common.bzl", "union_attrs") +load(":providers.bzl", "PyInfo") +load(":py_internal.bzl", "py_internal") +load( + ":semantics.bzl", + "DEPS_ATTR_ALLOW_RULES", + "PLATFORMS_LOCATION", + "SRCS_ATTR_ALLOW_FILES", + "TOOLS_REPO", +) + +# TODO: Load CcInfo from rules_cc +_CcInfo = CcInfo +_PackageSpecificationInfo = getattr(py_internal, "PackageSpecificationInfo", None) + +_STAMP_VALUES = [-1, 0, 1] + +def create_stamp_attr(**kwargs): + return {"stamp": attr.int(values = _STAMP_VALUES, **kwargs)} + +def create_srcs_attr(*, mandatory): + return { + "srcs": attr.label_list( + # Google builds change the set of allowed files. + allow_files = SRCS_ATTR_ALLOW_FILES, + mandatory = mandatory, + # Necessary for --compile_one_dependency to work. + flags = ["DIRECT_COMPILE_TIME_INPUT"], + ), + } + +SRCS_VERSION_ALL_VALUES = ["PY2", "PY2ONLY", "PY2AND3", "PY3", "PY3ONLY"] +SRCS_VERSION_NON_CONVERSION_VALUES = ["PY2AND3", "PY2ONLY", "PY3ONLY"] + +def create_srcs_version_attr(values): + return { + "srcs_version": attr.string( + default = "PY2AND3", + values = values, + ), + } + +def copy_common_binary_kwargs(kwargs): + return { + key: kwargs[key] + for key in BINARY_ATTR_NAMES + if key in kwargs + } + +def copy_common_test_kwargs(kwargs): + return { + key: kwargs[key] + for key in TEST_ATTR_NAMES + if key in kwargs + } + +CC_TOOLCHAIN = { + # NOTE: The `cc_helper.find_cpp_toolchain()` function expects the attribute + # name to be this name. + "_cc_toolchain": attr.label(default = "@" + TOOLS_REPO + "//tools/cpp:current_cc_toolchain"), +} + +# The common "data" attribute definition. +DATA_ATTRS = { + # NOTE: The "flags" attribute is deprecated, but there isn't an alternative + # way to specify that constraints should be ignored. + "data": attr.label_list( + allow_files = True, + flags = ["SKIP_CONSTRAINTS_OVERRIDE"], + ), +} + +def _create_native_rules_allowlist_attrs(): + if py_internal: + # The fragment and name are validated when configuration_field is called + default = configuration_field( + fragment = "py", + name = "native_rules_allowlist", + ) + + # A None provider isn't allowed + providers = [_PackageSpecificationInfo] + else: + default = None + providers = [] + + return { + "_native_rules_allowlist": attr.label( + default = default, + providers = providers, + ), + } + +NATIVE_RULES_ALLOWLIST_ATTRS = _create_native_rules_allowlist_attrs() + +# Attributes common to all rules. +COMMON_ATTRS = union_attrs( + DATA_ATTRS, + NATIVE_RULES_ALLOWLIST_ATTRS, + { + # NOTE: This attribute is deprecated and slated for removal. + "distribs": attr.string_list(), + # TODO(b/148103851): This attribute is deprecated and slated for + # removal. + # NOTE: The license attribute is missing in some Java integration tests, + # so fallback to a regular string_list for that case. + # buildifier: disable=attr-license + "licenses": attr.license() if hasattr(attr, "license") else attr.string_list(), + }, + allow_none = True, +) + +# Attributes common to rules accepting Python sources and deps. +PY_SRCS_ATTRS = union_attrs( + { + "deps": attr.label_list( + providers = [[PyInfo], [_CcInfo]], + # TODO(b/228692666): Google-specific; remove these allowances once + # the depot is cleaned up. + allow_rules = DEPS_ATTR_ALLOW_RULES, + ), + # Required attribute, but details vary by rule. + # Use create_srcs_attr to create one. + "srcs": None, + # NOTE: In Google, this attribute is deprecated, and can only + # effectively be PY3 or PY3ONLY. Externally, with Bazel, this attribute + # has a separate story. + # Required attribute, but the details vary by rule. + # Use create_srcs_version_attr to create one. + "srcs_version": None, + }, + allow_none = True, +) + +# Attributes specific to Python executable-equivalent rules. Such rules may not +# accept Python sources (e.g. some packaged-version of a py_test/py_binary), but +# still accept Python source-agnostic settings. +AGNOSTIC_EXECUTABLE_ATTRS = union_attrs( + DATA_ATTRS, + { + "env": attr.string_dict( + doc = """\ +Dictionary of strings; optional; values are subject to `$(location)` and "Make +variable" substitution. + +Specifies additional environment variables to set when the target is executed by +`test` or `run`. +""", + ), + # The value is required, but varies by rule and/or rule type. Use + # create_stamp_attr to create one. + "stamp": None, + }, + allow_none = True, +) + +# Attributes specific to Python test-equivalent executable rules. Such rules may +# not accept Python sources (e.g. some packaged-version of a py_test/py_binary), +# but still accept Python source-agnostic settings. +AGNOSTIC_TEST_ATTRS = union_attrs( + AGNOSTIC_EXECUTABLE_ATTRS, + # Tests have stamping disabled by default. + create_stamp_attr(default = 0), + { + "env_inherit": attr.string_list( + doc = """\ +List of strings; optional + +Specifies additional environment variables to inherit from the external +environment when the test is executed by bazel test. +""", + ), + # TODO(b/176993122): Remove when Bazel automatically knows to run on darwin. + "_apple_constraints": attr.label_list( + default = [ + PLATFORMS_LOCATION + "/os:ios", + PLATFORMS_LOCATION + "/os:macos", + PLATFORMS_LOCATION + "/os:tvos", + PLATFORMS_LOCATION + "/os:visionos", + PLATFORMS_LOCATION + "/os:watchos", + ], + ), + }, +) + +# Attributes specific to Python binary-equivalent executable rules. Such rules may +# not accept Python sources (e.g. some packaged-version of a py_test/py_binary), +# but still accept Python source-agnostic settings. +AGNOSTIC_BINARY_ATTRS = union_attrs( + AGNOSTIC_EXECUTABLE_ATTRS, + create_stamp_attr(default = -1), +) + +# Attribute names common to all Python rules +COMMON_ATTR_NAMES = [ + "compatible_with", + "deprecation", + "distribs", # NOTE: Currently common to all rules, but slated for removal + "exec_compatible_with", + "exec_properties", + "features", + "restricted_to", + "tags", + "target_compatible_with", + # NOTE: The testonly attribute requires careful handling: None/unset means + # to use the `package(default_testonly`) value, which isn't observable + # during the loading phase. + "testonly", + "toolchains", + "visibility", +] + COMMON_ATTRS.keys() + +# Attribute names common to all test=True rules +TEST_ATTR_NAMES = COMMON_ATTR_NAMES + [ + "args", + "size", + "timeout", + "flaky", + "shard_count", + "local", +] + AGNOSTIC_TEST_ATTRS.keys() + +# Attribute names common to all executable=True rules +BINARY_ATTR_NAMES = COMMON_ATTR_NAMES + [ + "args", + "output_licenses", # NOTE: Common to all rules, but slated for removal +] + AGNOSTIC_BINARY_ATTRS.keys() diff --git a/python/private/common/attributes_bazel.bzl b/python/private/common/attributes_bazel.bzl new file mode 100644 index 0000000000..f87245d6ff --- /dev/null +++ b/python/private/common/attributes_bazel.bzl @@ -0,0 +1,30 @@ +# Copyright 2022 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. +"""Attributes specific to the Bazel implementation of the Python rules.""" + +IMPORTS_ATTRS = { + "imports": attr.string_list( + doc = """ +List of import directories to be added to the PYTHONPATH. + +Subject to "Make variable" substitution. These import directories will be added +for this rule and all rules that depend on it (note: not the rules this rule +depends on. Each directory will be added to `PYTHONPATH` by `py_binary` rules +that depend on this rule. The strings are repo-runfiles-root relative, + +Absolute paths (paths that start with `/`) and paths that references a path +above the execution root are not allowed and will result in an error. +""", + ), +} diff --git a/python/private/common/cc_helper.bzl b/python/private/common/cc_helper.bzl new file mode 100644 index 0000000000..552b42eae8 --- /dev/null +++ b/python/private/common/cc_helper.bzl @@ -0,0 +1,23 @@ +# 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. +"""PYTHON RULE IMPLEMENTATION ONLY: Do not use outside of the rule implementations and their tests. + +Adapter for accessing Bazel's internal cc_helper. + +These may change at any time and are closely coupled to the rule implementation. +""" + +load(":py_internal.bzl", "py_internal") + +cc_helper = getattr(py_internal, "cc_helper", None) diff --git a/python/private/common/common.bzl b/python/private/common/common.bzl new file mode 100644 index 0000000000..bffbf6f0cf --- /dev/null +++ b/python/private/common/common.bzl @@ -0,0 +1,526 @@ +# Copyright 2022 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. +"""Various things common to Bazel and Google rule implementations.""" + +load(":cc_helper.bzl", "cc_helper") +load(":providers.bzl", "PyInfo") +load(":py_internal.bzl", "py_internal") +load( + ":semantics.bzl", + "NATIVE_RULES_MIGRATION_FIX_CMD", + "NATIVE_RULES_MIGRATION_HELP_URL", + "TOOLS_REPO", +) + +_testing = testing +_platform_common = platform_common +_coverage_common = coverage_common +_py_builtins = py_internal +PackageSpecificationInfo = getattr(py_internal, "PackageSpecificationInfo", None) + +TOOLCHAIN_TYPE = "@" + TOOLS_REPO + "//tools/python:toolchain_type" + +# Extensions without the dot +_PYTHON_SOURCE_EXTENSIONS = ["py"] + +# NOTE: Must stay in sync with the value used in rules_python +_MIGRATION_TAG = "__PYTHON_RULES_MIGRATION_DO_NOT_USE_WILL_BREAK__" + +def create_binary_semantics_struct( + *, + create_executable, + get_cc_details_for_binary, + get_central_uncachable_version_file, + get_coverage_deps, + get_debugger_deps, + get_extra_common_runfiles_for_binary, + get_extra_providers, + get_extra_write_build_data_env, + get_interpreter_path, + get_imports, + get_native_deps_dso_name, + get_native_deps_user_link_flags, + get_stamp_flag, + maybe_precompile, + should_build_native_deps_dso, + should_create_init_files, + should_include_build_data): + """Helper to ensure a semantics struct has all necessary fields. + + Call this instead of a raw call to `struct(...)`; it'll help ensure all + the necessary functions are being correctly provided. + + Args: + create_executable: Callable; creates a binary's executable output. See + py_executable.bzl#py_executable_base_impl for details. + get_cc_details_for_binary: Callable that returns a `CcDetails` struct; see + `create_cc_detail_struct`. + get_central_uncachable_version_file: Callable that returns an optional + Artifact; this artifact is special: it is never cached and is a copy + of `ctx.version_file`; see py_builtins.copy_without_caching + get_coverage_deps: Callable that returns a list of Targets for making + coverage work; only called if coverage is enabled. + get_debugger_deps: Callable that returns a list of Targets that provide + custom debugger support; only called for target-configuration. + get_extra_common_runfiles_for_binary: Callable that returns a runfiles + object of extra runfiles a binary should include. + get_extra_providers: Callable that returns extra providers; see + py_executable.bzl#_create_providers for details. + get_extra_write_build_data_env: Callable that returns a dict[str, str] + of additional environment variable to pass to build data generation. + get_interpreter_path: Callable that returns an optional string, which is + the path to the Python interpreter to use for running the binary. + get_imports: Callable that returns a list of the target's import + paths (from the `imports` attribute, so just the target's own import + path strings, not from dependencies). + get_native_deps_dso_name: Callable that returns a string, which is the + basename (with extension) of the native deps DSO library. + get_native_deps_user_link_flags: Callable that returns a list of strings, + which are any extra linker flags to pass onto the native deps DSO + linking action. + get_stamp_flag: Callable that returns bool of if the --stamp flag was + enabled or not. + maybe_precompile: Callable that may optional precompile the input `.py` + sources and returns the full set of desired outputs derived from + the source files (e.g., both py and pyc, only one of them, etc). + should_build_native_deps_dso: Callable that returns bool; True if + building a native deps DSO is supported, False if not. + should_create_init_files: Callable that returns bool; True if + `__init__.py` files should be generated, False if not. + should_include_build_data: Callable that returns bool; True if + build data should be generated, False if not. + Returns: + A "BinarySemantics" struct. + """ + return struct( + # keep-sorted + create_executable = create_executable, + get_cc_details_for_binary = get_cc_details_for_binary, + get_central_uncachable_version_file = get_central_uncachable_version_file, + get_coverage_deps = get_coverage_deps, + get_debugger_deps = get_debugger_deps, + get_extra_common_runfiles_for_binary = get_extra_common_runfiles_for_binary, + get_extra_providers = get_extra_providers, + get_extra_write_build_data_env = get_extra_write_build_data_env, + get_imports = get_imports, + get_interpreter_path = get_interpreter_path, + get_native_deps_dso_name = get_native_deps_dso_name, + get_native_deps_user_link_flags = get_native_deps_user_link_flags, + get_stamp_flag = get_stamp_flag, + maybe_precompile = maybe_precompile, + should_build_native_deps_dso = should_build_native_deps_dso, + should_create_init_files = should_create_init_files, + should_include_build_data = should_include_build_data, + ) + +def create_library_semantics_struct( + *, + get_cc_info_for_library, + get_imports, + maybe_precompile): + """Create a `LibrarySemantics` struct. + + Call this instead of a raw call to `struct(...)`; it'll help ensure all + the necessary functions are being correctly provided. + + Args: + get_cc_info_for_library: Callable that returns a CcInfo for the library; + see py_library_impl for arg details. + get_imports: Callable; see create_binary_semantics_struct. + maybe_precompile: Callable; see create_binary_semantics_struct. + Returns: + a `LibrarySemantics` struct. + """ + return struct( + # keep sorted + get_cc_info_for_library = get_cc_info_for_library, + get_imports = get_imports, + maybe_precompile = maybe_precompile, + ) + +def create_cc_details_struct( + *, + cc_info_for_propagating, + cc_info_for_self_link, + cc_info_with_extra_link_time_libraries, + extra_runfiles, + cc_toolchain): + """Creates a CcDetails struct. + + Args: + cc_info_for_propagating: CcInfo that is propagated out of the target + by returning it within a PyCcLinkParamsProvider object. + cc_info_for_self_link: CcInfo that is used when linking for the + binary (or its native deps DSO) itself. This may include extra + information that isn't propagating (e.g. a custom malloc) + cc_info_with_extra_link_time_libraries: CcInfo of extra link time + libraries that MUST come after `cc_info_for_self_link` (or possibly + always last; not entirely clear) when passed to + `link.linking_contexts`. + extra_runfiles: runfiles of extra files needed at runtime, usually as + part of `cc_info_with_extra_link_time_libraries`; should be added to + runfiles. + cc_toolchain: CcToolchain that should be used when building. + + Returns: + A `CcDetails` struct. + """ + return struct( + cc_info_for_propagating = cc_info_for_propagating, + cc_info_for_self_link = cc_info_for_self_link, + cc_info_with_extra_link_time_libraries = cc_info_with_extra_link_time_libraries, + extra_runfiles = extra_runfiles, + cc_toolchain = cc_toolchain, + ) + +def create_executable_result_struct(*, extra_files_to_build, output_groups): + """Creates a `CreateExecutableResult` struct. + + This is the return value type of the semantics create_executable function. + + Args: + extra_files_to_build: depset of File; additional files that should be + included as default outputs. + output_groups: dict[str, depset[File]]; additional output groups that + should be returned. + + Returns: + A `CreateExecutableResult` struct. + """ + return struct( + extra_files_to_build = extra_files_to_build, + output_groups = output_groups, + ) + +def union_attrs(*attr_dicts, allow_none = False): + """Helper for combining and building attriute dicts for rules. + + Similar to dict.update, except: + * Duplicate keys raise an error if they aren't equal. This is to prevent + unintentionally replacing an attribute with a potentially incompatible + definition. + * None values are special: They mean the attribute is required, but the + value should be provided by another attribute dict (depending on the + `allow_none` arg). + Args: + *attr_dicts: The dicts to combine. + allow_none: bool, if True, then None values are allowed. If False, + then one of `attrs_dicts` must set a non-None value for keys + with a None value. + + Returns: + dict of attributes. + """ + result = {} + missing = {} + for attr_dict in attr_dicts: + for attr_name, value in attr_dict.items(): + if value == None and not allow_none: + if attr_name not in result: + missing[attr_name] = None + else: + if attr_name in missing: + missing.pop(attr_name) + + if attr_name not in result or result[attr_name] == None: + result[attr_name] = value + elif value != None and result[attr_name] != value: + fail("Duplicate attribute name: '{}': existing={}, new={}".format( + attr_name, + result[attr_name], + value, + )) + + # Else, they're equal, so do nothing. This allows merging dicts + # that both define the same key from a common place. + + if missing and not allow_none: + fail("Required attributes missing: " + csv(missing.keys())) + return result + +def csv(values): + """Convert a list of strings to comma separated value string.""" + return ", ".join(sorted(values)) + +def filter_to_py_srcs(srcs): + """Filters .py files from the given list of files""" + + # TODO(b/203567235): Get the set of recognized extensions from + # elsewhere, as there may be others. e.g. Bazel recognizes .py3 + # as a valid extension. + return [f for f in srcs if f.extension == "py"] + +def collect_imports(ctx, semantics): + return depset(direct = semantics.get_imports(ctx), transitive = [ + dep[PyInfo].imports + for dep in ctx.attr.deps + if PyInfo in dep + ]) + +def collect_runfiles(ctx, files): + """Collects the necessary files from the rule's context. + + This presumes the ctx is for a py_binary, py_test, or py_library rule. + + Args: + ctx: rule ctx + files: depset of extra files to include in the runfiles. + Returns: + runfiles necessary for the ctx's target. + """ + return ctx.runfiles( + transitive_files = files, + # This little arg carries a lot of weight, but because Starlark doesn't + # have a way to identify if a target is just a File, the equivalent + # logic can't be re-implemented in pure-Starlark. + # + # Under the hood, it calls the Java `Runfiles#addRunfiles(ctx, + # DEFAULT_RUNFILES)` method, which is the what the Java implementation + # of the Python rules originally did, and the details of how that method + # works have become relied on in various ways. Specifically, what it + # does is visit the srcs, deps, and data attributes in the following + # ways: + # + # For each target in the "data" attribute... + # If the target is a File, then add that file to the runfiles. + # Otherwise, add the target's **data runfiles** to the runfiles. + # + # Note that, contray to best practice, the default outputs of the + # targets in `data` are *not* added, nor are the default runfiles. + # + # This ends up being important for several reasons, some of which are + # specific to Google-internal features of the rules. + # * For Python executables, we have to use `data_runfiles` to avoid + # conflicts for the build data files. Such files have + # target-specific content, but uses a fixed location, so if a + # binary has another binary in `data`, and both try to specify a + # file for that file path, then a warning is printed and an + # arbitrary one will be used. + # * For rules with _entirely_ different sets of files in data runfiles + # vs default runfiles vs default outputs. For example, + # proto_library: documented behavior of this rule is that putting it + # in the `data` attribute will cause the transitive closure of + # `.proto` source files to be included. This set of sources is only + # in the `data_runfiles` (`default_runfiles` is empty). + # * For rules with a _subset_ of files in data runfiles. For example, + # a certain Google rule used for packaging arbitrary binaries will + # generate multiple versions of a binary (e.g. different archs, + # stripped vs un-stripped, etc) in its default outputs, but only + # one of them in the runfiles; this helps avoid large, unused + # binaries contributing to remote executor input limits. + # + # Unfortunately, the above behavior also results in surprising behavior + # in some cases. For example, simple custom rules that only return their + # files in their default outputs won't have their files included. Such + # cases must either return their files in runfiles, or use `filegroup()` + # which will do so for them. + # + # For each target in "srcs" and "deps"... + # Add the default runfiles of the target to the runfiles. While this + # is desirable behavior, it also ends up letting a `py_library` + # be put in `srcs` and still mostly work. + # TODO(b/224640180): Reject py_library et al rules in srcs. + collect_default = True, + ) + +def create_py_info(ctx, *, direct_sources, imports): + """Create PyInfo provider. + + Args: + ctx: rule ctx. + direct_sources: depset of Files; the direct, raw `.py` sources for the + target. This should only be Python source files. It should not + include pyc files. + imports: depset of strings; the import path values to propagate. + + Returns: + A tuple of the PyInfo instance and a depset of the + transitive sources collected from dependencies (the latter is only + necessary for deprecated extra actions support). + """ + uses_shared_libraries = False + has_py2_only_sources = ctx.attr.srcs_version in ("PY2", "PY2ONLY") + has_py3_only_sources = ctx.attr.srcs_version in ("PY3", "PY3ONLY") + transitive_sources_depsets = [] # list of depsets + transitive_sources_files = [] # list of Files + for target in ctx.attr.deps: + # PyInfo may not be present for e.g. cc_library rules. + if PyInfo in target: + info = target[PyInfo] + transitive_sources_depsets.append(info.transitive_sources) + uses_shared_libraries = uses_shared_libraries or info.uses_shared_libraries + has_py2_only_sources = has_py2_only_sources or info.has_py2_only_sources + has_py3_only_sources = has_py3_only_sources or info.has_py3_only_sources + else: + # TODO(b/228692666): Remove this once non-PyInfo targets are no + # longer supported in `deps`. + files = target.files.to_list() + for f in files: + if f.extension == "py": + transitive_sources_files.append(f) + uses_shared_libraries = ( + uses_shared_libraries or + cc_helper.is_valid_shared_library_artifact(f) + ) + deps_transitive_sources = depset( + direct = transitive_sources_files, + transitive = transitive_sources_depsets, + ) + + # We only look at data to calculate uses_shared_libraries, if it's already + # true, then we don't need to waste time looping over it. + if not uses_shared_libraries: + # Similar to the above, except we only calculate uses_shared_libraries + for target in ctx.attr.data: + # TODO(b/234730058): Remove checking for PyInfo in data once depot + # cleaned up. + if PyInfo in target: + info = target[PyInfo] + uses_shared_libraries = info.uses_shared_libraries + else: + files = target.files.to_list() + for f in files: + uses_shared_libraries = cc_helper.is_valid_shared_library_artifact(f) + if uses_shared_libraries: + break + if uses_shared_libraries: + break + + # TODO(b/203567235): Set `uses_shared_libraries` field, though the Bazel + # docs indicate it's unused in Bazel and may be removed. + py_info = PyInfo( + transitive_sources = depset( + transitive = [deps_transitive_sources, direct_sources], + ), + imports = imports, + # NOTE: This isn't strictly correct, but with Python 2 gone, + # the srcs_version logic is largely defunct, so shouldn't matter in + # practice. + has_py2_only_sources = has_py2_only_sources, + has_py3_only_sources = has_py3_only_sources, + uses_shared_libraries = uses_shared_libraries, + ) + return py_info, deps_transitive_sources + +def create_instrumented_files_info(ctx): + return _coverage_common.instrumented_files_info( + ctx, + source_attributes = ["srcs"], + dependency_attributes = ["deps", "data"], + extensions = _PYTHON_SOURCE_EXTENSIONS, + ) + +def create_output_group_info(transitive_sources, extra_groups): + return OutputGroupInfo( + compilation_prerequisites_INTERNAL_ = transitive_sources, + compilation_outputs = transitive_sources, + **extra_groups + ) + +def maybe_add_test_execution_info(providers, ctx): + """Adds ExecutionInfo, if necessary for proper test execution. + + Args: + providers: Mutable list of providers; may have ExecutionInfo + provider appended. + ctx: Rule ctx. + """ + + # When built for Apple platforms, require the execution to be on a Mac. + # TODO(b/176993122): Remove when bazel automatically knows to run on darwin. + if target_platform_has_any_constraint(ctx, ctx.attr._apple_constraints): + providers.append(_testing.ExecutionInfo({"requires-darwin": ""})) + +_BOOL_TYPE = type(True) + +def is_bool(v): + return type(v) == _BOOL_TYPE + +def target_platform_has_any_constraint(ctx, constraints): + """Check if target platform has any of a list of constraints. + + Args: + ctx: rule context. + constraints: label_list of constraints. + + Returns: + True if target platform has at least one of the constraints. + """ + for constraint in constraints: + constraint_value = constraint[_platform_common.ConstraintValueInfo] + if ctx.target_platform_has_constraint(constraint_value): + return True + return False + +def check_native_allowed(ctx): + """Check if the usage of the native rule is allowed. + + Args: + ctx: rule context to check + """ + if not ctx.fragments.py.disallow_native_rules: + return + + if _MIGRATION_TAG in ctx.attr.tags: + return + + # NOTE: The main repo name is empty in *labels*, but not in + # ctx.workspace_name + is_main_repo = not bool(ctx.label.workspace_name) + if is_main_repo: + check_label = ctx.label + else: + # package_group doesn't allow @repo syntax, so we work around that + # by prefixing external repos with a fake package path. This also + # makes it easy to enable or disable all external repos. + check_label = Label("@//__EXTERNAL_REPOS__/{workspace}/{package}".format( + workspace = ctx.label.workspace_name, + package = ctx.label.package, + )) + allowlist = ctx.attr._native_rules_allowlist + if allowlist: + allowed = ctx.attr._native_rules_allowlist[PackageSpecificationInfo].contains(check_label) + allowlist_help = str(allowlist.label).replace("@//", "//") + else: + allowed = False + allowlist_help = ("no allowlist specified; all disallowed; specify one " + + "with --python_native_rules_allowlist") + if not allowed: + if ctx.attr.generator_function: + generator = "{generator_function}(name={generator_name}) in {generator_location}".format( + generator_function = ctx.attr.generator_function, + generator_name = ctx.attr.generator_name, + generator_location = ctx.attr.generator_location, + ) + else: + generator = "No generator (called directly in BUILD file)" + + msg = ( + "{target} not allowed to use native.{rule}\n" + + "Generated by: {generator}\n" + + "Allowlist: {allowlist}\n" + + "Migrate to using @rules_python, see {help_url}\n" + + "FIXCMD: {fix_cmd} --target={target} --rule={rule} " + + "--generator_name={generator_name} --location={generator_location}" + ) + fail(msg.format( + target = str(ctx.label).replace("@//", "//"), + rule = _py_builtins.get_rule_name(ctx), + generator = generator, + allowlist = allowlist_help, + generator_name = ctx.attr.generator_name, + generator_location = ctx.attr.generator_location, + help_url = NATIVE_RULES_MIGRATION_HELP_URL, + fix_cmd = NATIVE_RULES_MIGRATION_FIX_CMD, + )) diff --git a/python/private/common/common_bazel.bzl b/python/private/common/common_bazel.bzl new file mode 100644 index 0000000000..7277337849 --- /dev/null +++ b/python/private/common/common_bazel.bzl @@ -0,0 +1,109 @@ +# Copyright 2022 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. +"""Common functions that are specific to Bazel rule implementation""" + +load("@bazel_skylib//lib:paths.bzl", "paths") +load(":common.bzl", "is_bool") +load(":providers.bzl", "PyCcLinkParamsProvider") +load(":py_internal.bzl", "py_internal") + +# TODO: Load cc_common from rules_cc +_cc_common = cc_common + +# TODO: Load CcInfo from rules_cc +_CcInfo = CcInfo + +_py_builtins = py_internal + +def collect_cc_info(ctx, extra_deps = []): + """Collect C++ information from dependencies for Bazel. + + Args: + ctx: Rule ctx; must have `deps` attribute. + extra_deps: list of Target to also collect C+ information from. + + Returns: + CcInfo provider of merged information. + """ + deps = ctx.attr.deps + if extra_deps: + deps = list(deps) + deps.extend(extra_deps) + cc_infos = [] + for dep in deps: + if _CcInfo in dep: + cc_infos.append(dep[_CcInfo]) + + if PyCcLinkParamsProvider in dep: + cc_infos.append(dep[PyCcLinkParamsProvider].cc_info) + + return _cc_common.merge_cc_infos(cc_infos = cc_infos) + +def maybe_precompile(ctx, srcs): + """Computes all the outputs (maybe precompiled) from the input srcs. + + See create_binary_semantics_struct for details about this function. + + Args: + ctx: Rule ctx. + srcs: List of Files; the inputs to maybe precompile. + + Returns: + List of Files; the desired output files derived from the input sources. + """ + _ = ctx # @unused + + # Precompilation isn't implemented yet, so just return srcs as-is + return srcs + +def get_imports(ctx): + """Gets the imports from a rule's `imports` attribute. + + See create_binary_semantics_struct for details about this function. + + Args: + ctx: Rule ctx. + + Returns: + List of strings. + """ + prefix = "{}/{}".format( + ctx.workspace_name, + _py_builtins.get_label_repo_runfiles_path(ctx.label), + ) + result = [] + for import_str in ctx.attr.imports: + import_str = ctx.expand_make_variables("imports", import_str, {}) + if import_str.startswith("/"): + continue + + # To prevent "escaping" out of the runfiles tree, we normalize + # the path and ensure it doesn't have up-level references. + import_path = paths.normalize("{}/{}".format(prefix, import_str)) + if import_path.startswith("../") or import_path == "..": + fail("Path '{}' references a path above the execution root".format( + import_str, + )) + result.append(import_path) + return result + +def convert_legacy_create_init_to_int(kwargs): + """Convert "legacy_create_init" key to int, in-place. + + Args: + kwargs: The kwargs to modify. The key "legacy_create_init", if present + and bool, will be converted to its integer value, in place. + """ + if is_bool(kwargs.get("legacy_create_init")): + kwargs["legacy_create_init"] = 1 if kwargs["legacy_create_init"] else 0 diff --git a/python/private/common/providers.bzl b/python/private/common/providers.bzl new file mode 100644 index 0000000000..8a5089d976 --- /dev/null +++ b/python/private/common/providers.bzl @@ -0,0 +1,224 @@ +# Copyright 2022 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. +"""Providers for Python rules.""" + +load("@rules_python_internal//:rules_python_config.bzl", "config") +load(":semantics.bzl", "TOOLS_REPO") + +# TODO: load CcInfo from rules_cc +_CcInfo = CcInfo + +DEFAULT_STUB_SHEBANG = "#!/usr/bin/env python3" + +DEFAULT_BOOTSTRAP_TEMPLATE = "@" + TOOLS_REPO + "//tools/python:python_bootstrap_template.txt" +_PYTHON_VERSION_VALUES = ["PY2", "PY3"] + +# Helper to make the provider definitions not crash under Bazel 5.4: +# Bazel 5.4 doesn't support the `init` arg of `provider()`, so we have to +# not pass that when using Bazel 5.4. But, not passing the `init` arg +# changes the return value from a two-tuple to a single value, which then +# breaks Bazel 6+ code. +# This isn't actually used under Bazel 5.4, so just stub out the values +# to get past the loading phase. +def _define_provider(doc, fields, **kwargs): + if not config.enable_pystar: + return provider("Stub, not used", fields = []), None + return provider(doc = doc, fields = fields, **kwargs) + +def _PyRuntimeInfo_init( + *, + interpreter_path = None, + interpreter = None, + files = None, + coverage_tool = None, + coverage_files = None, + python_version, + stub_shebang = None, + bootstrap_template = None): + if (interpreter_path and interpreter) or (not interpreter_path and not interpreter): + fail("exactly one of interpreter or interpreter_path must be specified") + + if interpreter_path and files != None: + fail("cannot specify 'files' if 'interpreter_path' is given") + + if (coverage_tool and not coverage_files) or (not coverage_tool and coverage_files): + fail( + "coverage_tool and coverage_files must both be set or neither must be set, " + + "got coverage_tool={}, coverage_files={}".format( + coverage_tool, + coverage_files, + ), + ) + + if python_version not in _PYTHON_VERSION_VALUES: + fail("invalid python_version: '{}'; must be one of {}".format( + python_version, + _PYTHON_VERSION_VALUES, + )) + + if files != None and type(files) != type(depset()): + fail("invalid files: got value of type {}, want depset".format(type(files))) + + if interpreter: + if files == None: + files = depset() + else: + files = None + + if coverage_files == None: + coverage_files = depset() + + if not stub_shebang: + stub_shebang = DEFAULT_STUB_SHEBANG + + return { + "bootstrap_template": bootstrap_template, + "coverage_files": coverage_files, + "coverage_tool": coverage_tool, + "files": files, + "interpreter": interpreter, + "interpreter_path": interpreter_path, + "python_version": python_version, + "stub_shebang": stub_shebang, + } + +# TODO(#15897): Rename this to PyRuntimeInfo when we're ready to replace the Java +# implemented provider with the Starlark one. +PyRuntimeInfo, _unused_raw_py_runtime_info_ctor = _define_provider( + doc = """Contains information about a Python runtime, as returned by the `py_runtime` +rule. + +A Python runtime describes either a *platform runtime* or an *in-build runtime*. +A platform runtime accesses a system-installed interpreter at a known path, +whereas an in-build runtime points to a `File` that acts as the interpreter. In +both cases, an "interpreter" is really any executable binary or wrapper script +that is capable of running a Python script passed on the command line, following +the same conventions as the standard CPython interpreter. +""", + init = _PyRuntimeInfo_init, + fields = { + "bootstrap_template": ( + "See py_runtime_rule.bzl%py_runtime.bootstrap_template for docs." + ), + "coverage_files": ( + "The files required at runtime for using `coverage_tool`. " + + "Will be `None` if no `coverage_tool` was provided." + ), + "coverage_tool": ( + "If set, this field is a `File` representing tool used for collecting code coverage information from python tests. Otherwise, this is `None`." + ), + "files": ( + "If this is an in-build runtime, this field is a `depset` of `File`s" + + "that need to be added to the runfiles of an executable target that " + + "uses this runtime (in particular, files needed by `interpreter`). " + + "The value of `interpreter` need not be included in this field. If " + + "this is a platform runtime then this field is `None`." + ), + "interpreter": ( + "If this is an in-build runtime, this field is a `File` representing " + + "the interpreter. Otherwise, this is `None`. Note that an in-build " + + "runtime can use either a prebuilt, checked-in interpreter or an " + + "interpreter built from source." + ), + "interpreter_path": ( + "If this is a platform runtime, this field is the absolute " + + "filesystem path to the interpreter on the target platform. " + + "Otherwise, this is `None`." + ), + "python_version": ( + "Indicates whether this runtime uses Python major version 2 or 3. " + + "Valid values are (only) `\"PY2\"` and " + + "`\"PY3\"`." + ), + "stub_shebang": ( + "\"Shebang\" expression prepended to the bootstrapping Python stub " + + "script used when executing `py_binary` targets. Does not " + + "apply to Windows." + ), + }, +) + +def _check_arg_type(name, required_type, value): + value_type = type(value) + if value_type != required_type: + fail("parameter '{}' got value of type '{}', want '{}'".format( + name, + value_type, + required_type, + )) + +def _PyInfo_init( + *, + transitive_sources, + uses_shared_libraries = False, + imports = depset(), + has_py2_only_sources = False, + has_py3_only_sources = False): + _check_arg_type("transitive_sources", "depset", transitive_sources) + + # Verify it's postorder compatible, but retain is original ordering. + depset(transitive = [transitive_sources], order = "postorder") + + _check_arg_type("uses_shared_libraries", "bool", uses_shared_libraries) + _check_arg_type("imports", "depset", imports) + _check_arg_type("has_py2_only_sources", "bool", has_py2_only_sources) + _check_arg_type("has_py3_only_sources", "bool", has_py3_only_sources) + return { + "has_py2_only_sources": has_py2_only_sources, + "has_py3_only_sources": has_py2_only_sources, + "imports": imports, + "transitive_sources": transitive_sources, + "uses_shared_libraries": uses_shared_libraries, + } + +PyInfo, _unused_raw_py_info_ctor = _define_provider( + doc = "Encapsulates information provided by the Python rules.", + init = _PyInfo_init, + fields = { + "has_py2_only_sources": "Whether any of this target's transitive sources requires a Python 2 runtime.", + "has_py3_only_sources": "Whether any of this target's transitive sources requires a Python 3 runtime.", + "imports": """\ +A depset of import path strings to be added to the `PYTHONPATH` of executable +Python targets. These are accumulated from the transitive `deps`. +The order of the depset is not guaranteed and may be changed in the future. It +is recommended to use `default` order (the default). +""", + "transitive_sources": """\ +A (`postorder`-compatible) depset of `.py` files appearing in the target's +`srcs` and the `srcs` of the target's transitive `deps`. +""", + "uses_shared_libraries": """ +Whether any of this target's transitive `deps` has a shared library file (such +as a `.so` file). + +This field is currently unused in Bazel and may go away in the future. +""", + }, +) + +def _PyCcLinkParamsProvider_init(cc_info): + return { + "cc_info": _CcInfo(linking_context = cc_info.linking_context), + } + +# buildifier: disable=name-conventions +PyCcLinkParamsProvider, _unused_raw_py_cc_link_params_provider_ctor = _define_provider( + doc = ("Python-wrapper to forward CcInfo.linking_context. This is to " + + "allow Python targets to propagate C++ linking information, but " + + "without the Python target appearing to be a valid C++ rule dependency"), + init = _PyCcLinkParamsProvider_init, + fields = { + "cc_info": "A CcInfo instance; it has only linking_context set", + }, +) diff --git a/python/private/common/py_binary_macro_bazel.bzl b/python/private/common/py_binary_macro_bazel.bzl new file mode 100644 index 0000000000..a6c4e97dac --- /dev/null +++ b/python/private/common/py_binary_macro_bazel.bzl @@ -0,0 +1,21 @@ +# Copyright 2022 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. +"""Implementation of macro-half of py_binary rule.""" + +load(":common_bazel.bzl", "convert_legacy_create_init_to_int") +load(":py_binary_rule_bazel.bzl", py_binary_rule = "py_binary") + +def py_binary(**kwargs): + convert_legacy_create_init_to_int(kwargs) + py_binary_rule(**kwargs) diff --git a/python/private/common/py_binary_rule_bazel.bzl b/python/private/common/py_binary_rule_bazel.bzl new file mode 100644 index 0000000000..491d9050da --- /dev/null +++ b/python/private/common/py_binary_rule_bazel.bzl @@ -0,0 +1,49 @@ +# Copyright 2022 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. +"""Rule implementation of py_binary for Bazel.""" + +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load(":attributes.bzl", "AGNOSTIC_BINARY_ATTRS") +load( + ":py_executable_bazel.bzl", + "create_executable_rule", + "py_executable_bazel_impl", +) +load(":semantics.bzl", "TOOLS_REPO") + +_PY_TEST_ATTRS = { + "_collect_cc_coverage": attr.label( + default = "@" + TOOLS_REPO + "//tools/test:collect_cc_coverage", + executable = True, + cfg = "exec", + ), + "_lcov_merger": attr.label( + default = configuration_field(fragment = "coverage", name = "output_generator"), + executable = True, + cfg = "exec", + ), +} + +def _py_binary_impl(ctx): + return py_executable_bazel_impl( + ctx = ctx, + is_test = False, + inherited_environment = [], + ) + +py_binary = create_executable_rule( + implementation = _py_binary_impl, + attrs = dicts.add(AGNOSTIC_BINARY_ATTRS, _PY_TEST_ATTRS), + executable = True, +) diff --git a/python/private/common/py_executable.bzl b/python/private/common/py_executable.bzl new file mode 100644 index 0000000000..bb1f16d61a --- /dev/null +++ b/python/private/common/py_executable.bzl @@ -0,0 +1,861 @@ +# Copyright 2022 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. +"""Common functionality between test/binary executables.""" + +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load( + ":attributes.bzl", + "AGNOSTIC_EXECUTABLE_ATTRS", + "COMMON_ATTRS", + "PY_SRCS_ATTRS", + "SRCS_VERSION_ALL_VALUES", + "create_srcs_attr", + "create_srcs_version_attr", +) +load(":cc_helper.bzl", "cc_helper") +load( + ":common.bzl", + "TOOLCHAIN_TYPE", + "check_native_allowed", + "collect_imports", + "collect_runfiles", + "create_instrumented_files_info", + "create_output_group_info", + "create_py_info", + "csv", + "filter_to_py_srcs", + "target_platform_has_any_constraint", + "union_attrs", +) +load( + ":providers.bzl", + "PyCcLinkParamsProvider", + "PyRuntimeInfo", +) +load(":py_internal.bzl", "py_internal") +load( + ":semantics.bzl", + "ALLOWED_MAIN_EXTENSIONS", + "BUILD_DATA_SYMLINK_PATH", + "IS_BAZEL", + "PLATFORMS_LOCATION", + "PY_RUNTIME_ATTR_NAME", + "TOOLS_REPO", +) + +# TODO: Load cc_common from rules_cc +_cc_common = cc_common + +_py_builtins = py_internal + +# Bazel 5.4 doesn't have config_common.toolchain_type +_CC_TOOLCHAINS = [config_common.toolchain_type( + "@" + TOOLS_REPO + "//tools/cpp:toolchain_type", + mandatory = False, +)] if hasattr(config_common, "toolchain_type") else [] + +# Non-Google-specific attributes for executables +# These attributes are for rules that accept Python sources. +EXECUTABLE_ATTRS = union_attrs( + COMMON_ATTRS, + AGNOSTIC_EXECUTABLE_ATTRS, + PY_SRCS_ATTRS, + { + # TODO(b/203567235): In the Java impl, any file is allowed. While marked + # label, it is more treated as a string, and doesn't have to refer to + # anything that exists because it gets treated as suffix-search string + # over `srcs`. + "main": attr.label( + allow_single_file = True, + doc = """\ +Optional; the name of the source file that is the main entry point of the +application. This file must also be listed in `srcs`. If left unspecified, +`name`, with `.py` appended, is used instead. If `name` does not match any +filename in `srcs`, `main` must be specified. +""", + ), + # TODO(b/203567235): In Google, this attribute is deprecated, and can + # only effectively be PY3. Externally, with Bazel, this attribute has + # a separate story. + "python_version": attr.string( + # TODO(b/203567235): In the Java impl, the default comes from + # --python_version. Not clear what the Starlark equivalent is. + default = "PY3", + # NOTE: Some tests care about the order of these values. + values = ["PY2", "PY3"], + ), + "_windows_constraints": attr.label_list( + default = [ + PLATFORMS_LOCATION + "/os:windows", + ], + ), + }, + create_srcs_version_attr(values = SRCS_VERSION_ALL_VALUES), + create_srcs_attr(mandatory = True), + allow_none = True, +) + +def py_executable_base_impl(ctx, *, semantics, is_test, inherited_environment = []): + """Base rule implementation for a Python executable. + + Google and Bazel call this common base and apply customizations using the + semantics object. + + Args: + ctx: The rule ctx + semantics: BinarySemantics struct; see create_binary_semantics_struct() + is_test: bool, True if the rule is a test rule (has `test=True`), + False if not (has `executable=True`) + inherited_environment: List of str; additional environment variable + names that should be inherited from the runtime environment when the + executable is run. + Returns: + DefaultInfo provider for the executable + """ + _validate_executable(ctx) + + main_py = determine_main(ctx) + direct_sources = filter_to_py_srcs(ctx.files.srcs) + output_sources = semantics.maybe_precompile(ctx, direct_sources) + imports = collect_imports(ctx, semantics) + executable, files_to_build = _compute_outputs(ctx, output_sources) + + runtime_details = _get_runtime_details(ctx, semantics) + if ctx.configuration.coverage_enabled: + extra_deps = semantics.get_coverage_deps(ctx, runtime_details) + else: + extra_deps = [] + + # The debugger dependency should be prevented by select() config elsewhere, + # but just to be safe, also guard against adding it to the output here. + if not _is_tool_config(ctx): + extra_deps.extend(semantics.get_debugger_deps(ctx, runtime_details)) + + cc_details = semantics.get_cc_details_for_binary(ctx, extra_deps = extra_deps) + native_deps_details = _get_native_deps_details( + ctx, + semantics = semantics, + cc_details = cc_details, + is_test = is_test, + ) + runfiles_details = _get_base_runfiles_for_binary( + ctx, + executable = executable, + extra_deps = extra_deps, + files_to_build = files_to_build, + extra_common_runfiles = [ + runtime_details.runfiles, + cc_details.extra_runfiles, + native_deps_details.runfiles, + semantics.get_extra_common_runfiles_for_binary(ctx), + ], + semantics = semantics, + ) + exec_result = semantics.create_executable( + ctx, + executable = executable, + main_py = main_py, + imports = imports, + is_test = is_test, + runtime_details = runtime_details, + cc_details = cc_details, + native_deps_details = native_deps_details, + runfiles_details = runfiles_details, + ) + files_to_build = depset(transitive = [ + exec_result.extra_files_to_build, + files_to_build, + ]) + extra_exec_runfiles = ctx.runfiles(transitive_files = files_to_build) + runfiles_details = struct( + default_runfiles = runfiles_details.default_runfiles.merge(extra_exec_runfiles), + data_runfiles = runfiles_details.data_runfiles.merge(extra_exec_runfiles), + ) + + legacy_providers, modern_providers = _create_providers( + ctx = ctx, + executable = executable, + runfiles_details = runfiles_details, + main_py = main_py, + imports = imports, + direct_sources = direct_sources, + files_to_build = files_to_build, + runtime_details = runtime_details, + cc_info = cc_details.cc_info_for_propagating, + inherited_environment = inherited_environment, + semantics = semantics, + output_groups = exec_result.output_groups, + ) + return struct( + legacy_providers = legacy_providers, + providers = modern_providers, + ) + +def _validate_executable(ctx): + if ctx.attr.python_version != "PY3": + fail("It is not allowed to use Python 2") + check_native_allowed(ctx) + +def _compute_outputs(ctx, output_sources): + if target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints): + executable = ctx.actions.declare_file(ctx.label.name + ".exe") + else: + executable = ctx.actions.declare_file(ctx.label.name) + + # TODO(b/208657718): Remove output_sources from the default outputs + # once the depot is cleaned up. + return executable, depset([executable] + output_sources) + +def _get_runtime_details(ctx, semantics): + """Gets various information about the Python runtime to use. + + While most information comes from the toolchain, various legacy and + compatibility behaviors require computing some other information. + + Args: + ctx: Rule ctx + semantics: A `BinarySemantics` struct; see `create_binary_semantics_struct` + + Returns: + A struct; see inline-field comments of the return value for details. + """ + + # Bazel has --python_path. This flag has a computed default of "python" when + # its actual default is null (see + # BazelPythonConfiguration.java#getPythonPath). This flag is only used if + # toolchains are not enabled and `--python_top` isn't set. Note that Google + # used to have a variant of this named --python_binary, but it has since + # been removed. + # + # TOOD(bazelbuild/bazel#7901): Remove this once --python_path flag is removed. + + if IS_BAZEL: + flag_interpreter_path = ctx.fragments.bazel_py.python_path + toolchain_runtime, effective_runtime = _maybe_get_runtime_from_ctx(ctx) + if not effective_runtime: + # Clear these just in case + toolchain_runtime = None + effective_runtime = None + + else: # Google code path + flag_interpreter_path = None + toolchain_runtime, effective_runtime = _maybe_get_runtime_from_ctx(ctx) + if not effective_runtime: + fail("Unable to find Python runtime") + + if effective_runtime: + direct = [] # List of files + transitive = [] # List of depsets + if effective_runtime.interpreter: + direct.append(effective_runtime.interpreter) + transitive.append(effective_runtime.files) + + if ctx.configuration.coverage_enabled: + if effective_runtime.coverage_tool: + direct.append(effective_runtime.coverage_tool) + if effective_runtime.coverage_files: + transitive.append(effective_runtime.coverage_files) + runtime_files = depset(direct = direct, transitive = transitive) + else: + runtime_files = depset() + + executable_interpreter_path = semantics.get_interpreter_path( + ctx, + runtime = effective_runtime, + flag_interpreter_path = flag_interpreter_path, + ) + + return struct( + # Optional PyRuntimeInfo: The runtime found from toolchain resolution. + # This may be None because, within Google, toolchain resolution isn't + # yet enabled. + toolchain_runtime = toolchain_runtime, + # Optional PyRuntimeInfo: The runtime that should be used. When + # toolchain resolution is enabled, this is the same as + # `toolchain_resolution`. Otherwise, this probably came from the + # `_python_top` attribute that the Google implementation still uses. + # This is separate from `toolchain_runtime` because toolchain_runtime + # is propagated as a provider, while non-toolchain runtimes are not. + effective_runtime = effective_runtime, + # str; Path to the Python interpreter to use for running the executable + # itself (not the bootstrap script). Either an absolute path (which + # means it is platform-specific), or a runfiles-relative path (which + # means the interpreter should be within `runtime_files`) + executable_interpreter_path = executable_interpreter_path, + # runfiles: Additional runfiles specific to the runtime that should + # be included. For in-build runtimes, this shold include the interpreter + # and any supporting files. + runfiles = ctx.runfiles(transitive_files = runtime_files), + ) + +def _maybe_get_runtime_from_ctx(ctx): + """Finds the PyRuntimeInfo from the toolchain or attribute, if available. + + Returns: + 2-tuple of toolchain_runtime, effective_runtime + """ + if ctx.fragments.py.use_toolchains: + toolchain = ctx.toolchains[TOOLCHAIN_TYPE] + + if not hasattr(toolchain, "py3_runtime"): + fail("Python toolchain field 'py3_runtime' is missing") + if not toolchain.py3_runtime: + fail("Python toolchain missing py3_runtime") + py3_runtime = toolchain.py3_runtime + + # Hack around the fact that the autodetecting Python toolchain, which is + # automatically registered, does not yet support Windows. In this case, + # we want to return null so that _get_interpreter_path falls back on + # --python_path. See tools/python/toolchain.bzl. + # TODO(#7844): Remove this hack when the autodetecting toolchain has a + # Windows implementation. + if py3_runtime.interpreter_path == "/_magic_pyruntime_sentinel_do_not_use": + return None, None + + if py3_runtime.python_version != "PY3": + fail("Python toolchain py3_runtime must be python_version=PY3, got {}".format( + py3_runtime.python_version, + )) + toolchain_runtime = toolchain.py3_runtime + effective_runtime = toolchain_runtime + else: + toolchain_runtime = None + attr_target = getattr(ctx.attr, PY_RUNTIME_ATTR_NAME) + + # In Bazel, --python_top is null by default. + if attr_target and PyRuntimeInfo in attr_target: + effective_runtime = attr_target[PyRuntimeInfo] + else: + return None, None + + return toolchain_runtime, effective_runtime + +def _get_base_runfiles_for_binary( + ctx, + *, + executable, + extra_deps, + files_to_build, + extra_common_runfiles, + semantics): + """Returns the set of runfiles necessary prior to executable creation. + + NOTE: The term "common runfiles" refers to the runfiles that both the + default and data runfiles have in common. + + Args: + ctx: The rule ctx. + executable: The main executable output. + extra_deps: List of Targets; additional targets whose runfiles + will be added to the common runfiles. + files_to_build: depset of File of the default outputs to add into runfiles. + extra_common_runfiles: List of runfiles; additional runfiles that + will be added to the common runfiles. + semantics: A `BinarySemantics` struct; see `create_binary_semantics_struct`. + + Returns: + struct with attributes: + * default_runfiles: The default runfiles + * data_runfiles: The data runfiles + """ + common_runfiles = collect_runfiles(ctx, depset( + direct = [executable], + transitive = [files_to_build], + )) + if extra_deps: + common_runfiles = common_runfiles.merge_all([ + t[DefaultInfo].default_runfiles + for t in extra_deps + ]) + common_runfiles = common_runfiles.merge_all(extra_common_runfiles) + + if semantics.should_create_init_files(ctx): + common_runfiles = _py_builtins.merge_runfiles_with_generated_inits_empty_files_supplier( + ctx = ctx, + runfiles = common_runfiles, + ) + + # Don't include build_data.txt in data runfiles. This allows binaries to + # contain other binaries while still using the same fixed location symlink + # for the build_data.txt file. Really, the fixed location symlink should be + # removed and another way found to locate the underlying build data file. + data_runfiles = common_runfiles + + if is_stamping_enabled(ctx, semantics) and semantics.should_include_build_data(ctx): + default_runfiles = common_runfiles.merge(_create_runfiles_with_build_data( + ctx, + semantics.get_central_uncachable_version_file(ctx), + semantics.get_extra_write_build_data_env(ctx), + )) + else: + default_runfiles = common_runfiles + + return struct( + default_runfiles = default_runfiles, + data_runfiles = data_runfiles, + ) + +def _create_runfiles_with_build_data( + ctx, + central_uncachable_version_file, + extra_write_build_data_env): + return ctx.runfiles( + symlinks = { + BUILD_DATA_SYMLINK_PATH: _write_build_data( + ctx, + central_uncachable_version_file, + extra_write_build_data_env, + ), + }, + ) + +def _write_build_data(ctx, central_uncachable_version_file, extra_write_build_data_env): + # TODO: Remove this logic when a central file is always available + if not central_uncachable_version_file: + version_file = ctx.actions.declare_file(ctx.label.name + "-uncachable_version_file.txt") + _py_builtins.copy_without_caching( + ctx = ctx, + read_from = ctx.version_file, + write_to = version_file, + ) + else: + version_file = central_uncachable_version_file + + direct_inputs = [ctx.info_file, version_file] + + # A "constant metadata" file is basically a special file that doesn't + # support change detection logic and reports that it is unchanged. i.e., it + # behaves like ctx.version_file and is ignored when computing "what inputs + # changed" (see https://bazel.build/docs/user-manual#workspace-status). + # + # We do this so that consumers of the final build data file don't have + # to transitively rebuild everything -- the `uncachable_version_file` file + # isn't cachable, which causes the build data action to always re-run. + # + # While this technically means a binary could have stale build info, + # it ends up not mattering in practice because the volatile information + # doesn't meaningfully effect other outputs. + # + # This is also done for performance and Make It work reasons: + # * Passing the transitive dependencies into the action requires passing + # the runfiles, but actions don't directly accept runfiles. While + # flattening the depsets can be deferred, accessing the + # `runfiles.empty_filenames` attribute will will invoke the empty + # file supplier a second time, which is too much of a memory and CPU + # performance hit. + # * Some targets specify a directory in `data`, which is unsound, but + # mostly works. Google's RBE, unfortunately, rejects it. + # * A binary's transitive closure may be so large that it exceeds + # Google RBE limits for action inputs. + build_data = _py_builtins.declare_constant_metadata_file( + ctx = ctx, + name = ctx.label.name + ".build_data.txt", + root = ctx.bin_dir, + ) + + ctx.actions.run( + executable = ctx.executable._build_data_gen, + env = dicts.add({ + # NOTE: ctx.info_file is undocumented; see + # https://github.com/bazelbuild/bazel/issues/9363 + "INFO_FILE": ctx.info_file.path, + "OUTPUT": build_data.path, + "PLATFORM": cc_helper.find_cpp_toolchain(ctx).toolchain_id, + "TARGET": str(ctx.label), + "VERSION_FILE": version_file.path, + }, extra_write_build_data_env), + inputs = depset( + direct = direct_inputs, + ), + outputs = [build_data], + mnemonic = "PyWriteBuildData", + progress_message = "Generating %{label} build_data.txt", + ) + return build_data + +def _get_native_deps_details(ctx, *, semantics, cc_details, is_test): + if not semantics.should_build_native_deps_dso(ctx): + return struct(dso = None, runfiles = ctx.runfiles()) + + cc_info = cc_details.cc_info_for_self_link + + if not cc_info.linking_context.linker_inputs: + return struct(dso = None, runfiles = ctx.runfiles()) + + dso = ctx.actions.declare_file(semantics.get_native_deps_dso_name(ctx)) + share_native_deps = py_internal.share_native_deps(ctx) + cc_feature_config = cc_configure_features( + ctx, + cc_toolchain = cc_details.cc_toolchain, + # See b/171276569#comment18: this feature string is just to allow + # Google's RBE to know the link action is for the Python case so it can + # take special actions (though as of Jun 2022, no special action is + # taken). + extra_features = ["native_deps_link"], + ) + if share_native_deps: + linked_lib = _create_shared_native_deps_dso( + ctx, + cc_info = cc_info, + is_test = is_test, + requested_features = cc_feature_config.requested_features, + feature_configuration = cc_feature_config.feature_configuration, + ) + ctx.actions.symlink( + output = dso, + target_file = linked_lib, + progress_message = "Symlinking shared native deps for %{label}", + ) + else: + linked_lib = dso + _cc_common.link( + name = ctx.label.name, + actions = ctx.actions, + linking_contexts = [cc_info.linking_context], + output_type = "dynamic_library", + never_link = True, + native_deps = True, + feature_configuration = cc_feature_config.feature_configuration, + cc_toolchain = cc_details.cc_toolchain, + test_only_target = is_test, + stamp = 1 if is_stamping_enabled(ctx, semantics) else 0, + main_output = linked_lib, + use_shareable_artifact_factory = True, + # NOTE: Only flags not captured by cc_info.linking_context need to + # be manually passed + user_link_flags = semantics.get_native_deps_user_link_flags(ctx), + ) + return struct( + dso = dso, + runfiles = ctx.runfiles(files = [dso]), + ) + +def _create_shared_native_deps_dso( + ctx, + *, + cc_info, + is_test, + feature_configuration, + requested_features): + linkstamps = cc_info.linking_context.linkstamps() + + partially_disabled_thin_lto = ( + _cc_common.is_enabled( + feature_name = "thin_lto_linkstatic_tests_use_shared_nonlto_backends", + feature_configuration = feature_configuration, + ) and not _cc_common.is_enabled( + feature_name = "thin_lto_all_linkstatic_use_shared_nonlto_backends", + feature_configuration = feature_configuration, + ) + ) + dso_hash = _get_shared_native_deps_hash( + linker_inputs = cc_helper.get_static_mode_params_for_dynamic_library_libraries( + depset([ + lib + for linker_input in cc_info.linking_context.linker_inputs.to_list() + for lib in linker_input.libraries + ]), + ), + link_opts = [ + flag + for input in cc_info.linking_context.linker_inputs.to_list() + for flag in input.user_link_flags + ], + linkstamps = [linkstamp.file() for linkstamp in linkstamps.to_list()], + build_info_artifacts = _cc_common.get_build_info(ctx) if linkstamps else [], + features = requested_features, + is_test_target_partially_disabled_thin_lto = is_test and partially_disabled_thin_lto, + ) + return py_internal.declare_shareable_artifact(ctx, "_nativedeps/%x.so" % dso_hash) + +# This is a minimal version of NativeDepsHelper.getSharedNativeDepsPath, see +# com.google.devtools.build.lib.rules.nativedeps.NativeDepsHelper#getSharedNativeDepsPath +# The basic idea is to take all the inputs that affect linking and encode (via +# hashing) them into the filename. +# TODO(b/234232820): The settings that affect linking must be kept in sync with the actual +# C++ link action. For more information, see the large descriptive comment on +# NativeDepsHelper#getSharedNativeDepsPath. +def _get_shared_native_deps_hash( + *, + linker_inputs, + link_opts, + linkstamps, + build_info_artifacts, + features, + is_test_target_partially_disabled_thin_lto): + # NOTE: We use short_path because the build configuration root in which + # files are always created already captures the configuration-specific + # parts, so no need to include them manually. + parts = [] + for artifact in linker_inputs: + parts.append(artifact.short_path) + parts.append(str(len(link_opts))) + parts.extend(link_opts) + for artifact in linkstamps: + parts.append(artifact.short_path) + for artifact in build_info_artifacts: + parts.append(artifact.short_path) + parts.extend(sorted(features)) + + # Sharing of native dependencies may cause an {@link + # ActionConflictException} when ThinLTO is disabled for test and test-only + # targets that are statically linked, but enabled for other statically + # linked targets. This happens in case the artifacts for the shared native + # dependency are output by {@link Action}s owned by the non-test and test + # targets both. To fix this, we allow creation of multiple artifacts for the + # shared native library - one shared among the test and test-only targets + # where ThinLTO is disabled, and the other shared among other targets where + # ThinLTO is enabled. See b/138118275 + parts.append("1" if is_test_target_partially_disabled_thin_lto else "0") + + return hash("".join(parts)) + +def determine_main(ctx): + """Determine the main entry point .py source file. + + Args: + ctx: The rule ctx. + + Returns: + Artifact; the main file. If one can't be found, an error is raised. + """ + if ctx.attr.main: + proposed_main = ctx.attr.main.label.name + if not proposed_main.endswith(tuple(ALLOWED_MAIN_EXTENSIONS)): + fail("main must end in '.py'") + else: + if ctx.label.name.endswith(".py"): + fail("name must not end in '.py'") + proposed_main = ctx.label.name + ".py" + + main_files = [src for src in ctx.files.srcs if _path_endswith(src.short_path, proposed_main)] + if not main_files: + if ctx.attr.main: + fail("could not find '{}' as specified by 'main' attribute".format(proposed_main)) + else: + fail(("corresponding default '{}' does not appear in srcs. Add " + + "it or override default file name with a 'main' attribute").format( + proposed_main, + )) + + elif len(main_files) > 1: + if ctx.attr.main: + fail(("file name '{}' specified by 'main' attributes matches multiple files. " + + "Matches: {}").format( + proposed_main, + csv([f.short_path for f in main_files]), + )) + else: + fail(("default main file '{}' matches multiple files in srcs. Perhaps specify " + + "an explicit file with 'main' attribute? Matches were: {}").format( + proposed_main, + csv([f.short_path for f in main_files]), + )) + return main_files[0] + +def _path_endswith(path, endswith): + # Use slash to anchor each path to prevent e.g. + # "ab/c.py".endswith("b/c.py") from incorrectly matching. + return ("/" + path).endswith("/" + endswith) + +def is_stamping_enabled(ctx, semantics): + """Tells if stamping is enabled or not. + + Args: + ctx: The rule ctx + semantics: a semantics struct (see create_semantics_struct). + Returns: + bool; True if stamping is enabled, False if not. + """ + if _is_tool_config(ctx): + return False + + stamp = ctx.attr.stamp + if stamp == 1: + return True + elif stamp == 0: + return False + elif stamp == -1: + return semantics.get_stamp_flag(ctx) + else: + fail("Unsupported `stamp` value: {}".format(stamp)) + +def _is_tool_config(ctx): + # NOTE: The is_tool_configuration() function is only usable by builtins. + # See https://github.com/bazelbuild/bazel/issues/14444 for the FR for + # a more public API. Until that's available, py_internal to the rescue. + return py_internal.is_tool_configuration(ctx) + +def _create_providers( + *, + ctx, + executable, + main_py, + direct_sources, + files_to_build, + runfiles_details, + imports, + cc_info, + inherited_environment, + runtime_details, + output_groups, + semantics): + """Creates the providers an executable should return. + + Args: + ctx: The rule ctx. + executable: File; the target's executable file. + main_py: File; the main .py entry point. + direct_sources: list of Files; the direct, raw `.py` sources for the target. + This should only be Python source files. It should not include pyc + files. + files_to_build: depset of Files; the files for DefaultInfo.files + runfiles_details: runfiles that will become the default and data runfiles. + imports: depset of strings; the import paths to propagate + cc_info: optional CcInfo; Linking information to propagate as + PyCcLinkParamsProvider. Note that only the linking information + is propagated, not the whole CcInfo. + inherited_environment: list of strings; Environment variable names + that should be inherited from the environment the executuble + is run within. + runtime_details: struct of runtime information; see _get_runtime_details() + output_groups: dict[str, depset[File]]; used to create OutputGroupInfo + semantics: BinarySemantics struct; see create_binary_semantics() + + Returns: + A two-tuple of: + 1. A dict of legacy providers. + 2. A list of modern providers. + """ + providers = [ + DefaultInfo( + executable = executable, + files = files_to_build, + default_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles( + ctx, + runfiles_details.default_runfiles, + ), + data_runfiles = _py_builtins.make_runfiles_respect_legacy_external_runfiles( + ctx, + runfiles_details.data_runfiles, + ), + ), + create_instrumented_files_info(ctx), + _create_run_environment_info(ctx, inherited_environment), + ] + + # TODO(b/265840007): Make this non-conditional once Google enables + # --incompatible_use_python_toolchains. + if runtime_details.toolchain_runtime: + providers.append(runtime_details.toolchain_runtime) + + # TODO(b/163083591): Remove the PyCcLinkParamsProvider once binaries-in-deps + # are cleaned up. + if cc_info: + providers.append( + PyCcLinkParamsProvider(cc_info = cc_info), + ) + + py_info, deps_transitive_sources = create_py_info( + ctx, + direct_sources = depset(direct_sources), + imports = imports, + ) + + # TODO(b/253059598): Remove support for extra actions; https://github.com/bazelbuild/bazel/issues/16455 + listeners_enabled = _py_builtins.are_action_listeners_enabled(ctx) + if listeners_enabled: + _py_builtins.add_py_extra_pseudo_action( + ctx = ctx, + dependency_transitive_python_sources = deps_transitive_sources, + ) + + providers.append(py_info) + providers.append(create_output_group_info(py_info.transitive_sources, output_groups)) + + extra_legacy_providers, extra_providers = semantics.get_extra_providers( + ctx, + main_py = main_py, + runtime_details = runtime_details, + ) + providers.extend(extra_providers) + return extra_legacy_providers, providers + +def _create_run_environment_info(ctx, inherited_environment): + expanded_env = {} + for key, value in ctx.attr.env.items(): + expanded_env[key] = _py_builtins.expand_location_and_make_variables( + ctx = ctx, + attribute_name = "env[{}]".format(key), + expression = value, + targets = ctx.attr.data, + ) + return RunEnvironmentInfo( + environment = expanded_env, + inherited_environment = inherited_environment, + ) + +def create_base_executable_rule(*, attrs, fragments = [], **kwargs): + """Create a function for defining for Python binary/test targets. + + Args: + attrs: Rule attributes + fragments: List of str; extra config fragments that are required. + **kwargs: Additional args to pass onto `rule()` + + Returns: + A rule function + """ + if "py" not in fragments: + # The list might be frozen, so use concatentation + fragments = fragments + ["py"] + return rule( + # TODO: add ability to remove attrs, i.e. for imports attr + attrs = dicts.add(EXECUTABLE_ATTRS, attrs), + toolchains = [TOOLCHAIN_TYPE] + _CC_TOOLCHAINS, + fragments = fragments, + **kwargs + ) + +def cc_configure_features(ctx, *, cc_toolchain, extra_features): + """Configure C++ features for Python purposes. + + Args: + ctx: Rule ctx + cc_toolchain: The CcToolchain the target is using. + extra_features: list of strings; additional features to request be + enabled. + + Returns: + struct of the feature configuration and all requested features. + """ + requested_features = ["static_linking_mode"] + requested_features.extend(extra_features) + requested_features.extend(ctx.features) + if "legacy_whole_archive" not in ctx.disabled_features: + requested_features.append("legacy_whole_archive") + feature_configuration = _cc_common.configure_features( + ctx = ctx, + cc_toolchain = cc_toolchain, + requested_features = requested_features, + unsupported_features = ctx.disabled_features, + ) + return struct( + feature_configuration = feature_configuration, + requested_features = requested_features, + ) + +only_exposed_for_google_internal_reason = struct( + create_runfiles_with_build_data = _create_runfiles_with_build_data, +) diff --git a/python/private/common/py_executable_bazel.bzl b/python/private/common/py_executable_bazel.bzl new file mode 100644 index 0000000000..97712c5e43 --- /dev/null +++ b/python/private/common/py_executable_bazel.bzl @@ -0,0 +1,484 @@ +# Copyright 2022 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. +"""Implementation for Bazel Python executable.""" + +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load("@bazel_skylib//lib:paths.bzl", "paths") +load(":attributes_bazel.bzl", "IMPORTS_ATTRS") +load( + ":common.bzl", + "create_binary_semantics_struct", + "create_cc_details_struct", + "create_executable_result_struct", + "target_platform_has_any_constraint", + "union_attrs", +) +load(":common_bazel.bzl", "collect_cc_info", "get_imports", "maybe_precompile") +load(":providers.bzl", "DEFAULT_STUB_SHEBANG") +load( + ":py_executable.bzl", + "create_base_executable_rule", + "py_executable_base_impl", +) +load(":py_internal.bzl", "py_internal") +load(":semantics.bzl", "TOOLS_REPO") + +_py_builtins = py_internal +_EXTERNAL_PATH_PREFIX = "external" +_ZIP_RUNFILES_DIRECTORY_NAME = "runfiles" + +BAZEL_EXECUTABLE_ATTRS = union_attrs( + IMPORTS_ATTRS, + { + "legacy_create_init": attr.int( + default = -1, + values = [-1, 0, 1], + doc = """\ +Whether to implicitly create empty `__init__.py` files in the runfiles tree. +These are created in every directory containing Python source code or shared +libraries, and every parent directory of those directories, excluding the repo +root directory. The default, `-1` (auto), means true unless +`--incompatible_default_to_explicit_init_py` is used. If false, the user is +responsible for creating (possibly empty) `__init__.py` files and adding them to +the `srcs` of Python targets as required. + """, + ), + "_bootstrap_template": attr.label( + allow_single_file = True, + default = "@" + TOOLS_REPO + "//tools/python:python_bootstrap_template.txt", + ), + "_launcher": attr.label( + cfg = "target", + default = "@" + TOOLS_REPO + "//tools/launcher:launcher", + executable = True, + ), + "_py_interpreter": attr.label( + # The configuration_field args are validated when called; + # we use the precense of py_internal to indicate this Bazel + # build has that fragment and name. + default = configuration_field( + fragment = "bazel_py", + name = "python_top", + ) if py_internal else None, + ), + # TODO: This appears to be vestigial. It's only added because + # GraphlessQueryTest.testLabelsOperator relies on it to test for + # query behavior of implicit dependencies. + "_py_toolchain_type": attr.label( + default = "@" + TOOLS_REPO + "//tools/python:toolchain_type", + ), + "_windows_launcher_maker": attr.label( + default = "@" + TOOLS_REPO + "//tools/launcher:launcher_maker", + cfg = "exec", + executable = True, + ), + "_zipper": attr.label( + cfg = "exec", + executable = True, + default = "@" + TOOLS_REPO + "//tools/zip:zipper", + ), + }, +) + +def create_executable_rule(*, attrs, **kwargs): + return create_base_executable_rule( + attrs = dicts.add(BAZEL_EXECUTABLE_ATTRS, attrs), + fragments = ["py", "bazel_py"], + **kwargs + ) + +def py_executable_bazel_impl(ctx, *, is_test, inherited_environment): + """Common code for executables for Baze.""" + result = py_executable_base_impl( + ctx = ctx, + semantics = create_binary_semantics_bazel(), + is_test = is_test, + inherited_environment = inherited_environment, + ) + return struct( + providers = result.providers, + **result.legacy_providers + ) + +def create_binary_semantics_bazel(): + return create_binary_semantics_struct( + # keep-sorted start + create_executable = _create_executable, + get_cc_details_for_binary = _get_cc_details_for_binary, + get_central_uncachable_version_file = lambda ctx: None, + get_coverage_deps = _get_coverage_deps, + get_debugger_deps = _get_debugger_deps, + get_extra_common_runfiles_for_binary = lambda ctx: ctx.runfiles(), + get_extra_providers = _get_extra_providers, + get_extra_write_build_data_env = lambda ctx: {}, + get_imports = get_imports, + get_interpreter_path = _get_interpreter_path, + get_native_deps_dso_name = _get_native_deps_dso_name, + get_native_deps_user_link_flags = _get_native_deps_user_link_flags, + get_stamp_flag = _get_stamp_flag, + maybe_precompile = maybe_precompile, + should_build_native_deps_dso = lambda ctx: False, + should_create_init_files = _should_create_init_files, + should_include_build_data = lambda ctx: False, + # keep-sorted end + ) + +def _get_coverage_deps(ctx, runtime_details): + _ = ctx, runtime_details # @unused + return [] + +def _get_debugger_deps(ctx, runtime_details): + _ = ctx, runtime_details # @unused + return [] + +def _get_extra_providers(ctx, main_py, runtime_details): + _ = ctx, main_py, runtime_details # @unused + return {}, [] + +def _get_stamp_flag(ctx): + # NOTE: Undocumented API; private to builtins + return ctx.configuration.stamp_binaries + +def _should_create_init_files(ctx): + if ctx.attr.legacy_create_init == -1: + return not ctx.fragments.py.default_to_explicit_init_py + else: + return bool(ctx.attr.legacy_create_init) + +def _create_executable( + ctx, + *, + executable, + main_py, + imports, + is_test, + runtime_details, + cc_details, + native_deps_details, + runfiles_details): + _ = is_test, cc_details, native_deps_details # @unused + + common_bootstrap_template_kwargs = dict( + main_py = main_py, + imports = imports, + runtime_details = runtime_details, + ) + + is_windows = target_platform_has_any_constraint(ctx, ctx.attr._windows_constraints) + + if is_windows: + if not executable.extension == "exe": + fail("Should not happen: somehow we are generating a non-.exe file on windows") + base_executable_name = executable.basename[0:-4] + else: + base_executable_name = executable.basename + + zip_bootstrap = ctx.actions.declare_file(base_executable_name + ".temp", sibling = executable) + zip_file = ctx.actions.declare_file(base_executable_name + ".zip", sibling = executable) + + _expand_bootstrap_template( + ctx, + output = zip_bootstrap, + is_for_zip = True, + **common_bootstrap_template_kwargs + ) + _create_zip_file( + ctx, + output = zip_file, + original_nonzip_executable = executable, + executable_for_zip_file = zip_bootstrap, + runfiles = runfiles_details.default_runfiles, + ) + + extra_files_to_build = [] + + # NOTE: --build_python_zip defauls to true on Windows + build_zip_enabled = ctx.fragments.py.build_python_zip + + # When --build_python_zip is enabled, then the zip file becomes + # one of the default outputs. + if build_zip_enabled: + extra_files_to_build.append(zip_file) + + # The logic here is a bit convoluted. Essentially, there are 3 types of + # executables produced: + # 1. (non-Windows) A bootstrap template based program. + # 2. (non-Windows) A self-executable zip file of a bootstrap template based program. + # 3. (Windows) A native Windows executable that finds and launches + # the actual underlying Bazel program (one of the above). Note that + # it implicitly assumes one of the above is located next to it, and + # that --build_python_zip defaults to true for Windows. + + should_create_executable_zip = False + bootstrap_output = None + if not is_windows: + if build_zip_enabled: + should_create_executable_zip = True + else: + bootstrap_output = executable + else: + _create_windows_exe_launcher( + ctx, + output = executable, + use_zip_file = build_zip_enabled, + python_binary_path = runtime_details.executable_interpreter_path, + ) + if not build_zip_enabled: + # On Windows, the main executable has an "exe" extension, so + # here we re-use the un-extensioned name for the bootstrap output. + bootstrap_output = ctx.actions.declare_file(base_executable_name) + + # The launcher looks for the non-zip executable next to + # itself, so add it to the default outputs. + extra_files_to_build.append(bootstrap_output) + + if should_create_executable_zip: + if bootstrap_output != None: + fail("Should not occur: bootstrap_output should not be used " + + "when creating an executable zip") + _create_executable_zip_file(ctx, output = executable, zip_file = zip_file) + elif bootstrap_output: + _expand_bootstrap_template( + ctx, + output = bootstrap_output, + is_for_zip = build_zip_enabled, + **common_bootstrap_template_kwargs + ) + else: + # Otherwise, this should be the Windows case of launcher + zip. + # Double check this just to make sure. + if not is_windows or not build_zip_enabled: + fail(("Should not occur: The non-executable-zip and " + + "non-boostrap-template case should have windows and zip " + + "both true, but got " + + "is_windows={is_windows} " + + "build_zip_enabled={build_zip_enabled}").format( + is_windows = is_windows, + build_zip_enabled = build_zip_enabled, + )) + + return create_executable_result_struct( + extra_files_to_build = depset(extra_files_to_build), + output_groups = {"python_zip_file": depset([zip_file])}, + ) + +def _expand_bootstrap_template( + ctx, + *, + output, + main_py, + imports, + is_for_zip, + runtime_details): + runtime = runtime_details.effective_runtime + if (ctx.configuration.coverage_enabled and + runtime and + runtime.coverage_tool): + coverage_tool_runfiles_path = "{}/{}".format( + ctx.workspace_name, + runtime.coverage_tool.short_path, + ) + else: + coverage_tool_runfiles_path = "" + + if runtime: + shebang = runtime.stub_shebang + template = runtime.bootstrap_template + else: + shebang = DEFAULT_STUB_SHEBANG + template = ctx.file._bootstrap_template + + ctx.actions.expand_template( + template = template, + output = output, + substitutions = { + "%coverage_tool%": coverage_tool_runfiles_path, + "%import_all%": "True" if ctx.fragments.bazel_py.python_import_all_repositories else "False", + "%imports%": ":".join(imports.to_list()), + "%is_zipfile%": "True" if is_for_zip else "False", + "%main%": "{}/{}".format( + ctx.workspace_name, + main_py.short_path, + ), + "%python_binary%": runtime_details.executable_interpreter_path, + "%shebang%": shebang, + "%target%": str(ctx.label), + "%workspace_name%": ctx.workspace_name, + }, + is_executable = True, + ) + +def _create_windows_exe_launcher( + ctx, + *, + output, + python_binary_path, + use_zip_file): + launch_info = ctx.actions.args() + launch_info.use_param_file("%s", use_always = True) + launch_info.set_param_file_format("multiline") + launch_info.add("binary_type=Python") + launch_info.add(ctx.workspace_name, format = "workspace_name=%s") + launch_info.add( + "1" if py_internal.runfiles_enabled(ctx) else "0", + format = "symlink_runfiles_enabled=%s", + ) + launch_info.add(python_binary_path, format = "python_bin_path=%s") + launch_info.add("1" if use_zip_file else "0", format = "use_zip_file=%s") + + ctx.actions.run( + executable = ctx.executable._windows_launcher_maker, + arguments = [ctx.executable._launcher.path, launch_info, output.path], + inputs = [ctx.executable._launcher], + outputs = [output], + mnemonic = "PyBuildLauncher", + progress_message = "Creating launcher for %{label}", + # Needed to inherit PATH when using non-MSVC compilers like MinGW + use_default_shell_env = True, + ) + +def _create_zip_file(ctx, *, output, original_nonzip_executable, executable_for_zip_file, runfiles): + workspace_name = ctx.workspace_name + legacy_external_runfiles = _py_builtins.get_legacy_external_runfiles(ctx) + + manifest = ctx.actions.args() + manifest.use_param_file("@%s", use_always = True) + manifest.set_param_file_format("multiline") + + manifest.add("__main__.py={}".format(executable_for_zip_file.path)) + manifest.add("__init__.py=") + manifest.add( + "{}=".format( + _get_zip_runfiles_path("__init__.py", workspace_name, legacy_external_runfiles), + ), + ) + for path in runfiles.empty_filenames.to_list(): + manifest.add("{}=".format(_get_zip_runfiles_path(path, workspace_name, legacy_external_runfiles))) + + def map_zip_runfiles(file): + if file != original_nonzip_executable and file != output: + return "{}={}".format( + _get_zip_runfiles_path(file.short_path, workspace_name, legacy_external_runfiles), + file.path, + ) + else: + return None + + manifest.add_all(runfiles.files, map_each = map_zip_runfiles, allow_closure = True) + + inputs = [executable_for_zip_file] + if _py_builtins.is_bzlmod_enabled(ctx): + zip_repo_mapping_manifest = ctx.actions.declare_file( + output.basename + ".repo_mapping", + sibling = output, + ) + _py_builtins.create_repo_mapping_manifest( + ctx = ctx, + runfiles = runfiles, + output = zip_repo_mapping_manifest, + ) + manifest.add("{}/_repo_mapping={}".format( + _ZIP_RUNFILES_DIRECTORY_NAME, + zip_repo_mapping_manifest.path, + )) + inputs.append(zip_repo_mapping_manifest) + + for artifact in runfiles.files.to_list(): + # Don't include the original executable because it isn't used by the + # zip file, so no need to build it for the action. + # Don't include the zipfile itself because it's an output. + if artifact != original_nonzip_executable and artifact != output: + inputs.append(artifact) + + zip_cli_args = ctx.actions.args() + zip_cli_args.add("cC") + zip_cli_args.add(output) + + ctx.actions.run( + executable = ctx.executable._zipper, + arguments = [zip_cli_args, manifest], + inputs = depset(inputs), + outputs = [output], + use_default_shell_env = True, + mnemonic = "PythonZipper", + progress_message = "Building Python zip: %{label}", + ) + +def _get_zip_runfiles_path(path, workspace_name, legacy_external_runfiles): + if legacy_external_runfiles and path.startswith(_EXTERNAL_PATH_PREFIX): + zip_runfiles_path = paths.relativize(path, _EXTERNAL_PATH_PREFIX) + else: + # NOTE: External runfiles (artifacts in other repos) will have a leading + # path component of "../" so that they refer outside the main workspace + # directory and into the runfiles root. By normalizing, we simplify e.g. + # "workspace/../foo/bar" to simply "foo/bar". + zip_runfiles_path = paths.normalize("{}/{}".format(workspace_name, path)) + return "{}/{}".format(_ZIP_RUNFILES_DIRECTORY_NAME, zip_runfiles_path) + +def _create_executable_zip_file(ctx, *, output, zip_file): + ctx.actions.run_shell( + command = "echo '{shebang}' | cat - {zip} > {output}".format( + shebang = "#!/usr/bin/env python3", + zip = zip_file.path, + output = output.path, + ), + inputs = [zip_file], + outputs = [output], + use_default_shell_env = True, + mnemonic = "BuildBinary", + progress_message = "Build Python zip executable: %{label}", + ) + +def _get_cc_details_for_binary(ctx, extra_deps): + cc_info = collect_cc_info(ctx, extra_deps = extra_deps) + return create_cc_details_struct( + cc_info_for_propagating = cc_info, + cc_info_for_self_link = cc_info, + cc_info_with_extra_link_time_libraries = None, + extra_runfiles = ctx.runfiles(), + # Though the rules require the CcToolchain, it isn't actually used. + cc_toolchain = None, + ) + +def _get_interpreter_path(ctx, *, runtime, flag_interpreter_path): + if runtime: + if runtime.interpreter_path: + interpreter_path = runtime.interpreter_path + else: + interpreter_path = "{}/{}".format( + ctx.workspace_name, + runtime.interpreter.short_path, + ) + + # NOTE: External runfiles (artifacts in other repos) will have a + # leading path component of "../" so that they refer outside the + # main workspace directory and into the runfiles root. By + # normalizing, we simplify e.g. "workspace/../foo/bar" to simply + # "foo/bar" + interpreter_path = paths.normalize(interpreter_path) + + elif flag_interpreter_path: + interpreter_path = flag_interpreter_path + else: + fail("Unable to determine interpreter path") + + return interpreter_path + +def _get_native_deps_dso_name(ctx): + _ = ctx # @unused + fail("Building native deps DSO not supported.") + +def _get_native_deps_user_link_flags(ctx): + _ = ctx # @unused + fail("Building native deps DSO not supported.") diff --git a/python/private/common/py_internal.bzl b/python/private/common/py_internal.bzl new file mode 100644 index 0000000000..429637253f --- /dev/null +++ b/python/private/common/py_internal.bzl @@ -0,0 +1,26 @@ +# 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. +"""PYTHON RULE IMPLEMENTATION ONLY: Do not use outside of the rule implementations and their tests. + +Re-exports the restricted-use py_internal helper under its original name. + +These may change at any time and are closely coupled to the rule implementation. +""" + +# The py_internal global is only available in Bazel 7+, so loading of it +# must go through a repo rule with Bazel version detection logic. +load("@rules_python_internal//:py_internal.bzl", "py_internal_impl") + +# NOTE: This is None prior to Bazel 7, as set by @rules_python_internal +py_internal = py_internal_impl diff --git a/python/private/common/py_library.bzl b/python/private/common/py_library.bzl new file mode 100644 index 0000000000..8d09c51092 --- /dev/null +++ b/python/private/common/py_library.bzl @@ -0,0 +1,101 @@ +# Copyright 2022 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. +"""Implementation of py_library rule.""" + +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load( + ":attributes.bzl", + "COMMON_ATTRS", + "PY_SRCS_ATTRS", + "SRCS_VERSION_ALL_VALUES", + "create_srcs_attr", + "create_srcs_version_attr", +) +load( + ":common.bzl", + "check_native_allowed", + "collect_imports", + "collect_runfiles", + "create_instrumented_files_info", + "create_output_group_info", + "create_py_info", + "filter_to_py_srcs", + "union_attrs", +) +load(":providers.bzl", "PyCcLinkParamsProvider") +load(":py_internal.bzl", "py_internal") + +_py_builtins = py_internal + +LIBRARY_ATTRS = union_attrs( + COMMON_ATTRS, + PY_SRCS_ATTRS, + create_srcs_version_attr(values = SRCS_VERSION_ALL_VALUES), + create_srcs_attr(mandatory = False), +) + +def py_library_impl(ctx, *, semantics): + """Abstract implementation of py_library rule. + + Args: + ctx: The rule ctx + semantics: A `LibrarySemantics` struct; see `create_library_semantics_struct` + + Returns: + A list of modern providers to propagate. + """ + check_native_allowed(ctx) + direct_sources = filter_to_py_srcs(ctx.files.srcs) + output_sources = depset(semantics.maybe_precompile(ctx, direct_sources)) + runfiles = collect_runfiles(ctx = ctx, files = output_sources) + + cc_info = semantics.get_cc_info_for_library(ctx) + py_info, deps_transitive_sources = create_py_info( + ctx, + direct_sources = depset(direct_sources), + imports = collect_imports(ctx, semantics), + ) + + # TODO(b/253059598): Remove support for extra actions; https://github.com/bazelbuild/bazel/issues/16455 + listeners_enabled = _py_builtins.are_action_listeners_enabled(ctx) + if listeners_enabled: + _py_builtins.add_py_extra_pseudo_action( + ctx = ctx, + dependency_transitive_python_sources = deps_transitive_sources, + ) + + return [ + DefaultInfo(files = output_sources, runfiles = runfiles), + py_info, + create_instrumented_files_info(ctx), + PyCcLinkParamsProvider(cc_info = cc_info), + create_output_group_info(py_info.transitive_sources, extra_groups = {}), + ] + +def create_py_library_rule(*, attrs = {}, **kwargs): + """Creates a py_library rule. + + Args: + attrs: dict of rule attributes. + **kwargs: Additional kwargs to pass onto the rule() call. + Returns: + A rule object + """ + return rule( + attrs = dicts.add(LIBRARY_ATTRS, attrs), + # TODO(b/253818097): fragments=py is only necessary so that + # RequiredConfigFragmentsTest passes + fragments = ["py"], + **kwargs + ) diff --git a/python/private/common/py_library_macro_bazel.bzl b/python/private/common/py_library_macro_bazel.bzl new file mode 100644 index 0000000000..b4f51eff1d --- /dev/null +++ b/python/private/common/py_library_macro_bazel.bzl @@ -0,0 +1,19 @@ +# Copyright 2022 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. +"""Implementation of macro-half of py_library rule.""" + +load(":py_library_rule_bazel.bzl", py_library_rule = "py_library") + +def py_library(**kwargs): + py_library_rule(**kwargs) diff --git a/python/private/common/py_library_rule_bazel.bzl b/python/private/common/py_library_rule_bazel.bzl new file mode 100644 index 0000000000..453abcb816 --- /dev/null +++ b/python/private/common/py_library_rule_bazel.bzl @@ -0,0 +1,47 @@ +# Copyright 2022 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. +"""Implementation of py_library for Bazel.""" + +load(":attributes_bazel.bzl", "IMPORTS_ATTRS") +load(":common.bzl", "create_library_semantics_struct", "union_attrs") +load(":common_bazel.bzl", "collect_cc_info", "get_imports", "maybe_precompile") +load( + ":py_library.bzl", + "LIBRARY_ATTRS", + "create_py_library_rule", + bazel_py_library_impl = "py_library_impl", +) + +_BAZEL_LIBRARY_ATTRS = union_attrs( + LIBRARY_ATTRS, + IMPORTS_ATTRS, +) + +def create_library_semantics_bazel(): + return create_library_semantics_struct( + get_imports = get_imports, + maybe_precompile = maybe_precompile, + get_cc_info_for_library = collect_cc_info, + ) + +def _py_library_impl(ctx): + return bazel_py_library_impl( + ctx, + semantics = create_library_semantics_bazel(), + ) + +py_library = create_py_library_rule( + implementation = _py_library_impl, + attrs = _BAZEL_LIBRARY_ATTRS, +) diff --git a/python/private/common/py_runtime_macro.bzl b/python/private/common/py_runtime_macro.bzl new file mode 100644 index 0000000000..7d04388fd6 --- /dev/null +++ b/python/private/common/py_runtime_macro.bzl @@ -0,0 +1,22 @@ +# Copyright 2022 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. +"""Macro to wrap the py_runtime rule.""" + +load(":py_runtime_rule.bzl", py_runtime_rule = "py_runtime") + +# NOTE: The function name is purposefully selected to match the underlying +# rule name so that e.g. 'generator_function' shows as the same name so +# that it is less confusing to users. +def py_runtime(**kwargs): + py_runtime_rule(**kwargs) diff --git a/python/private/common/py_runtime_rule.bzl b/python/private/common/py_runtime_rule.bzl new file mode 100644 index 0000000000..39434042ea --- /dev/null +++ b/python/private/common/py_runtime_rule.bzl @@ -0,0 +1,216 @@ +# Copyright 2022 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. +"""Implementation of py_runtime rule.""" + +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load("@bazel_skylib//lib:paths.bzl", "paths") +load(":attributes.bzl", "NATIVE_RULES_ALLOWLIST_ATTRS") +load(":common.bzl", "check_native_allowed") +load(":providers.bzl", "DEFAULT_BOOTSTRAP_TEMPLATE", "DEFAULT_STUB_SHEBANG", _PyRuntimeInfo = "PyRuntimeInfo") +load(":py_internal.bzl", "py_internal") + +_py_builtins = py_internal + +def _py_runtime_impl(ctx): + check_native_allowed(ctx) + interpreter_path = ctx.attr.interpreter_path or None # Convert empty string to None + interpreter = ctx.file.interpreter + if (interpreter_path and interpreter) or (not interpreter_path and not interpreter): + fail("exactly one of the 'interpreter' or 'interpreter_path' attributes must be specified") + + runtime_files = depset(transitive = [ + t[DefaultInfo].files + for t in ctx.attr.files + ]) + + hermetic = bool(interpreter) + if not hermetic: + if runtime_files: + fail("if 'interpreter_path' is given then 'files' must be empty") + if not paths.is_absolute(interpreter_path): + fail("interpreter_path must be an absolute path") + + if ctx.attr.coverage_tool: + coverage_di = ctx.attr.coverage_tool[DefaultInfo] + + if _py_builtins.is_singleton_depset(coverage_di.files): + coverage_tool = coverage_di.files.to_list()[0] + elif coverage_di.files_to_run and coverage_di.files_to_run.executable: + coverage_tool = coverage_di.files_to_run.executable + else: + fail("coverage_tool must be an executable target or must produce exactly one file.") + + coverage_files = depset(transitive = [ + coverage_di.files, + coverage_di.default_runfiles.files, + ]) + else: + coverage_tool = None + coverage_files = None + + python_version = ctx.attr.python_version + if python_version == "_INTERNAL_SENTINEL": + if ctx.fragments.py.use_toolchains: + fail( + "When using Python toolchains, this attribute must be set explicitly to either 'PY2' " + + "or 'PY3'. See https://github.com/bazelbuild/bazel/issues/7899 for more " + + "information. You can temporarily avoid this error by reverting to the legacy " + + "Python runtime mechanism (`--incompatible_use_python_toolchains=false`).", + ) + else: + python_version = ctx.fragments.py.default_python_version + + # TODO: Uncomment this after --incompatible_python_disable_py2 defaults to true + # if ctx.fragments.py.disable_py2 and python_version == "PY2": + # fail("Using Python 2 is not supported and disabled; see " + + # "https://github.com/bazelbuild/bazel/issues/15684") + + return [ + _PyRuntimeInfo( + interpreter_path = interpreter_path or None, + interpreter = interpreter, + files = runtime_files if hermetic else None, + coverage_tool = coverage_tool, + coverage_files = coverage_files, + python_version = python_version, + stub_shebang = ctx.attr.stub_shebang, + bootstrap_template = ctx.file.bootstrap_template, + ), + DefaultInfo( + files = runtime_files, + runfiles = ctx.runfiles(), + ), + ] + +# Bind to the name "py_runtime" to preserve the kind/rule_class it shows up +# as elsewhere. +py_runtime = rule( + implementation = _py_runtime_impl, + doc = """ +Represents a Python runtime used to execute Python code. + +A `py_runtime` target can represent either a *platform runtime* or an *in-build +runtime*. A platform runtime accesses a system-installed interpreter at a known +path, whereas an in-build runtime points to an executable target that acts as +the interpreter. In both cases, an "interpreter" means any executable binary or +wrapper script that is capable of running a Python script passed on the command +line, following the same conventions as the standard CPython interpreter. + +A platform runtime is by its nature non-hermetic. It imposes a requirement on +the target platform to have an interpreter located at a specific path. An +in-build runtime may or may not be hermetic, depending on whether it points to +a checked-in interpreter or a wrapper script that accesses the system +interpreter. + +# Example + +``` +py_runtime( + name = "python-2.7.12", + files = glob(["python-2.7.12/**"]), + interpreter = "python-2.7.12/bin/python", +) + +py_runtime( + name = "python-3.6.0", + interpreter_path = "/opt/pyenv/versions/3.6.0/bin/python", +) +``` +""", + fragments = ["py"], + attrs = dicts.add(NATIVE_RULES_ALLOWLIST_ATTRS, { + "bootstrap_template": attr.label( + allow_single_file = True, + default = DEFAULT_BOOTSTRAP_TEMPLATE, + doc = """ +The bootstrap script template file to use. Should have %python_binary%, +%workspace_name%, %main%, and %imports%. + +This template, after expansion, becomes the executable file used to start the +process, so it is responsible for initial bootstrapping actions such as finding +the Python interpreter, runfiles, and constructing an environment to run the +intended Python application. + +While this attribute is currently optional, it will become required when the +Python rules are moved out of Bazel itself. + +The exact variable names expanded is an unstable API and is subject to change. +The API will become more stable when the Python rules are moved out of Bazel +itself. + +See @bazel_tools//tools/python:python_bootstrap_template.txt for more variables. +""", + ), + "coverage_tool": attr.label( + allow_files = False, + doc = """ +This is a target to use for collecting code coverage information from `py_binary` +and `py_test` targets. + +If set, the target must either produce a single file or be an executable target. +The path to the single file, or the executable if the target is executable, +determines the entry point for the python coverage tool. The target and its +runfiles will be added to the runfiles when coverage is enabled. + +The entry point for the tool must be loadable by a Python interpreter (e.g. a +`.py` or `.pyc` file). It must accept the command line arguments +of coverage.py (https://coverage.readthedocs.io), at least including +the `run` and `lcov` subcommands. +""", + ), + "files": attr.label_list( + allow_files = True, + doc = """ +For an in-build runtime, this is the set of files comprising this runtime. +These files will be added to the runfiles of Python binaries that use this +runtime. For a platform runtime this attribute must not be set. +""", + ), + "interpreter": attr.label( + allow_single_file = True, + doc = """ +For an in-build runtime, this is the target to invoke as the interpreter. For a +platform runtime this attribute must not be set. +""", + ), + "interpreter_path": attr.string(doc = """ +For a platform runtime, this is the absolute path of a Python interpreter on +the target platform. For an in-build runtime this attribute must not be set. +"""), + "python_version": attr.string( + default = "_INTERNAL_SENTINEL", + values = ["PY2", "PY3", "_INTERNAL_SENTINEL"], + doc = """ +Whether this runtime is for Python major version 2 or 3. Valid values are `"PY2"` +and `"PY3"`. + +The default value is controlled by the `--incompatible_py3_is_default` flag. +However, in the future this attribute will be mandatory and have no default +value. + """, + ), + "stub_shebang": attr.string( + default = DEFAULT_STUB_SHEBANG, + doc = """ +"Shebang" expression prepended to the bootstrapping Python stub script +used when executing `py_binary` targets. + +See https://github.com/bazelbuild/bazel/issues/8685 for +motivation. + +Does not apply to Windows. +""", + ), + }), +) diff --git a/python/private/common/py_test_macro_bazel.bzl b/python/private/common/py_test_macro_bazel.bzl new file mode 100644 index 0000000000..24b78fef96 --- /dev/null +++ b/python/private/common/py_test_macro_bazel.bzl @@ -0,0 +1,21 @@ +# Copyright 2022 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. +"""Implementation of macro-half of py_test rule.""" + +load(":common_bazel.bzl", "convert_legacy_create_init_to_int") +load(":py_test_rule_bazel.bzl", py_test_rule = "py_test") + +def py_test(**kwargs): + convert_legacy_create_init_to_int(kwargs) + py_test_rule(**kwargs) diff --git a/python/private/common/py_test_rule_bazel.bzl b/python/private/common/py_test_rule_bazel.bzl new file mode 100644 index 0000000000..348935edee --- /dev/null +++ b/python/private/common/py_test_rule_bazel.bzl @@ -0,0 +1,56 @@ +# Copyright 2022 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. +"""Rule implementation of py_test for Bazel.""" + +load("@bazel_skylib//lib:dicts.bzl", "dicts") +load(":attributes.bzl", "AGNOSTIC_TEST_ATTRS") +load(":common.bzl", "maybe_add_test_execution_info") +load( + ":py_executable_bazel.bzl", + "create_executable_rule", + "py_executable_bazel_impl", +) +load(":semantics.bzl", "TOOLS_REPO") + +_BAZEL_PY_TEST_ATTRS = { + # This *might* be a magic attribute to help C++ coverage work. There's no + # docs about this; see TestActionBuilder.java + "_collect_cc_coverage": attr.label( + default = "@" + TOOLS_REPO + "//tools/test:collect_cc_coverage", + executable = True, + cfg = "exec", + ), + # This *might* be a magic attribute to help C++ coverage work. There's no + # docs about this; see TestActionBuilder.java + "_lcov_merger": attr.label( + default = configuration_field(fragment = "coverage", name = "output_generator"), + cfg = "exec", + executable = True, + ), +} + +def _py_test_impl(ctx): + providers = py_executable_bazel_impl( + ctx = ctx, + is_test = True, + inherited_environment = ctx.attr.env_inherit, + ) + maybe_add_test_execution_info(providers.providers, ctx) + return providers + +py_test = create_executable_rule( + implementation = _py_test_impl, + attrs = dicts.add(AGNOSTIC_TEST_ATTRS, _BAZEL_PY_TEST_ATTRS), + test = True, +) diff --git a/python/private/common/semantics.bzl b/python/private/common/semantics.bzl new file mode 100644 index 0000000000..487ff303ef --- /dev/null +++ b/python/private/common/semantics.bzl @@ -0,0 +1,34 @@ +# Copyright 2022 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. +"""Contains constants that vary between Bazel and Google-internal""" + +IMPORTS_ATTR_SUPPORTED = True + +TOOLS_REPO = "bazel_tools" +PLATFORMS_LOCATION = "@platforms/" + +SRCS_ATTR_ALLOW_FILES = [".py", ".py3"] + +DEPS_ATTR_ALLOW_RULES = None + +PY_RUNTIME_ATTR_NAME = "_py_interpreter" + +BUILD_DATA_SYMLINK_PATH = None + +IS_BAZEL = True + +NATIVE_RULES_MIGRATION_HELP_URL = "https://github.com/bazelbuild/bazel/issues/17773" +NATIVE_RULES_MIGRATION_FIX_CMD = "add_python_loads" + +ALLOWED_MAIN_EXTENSIONS = [".py"] diff --git a/python/private/full_version.bzl b/python/private/full_version.bzl new file mode 100644 index 0000000000..68c969416e --- /dev/null +++ b/python/private/full_version.bzl @@ -0,0 +1,43 @@ +# 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. + +"""A small helper to ensure that we are working with full versions.""" + +load("//python:versions.bzl", "MINOR_MAPPING") + +def full_version(version): + """Return a full version. + + Args: + version: the version in `X.Y` or `X.Y.Z` format. + + Returns: + a full version given the version string. If the string is already a + major version then we return it as is. + """ + if version in MINOR_MAPPING: + return MINOR_MAPPING[version] + + parts = version.split(".") + if len(parts) == 3: + return version + elif len(parts) == 2: + fail( + "Unknown Python version '{}', available values are: {}".format( + version, + ",".join(MINOR_MAPPING.keys()), + ), + ) + else: + fail("Unknown version format: {}".format(version)) diff --git a/python/private/internal_config_repo.bzl b/python/private/internal_config_repo.bzl new file mode 100644 index 0000000000..cfc7616de9 --- /dev/null +++ b/python/private/internal_config_repo.bzl @@ -0,0 +1,99 @@ +# 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. +"""Repository to generate configuration settings info from the environment. + +This handles settings that can't be encoded as regular build configuration flags, +such as globals available to Bazel versions, or propagating user environment +settings for rules to later use. +""" + +load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") + +_ENABLE_PYSTAR_ENVVAR_NAME = "RULES_PYTHON_ENABLE_PYSTAR" +_ENABLE_PYSTAR_DEFAULT = "0" + +_CONFIG_TEMPLATE = """\ +config = struct( + enable_pystar = {enable_pystar}, +) +""" + +# The py_internal symbol is only accessible from within @rules_python, so we have to +# load it from there and re-export it so that rules_python can later load it. +_PY_INTERNAL_SHIM = """\ +load("@rules_python//tools/build_defs/python/private:py_internal_renamed.bzl", "py_internal_renamed") +py_internal_impl = py_internal_renamed +""" + +ROOT_BUILD_TEMPLATE = """\ +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +package( + default_visibility = [ + "{visibility}", + ] +) + +bzl_library( + name = "rules_python_config_bzl", + srcs = ["rules_python_config.bzl"] +) + +bzl_library( + name = "py_internal_bzl", + srcs = ["py_internal.bzl"], + deps = [{py_internal_dep}], +) +""" + +def _internal_config_repo_impl(rctx): + enable_pystar = _bool_from_environ(rctx, _ENABLE_PYSTAR_ENVVAR_NAME, _ENABLE_PYSTAR_DEFAULT) + rctx.file("rules_python_config.bzl", _CONFIG_TEMPLATE.format( + enable_pystar = enable_pystar, + )) + + if enable_pystar or ( + # Bazel 7+ (dev and later) has native.starlark_doc_extract, and thus the py_internal global + hasattr(native, "starlark_doc_extract") and + # The logic to allow the symbol doesn't work properly under bzlmod, + # even if the symbol is otherwise functional. + not BZLMOD_ENABLED + ): + shim_content = _PY_INTERNAL_SHIM + py_internal_dep = '"@rules_python//tools/build_defs/python/private:py_internal_renamed_bzl"' + else: + shim_content = "py_internal_impl = None\n" + py_internal_dep = "" + + # Bazel 5 doesn't support repository visibility, so just use public + # as a stand-in + if native.bazel_version.startswith("5."): + visibility = "//visibility:public" + else: + visibility = "@rules_python//:__subpackages__" + + rctx.file("BUILD", ROOT_BUILD_TEMPLATE.format( + py_internal_dep = py_internal_dep, + visibility = visibility, + )) + rctx.file("py_internal.bzl", shim_content) + return None + +internal_config_repo = repository_rule( + implementation = _internal_config_repo_impl, + environ = [_ENABLE_PYSTAR_ENVVAR_NAME], +) + +def _bool_from_environ(rctx, key, default): + return bool(int(rctx.os.environ.get(key, default))) diff --git a/python/private/py_console_script_binary.bzl b/python/private/py_console_script_binary.bzl new file mode 100644 index 0000000000..bd992a8f75 --- /dev/null +++ b/python/private/py_console_script_binary.bzl @@ -0,0 +1,87 @@ +# 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. + +""" +Implementation for the macro to generate a console_script py_binary from an 'entry_points.txt' config. +""" + +load("//python:py_binary.bzl", "py_binary") +load(":py_console_script_gen.bzl", "py_console_script_gen") + +def _dist_info(pkg): + """Return the first candidate for the dist_info target label. + + We cannot do `Label(pkg)` here because the string will be evaluated within + the context of the rules_python repo_mapping and it will fail because + rules_python does not know anything about the hub repos that the user has + available. + + NOTE: Works with `incompatible_generate_aliases` and without by assuming the + following formats: + * @pypi_pylint//:pkg + * @pypi//pylint + * @pypi//pylint:pkg + * Label("@pypi//pylint:pkg") + """ + + # str() is called to convert Label objects + return str(pkg).replace(":pkg", "") + ":dist_info" + +def py_console_script_binary( + *, + name, + pkg, + entry_points_txt = None, + script = None, + binary_rule = py_binary, + **kwargs): + """Generate a py_binary for a console_script entry_point. + + Args: + name: str, The name of the resulting target. + pkg: target, the package for which to generate the script. + entry_points_txt: optional target, the entry_points.txt file to parse + for available console_script values. It may be a single file, or a + group of files, but must contain a file named `entry_points.txt`. + If not specified, defaults to the `dist_info` target in the same + package as the `pkg` Label. + script: str, The console script name that the py_binary is going to be + generated for. Defaults to the normalized name attribute. + binary_rule: callable, The rule/macro to use to instantiate + the target. It's expected to behave like `py_binary`. + Defaults to @rules_python//python:py_binary.bzl#py_binary. + **kwargs: Extra parameters forwarded to binary_rule. + """ + main = "rules_python_entry_point_{}.py".format(name) + + if kwargs.pop("srcs", None): + fail("passing 'srcs' attribute to py_console_script_binary is unsupported") + + py_console_script_gen( + name = "_{}_gen".format(name), + # NOTE @aignas 2023-08-05: Works with `incompatible_generate_aliases` and without. + entry_points_txt = entry_points_txt or _dist_info(pkg), + out = main, + console_script = script, + console_script_guess = name, + visibility = ["//visibility:private"], + ) + + binary_rule( + name = name, + srcs = [main], + main = main, + deps = [pkg] + kwargs.pop("deps", []), + **kwargs + ) diff --git a/python/private/py_console_script_gen.bzl b/python/private/py_console_script_gen.bzl new file mode 100644 index 0000000000..7dd4dd2dad --- /dev/null +++ b/python/private/py_console_script_gen.bzl @@ -0,0 +1,93 @@ +# 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. + +""" +A private rule to generate an entry_point python file to be used in a py_binary. + +Right now it only supports console_scripts via the entry_points.txt file in the dist-info. + +NOTE @aignas 2023-08-07: This cannot be in pure starlark, because we need to +read a file and then create a `.py` file based on the contents of that file, +which cannot be done in pure starlark according to +https://github.com/bazelbuild/bazel/issues/14744 +""" + +_ENTRY_POINTS_TXT = "entry_points.txt" + +def _get_entry_points_txt(entry_points_txt): + """Get the entry_points.txt file + + TODO: use map_each to avoid flattening of the directories outside the execution phase. + """ + for file in entry_points_txt.files.to_list(): + if file.basename == _ENTRY_POINTS_TXT: + return file + + fail("{} does not contain {}".format(entry_points_txt, _ENTRY_POINTS_TXT)) + +def _py_console_script_gen_impl(ctx): + entry_points_txt = _get_entry_points_txt(ctx.attr.entry_points_txt) + + args = ctx.actions.args() + args.add("--console-script", ctx.attr.console_script) + args.add("--console-script-guess", ctx.attr.console_script_guess) + args.add(entry_points_txt) + args.add(ctx.outputs.out) + + ctx.actions.run( + inputs = [ + entry_points_txt, + ], + outputs = [ctx.outputs.out], + arguments = [args], + mnemonic = "PyConsoleScriptBinaryGen", + progress_message = "Generating py_console_script_binary main: %{label}", + executable = ctx.executable._tool, + ) + + return [DefaultInfo( + files = depset([ctx.outputs.out]), + )] + +py_console_script_gen = rule( + _py_console_script_gen_impl, + attrs = { + "console_script": attr.string( + doc = "The name of the console_script to create the .py file for. Optional if there is only a single entry-point available.", + default = "", + mandatory = False, + ), + "console_script_guess": attr.string( + doc = "The string used for guessing the console_script if it is not provided.", + default = "", + mandatory = False, + ), + "entry_points_txt": attr.label( + doc = "The filegroup to search for entry_points.txt.", + mandatory = True, + ), + "out": attr.output( + doc = "Output file location.", + mandatory = True, + ), + "_tool": attr.label( + default = ":py_console_script_gen_py", + executable = True, + cfg = "exec", + ), + }, + doc = """\ +Builds an entry_point script from an entry_points.txt file. +""", +) diff --git a/python/private/py_console_script_gen.py b/python/private/py_console_script_gen.py new file mode 100644 index 0000000000..30e93c2e5b --- /dev/null +++ b/python/private/py_console_script_gen.py @@ -0,0 +1,180 @@ +# 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. + +""" +console_script generator from entry_points.txt contents. + +For Python versions earlier than 3.11 and for earlier bazel versions than 7.0 we need to workaround the issue of +sys.path[0] breaking out of the runfiles tree see the following for more context: +* https://github.com/bazelbuild/rules_python/issues/382 +* https://github.com/bazelbuild/bazel/pull/15701 + +In affected bazel and Python versions we see in programs such as `flake8`, `pylint` or `pytest` errors because the +first `sys.path` element is outside the `runfiles` directory and if the `name` of the `py_binary` is the same as +the program name, then the script (e.g. `flake8`) will start failing whilst trying to import its own internals from +the bazel entrypoint script. + +The mitigation strategy is to remove the first entry in the `sys.path` if it does not have `.runfiles` and it seems +to fix the behaviour of console_scripts under `bazel run`. + +This would not happen if we created an console_script binary in the root of an external repository, e.g. +`@pypi_pylint//` because the path for the external repository is already in the runfiles directory. +""" + +from __future__ import annotations + +import argparse +import configparser +import pathlib +import re +import sys +import textwrap + +_ENTRY_POINTS_TXT = "entry_points.txt" + +_TEMPLATE = """\ +import sys + +# See @rules_python//python/private:py_console_script_gen.py for explanation +if getattr(sys.flags, "safe_path", False): + # We are running on Python 3.11 and we don't need this workaround + pass +elif ".runfiles" not in sys.path[0]: + sys.path = sys.path[1:] + +try: + from {module} import {attr} +except ImportError: + entries = "\\n".join(sys.path) + print("Printing sys.path entries for easier debugging:", file=sys.stderr) + print(f"sys.path is:\\n{{entries}}", file=sys.stderr) + raise + +if __name__ == "__main__": + sys.exit({entry_point}()) +""" + + +class EntryPointsParser(configparser.ConfigParser): + """A class handling entry_points.txt + + See https://packaging.python.org/en/latest/specifications/entry-points/ + """ + + optionxform = staticmethod(str) + + +def _guess_entry_point(guess: str, console_scripts: dict[string, string]) -> str | None: + for key, candidate in console_scripts.items(): + if guess == key: + return candidate + + +def run( + *, + entry_points: pathlib.Path, + out: pathlib.Path, + console_script: str, + console_script_guess: str, +): + """Run the generator + + Args: + entry_points: The entry_points.txt file to be parsed. + out: The output file. + console_script: The console_script entry in the entry_points.txt file. + """ + config = EntryPointsParser() + config.read(entry_points) + + try: + console_scripts = dict(config["console_scripts"]) + except KeyError: + raise RuntimeError( + f"The package does not provide any console_scripts in it's {_ENTRY_POINTS_TXT}" + ) + + if console_script: + try: + entry_point = console_scripts[console_script] + except KeyError: + available = ", ".join(sorted(console_scripts.keys())) + raise RuntimeError( + f"The console_script '{console_script}' was not found, only the following are available: {available}" + ) from None + else: + # Get rid of the extension and the common prefix + entry_point = _guess_entry_point( + guess=console_script_guess, + console_scripts=console_scripts, + ) + + if not entry_point: + available = ", ".join(sorted(console_scripts.keys())) + raise RuntimeError( + f"Tried to guess that you wanted '{console_script_guess}', but could not find it. " + f"Please select one of the following console scripts: {available}" + ) from None + + module, _, entry_point = entry_point.rpartition(":") + attr, _, _ = entry_point.partition(".") + # TODO: handle 'extras' in entry_point generation + # See https://github.com/bazelbuild/rules_python/issues/1383 + # See https://packaging.python.org/en/latest/specifications/entry-points/ + + with open(out, "w") as f: + f.write( + _TEMPLATE.format( + module=module, + attr=attr, + entry_point=entry_point, + ), + ) + + +def main(): + parser = argparse.ArgumentParser(description="console_script generator") + parser.add_argument( + "--console-script", + help="The console_script to generate the entry_point template for.", + ) + parser.add_argument( + "--console-script-guess", + required=True, + help="The string used for guessing the console_script if it is not provided.", + ) + parser.add_argument( + "entry_points", + metavar="ENTRY_POINTS_TXT", + type=pathlib.Path, + help="The entry_points.txt within the dist-info of a PyPI wheel", + ) + parser.add_argument( + "out", + type=pathlib.Path, + metavar="OUT", + help="The output file.", + ) + args = parser.parse_args() + + run( + entry_points=args.entry_points, + out=args.out, + console_script=args.console_script, + console_script_guess=args.console_script_guess, + ) + + +if __name__ == "__main__": + main() diff --git a/python/private/py_runtime_pair_macro.bzl b/python/private/py_runtime_pair_macro.bzl new file mode 100644 index 0000000000..3cc359968e --- /dev/null +++ b/python/private/py_runtime_pair_macro.bzl @@ -0,0 +1,27 @@ +# 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. + +"""Implementation of py_runtime_pair macro portion.""" + +load(":py_runtime_pair_rule.bzl", _py_runtime_pair = "py_runtime_pair") + +# A fronting macro is used because macros have user-observable behavior; +# using one from the onset avoids introducing those changes in the future. +def py_runtime_pair(**kwargs): + """Creates a py_runtime_pair target. + + Args: + **kwargs: Keyword args to pass onto underlying rule. + """ + _py_runtime_pair(**kwargs) diff --git a/python/private/py_runtime_pair_rule.bzl b/python/private/py_runtime_pair_rule.bzl new file mode 100644 index 0000000000..d0d8c5b5ee --- /dev/null +++ b/python/private/py_runtime_pair_rule.bzl @@ -0,0 +1,138 @@ +# Copyright 2019 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. + +"""Implementation of py_runtime_pair.""" + +load("//python:py_runtime_info.bzl", "PyRuntimeInfo") + +def _py_runtime_pair_impl(ctx): + if ctx.attr.py2_runtime != None: + py2_runtime = ctx.attr.py2_runtime[PyRuntimeInfo] + if py2_runtime.python_version != "PY2": + fail("The Python runtime in the 'py2_runtime' attribute did not have " + + "version 'PY2'") + else: + py2_runtime = None + + if ctx.attr.py3_runtime != None: + py3_runtime = ctx.attr.py3_runtime[PyRuntimeInfo] + if py3_runtime.python_version != "PY3": + fail("The Python runtime in the 'py3_runtime' attribute did not have " + + "version 'PY3'") + else: + py3_runtime = None + + # TODO: Uncomment this after --incompatible_python_disable_py2 defaults to true + # if _is_py2_disabled(ctx) and py2_runtime != None: + # fail("Using Python 2 is not supported and disabled; see " + + # "https://github.com/bazelbuild/bazel/issues/15684") + + return [platform_common.ToolchainInfo( + py2_runtime = py2_runtime, + py3_runtime = py3_runtime, + )] + +# buildifier: disable=unused-variable +def _is_py2_disabled(ctx): + # Because this file isn't bundled with Bazel, so we have to conditionally + # check for this flag. + # TODO: Remove this once all supported Balze versions have this flag. + if not hasattr(ctx.fragments.py, "disable_py"): + return False + return ctx.fragments.py.disable_py2 + +py_runtime_pair = rule( + implementation = _py_runtime_pair_impl, + attrs = { + # The two runtimes are used by the py_binary at runtime, and so need to + # be built for the target platform. + "py2_runtime": attr.label( + providers = [PyRuntimeInfo], + cfg = "target", + doc = """\ +The runtime to use for Python 2 targets. Must have `python_version` set to +`PY2`. +""", + ), + "py3_runtime": attr.label( + providers = [PyRuntimeInfo], + cfg = "target", + doc = """\ +The runtime to use for Python 3 targets. Must have `python_version` set to +`PY3`. +""", + ), + }, + fragments = ["py"], + doc = """\ +A toolchain rule for Python. + +This wraps up to two Python runtimes, one for Python 2 and one for Python 3. +The rule consuming this toolchain will choose which runtime is appropriate. +Either runtime may be omitted, in which case the resulting toolchain will be +unusable for building Python code using that version. + +Usually the wrapped runtimes are declared using the `py_runtime` rule, but any +rule returning a `PyRuntimeInfo` provider may be used. + +This rule returns a `platform_common.ToolchainInfo` provider with the following +schema: + +```python +platform_common.ToolchainInfo( + py2_runtime = , + py3_runtime = , +) +``` + +Example usage: + +```python +# In your BUILD file... + +load("@rules_python//python:defs.bzl", "py_runtime_pair") + +py_runtime( + name = "my_py2_runtime", + interpreter_path = "/system/python2", + python_version = "PY2", +) + +py_runtime( + name = "my_py3_runtime", + interpreter_path = "/system/python3", + python_version = "PY3", +) + +py_runtime_pair( + name = "my_py_runtime_pair", + py2_runtime = ":my_py2_runtime", + py3_runtime = ":my_py3_runtime", +) + +toolchain( + name = "my_toolchain", + target_compatible_with = <...>, + toolchain = ":my_py_runtime_pair", + toolchain_type = "@rules_python//python:toolchain_type", +) +``` + +```python +# In your WORKSPACE... + +register_toolchains("//my_pkg:my_toolchain") +``` +""", +) diff --git a/python/private/py_wheel.bzl b/python/private/py_wheel.bzl index d8bceabcb8..4152e08c18 100644 --- a/python/private/py_wheel.bzl +++ b/python/private/py_wheel.bzl @@ -16,6 +16,7 @@ load("//python/private:stamp.bzl", "is_stamping_enabled") load(":py_package.bzl", "py_package_lib") +load(":py_wheel_normalize_pep440.bzl", "normalize_pep440") PyWheelInfo = provider( doc = "Information about a wheel produced by `py_wheel`", @@ -117,6 +118,29 @@ See [`py_wheel_dist`](/docs/packaging.md#py_wheel_dist) for more info. ), } +_feature_flags = { + "incompatible_normalize_name": attr.bool( + default = False, + doc = """\ +Normalize the package distribution name according to latest +Python packaging standards. + +See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode +and https://packaging.python.org/en/latest/specifications/name-normalization/. + +Apart from the valid names according to the above, we also accept +'{' and '}', which may be used as placeholders for stamping. +""", + ), + "incompatible_normalize_version": attr.bool( + default = False, + doc = "Normalize the package version according to PEP440 standard. " + + "With this option set to True, if the user wants to pass any " + + "stamp variables, they have to be enclosed in '{}', e.g. " + + "'{BUILD_TIMESTAMP}'.", + ), +} + _requirement_attrs = { "extra_requires": attr.string_list_dict( doc = "List of optional requirements for this package", @@ -203,6 +227,42 @@ _DESCRIPTION_FILE_EXTENSION_TO_TYPE = { } _DEFAULT_DESCRIPTION_FILE_TYPE = "text/plain" +def _escape_filename_distribution_name(name): + """Escape the distribution name component of a filename. + + See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode + and https://packaging.python.org/en/latest/specifications/name-normalization/. + + Apart from the valid names according to the above, we also accept + '{' and '}', which may be used as placeholders for stamping. + """ + escaped = "" + for character in name.elems(): + if character.isalnum() or character in ["{", "}"]: + escaped += character.lower() + elif character in ["-", "_", "."]: + if escaped == "": + fail( + "A valid name must start with a letter or number.", + "Name '%s' does not." % name, + ) + elif escaped.endswith("_"): + pass + else: + escaped += "_" + else: + fail( + "A valid name consists only of ASCII letters ", + "and numbers, period, underscore and hyphen.", + "Name '%s' has bad character '%s'." % (name, character), + ) + if escaped.endswith("_"): + fail( + "A valid name must end with a letter or number.", + "Name '%s' does not." % name, + ) + return escaped + def _escape_filename_segment(segment): """Escape a segment of the wheel filename. @@ -237,13 +297,25 @@ def _py_wheel_impl(ctx): python_tag = _replace_make_variables(ctx.attr.python_tag, ctx) version = _replace_make_variables(ctx.attr.version, ctx) - outfile = ctx.actions.declare_file("-".join([ - _escape_filename_segment(ctx.attr.distribution), - _escape_filename_segment(version), + filename_segments = [] + + if ctx.attr.incompatible_normalize_name: + filename_segments.append(_escape_filename_distribution_name(ctx.attr.distribution)) + else: + filename_segments.append(_escape_filename_segment(ctx.attr.distribution)) + + if ctx.attr.incompatible_normalize_version: + filename_segments.append(normalize_pep440(version)) + else: + filename_segments.append(_escape_filename_segment(version)) + + filename_segments.extend([ _escape_filename_segment(python_tag), _escape_filename_segment(abi), _escape_filename_segment(ctx.attr.platform), - ]) + ".whl") + ]) + + outfile = ctx.actions.declare_file("-".join(filename_segments) + ".whl") name_file = ctx.actions.declare_file(ctx.label.name + ".name") @@ -272,6 +344,10 @@ def _py_wheel_impl(ctx): args.add("--out", outfile) args.add("--name_file", name_file) args.add_all(ctx.attr.strip_path_prefixes, format_each = "--strip_path_prefix=%s") + if ctx.attr.incompatible_normalize_name: + args.add("--incompatible_normalize_name") + if ctx.attr.incompatible_normalize_version: + args.add("--incompatible_normalize_version") # Pass workspace status files if stamping is enabled if is_stamping_enabled(ctx.attr): @@ -423,6 +499,7 @@ tries to locate `.runfiles` directory which is not packaged in the wheel. ), }, _distribution_attrs, + _feature_flags, _requirement_attrs, _entrypoint_attrs, _other_attrs, diff --git a/python/private/py_wheel_normalize_pep440.bzl b/python/private/py_wheel_normalize_pep440.bzl new file mode 100644 index 0000000000..9566348987 --- /dev/null +++ b/python/private/py_wheel_normalize_pep440.bzl @@ -0,0 +1,519 @@ +# 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. + +"Implementation of PEP440 version string normalization" + +def mkmethod(self, method): + """Bind a struct as the first arg to a function. + + This is loosely equivalent to creating a bound method of a class. + """ + return lambda *args, **kwargs: method(self, *args, **kwargs) + +def _isdigit(token): + return token.isdigit() + +def _isalnum(token): + return token.isalnum() + +def _lower(token): + # PEP 440: Case sensitivity + return token.lower() + +def _is(reference): + """Predicate testing a token for equality with `reference`.""" + return lambda token: token == reference + +def _is_not(reference): + """Predicate testing a token for inequality with `reference`.""" + return lambda token: token != reference + +def _in(reference): + """Predicate testing if a token is in the list `reference`.""" + return lambda token: token in reference + +def _ctx(start): + return {"norm": "", "start": start} + +def _open_context(self): + """Open an new parsing ctx. + + If the current parsing step succeeds, call self.accept(). + If the current parsing step fails, call self.discard() to + go back to how it was before we opened a new ctx. + + Args: + self: The normalizer. + """ + self.contexts.append(_ctx(_context(self)["start"])) + return self.contexts[-1] + +def _accept(self): + """Close the current ctx successfully and merge the results.""" + finished = self.contexts.pop() + self.contexts[-1]["norm"] += finished["norm"] + self.contexts[-1]["start"] = finished["start"] + return True + +def _context(self): + return self.contexts[-1] + +def _discard(self): + self.contexts.pop() + return False + +def _new(input): + """Create a new normalizer""" + self = struct( + input = input, + contexts = [_ctx(0)], + ) + + public = struct( + # methods: keep sorted + accept = mkmethod(self, _accept), + context = mkmethod(self, _context), + discard = mkmethod(self, _discard), + open_context = mkmethod(self, _open_context), + + # attributes: keep sorted + input = self.input, + ) + return public + +def accept(parser, predicate, value): + """If `predicate` matches the next token, accept the token. + + Accepting the token means adding it (according to `value`) to + the running results maintained in ctx["norm"] and + advancing the cursor in ctx["start"] to the next token in + `version`. + + Args: + parser: The normalizer. + predicate: function taking a token and returning a boolean + saying if we want to accept the token. + value: the string to add if there's a match, or, if `value` + is a function, the function to apply to the current token + to get the string to add. + + Returns: + whether a token was accepted. + """ + + ctx = parser.context() + + if ctx["start"] >= len(parser.input): + return False + + token = parser.input[ctx["start"]] + + if predicate(token): + if type(value) in ["function", "builtin_function_or_method"]: + value = value(token) + + ctx["norm"] += value + ctx["start"] += 1 + return True + + return False + +def accept_placeholder(parser): + """Accept a Bazel placeholder. + + Placeholders aren't actually part of PEP 440, but are used for + stamping purposes. A placeholder might be + ``{BUILD_TIMESTAMP}``, for instance. We'll accept these as + they are, assuming they will expand to something that makes + sense where they appear. Before the stamping has happened, a + resulting wheel file name containing a placeholder will not + actually be valid. + + Args: + parser: The normalizer. + + Returns: + whether a placeholder was accepted. + """ + ctx = parser.open_context() + + if not accept(parser, _is("{"), str): + return parser.discard() + + start = ctx["start"] + for _ in range(start, len(parser.input) + 1): + if not accept(parser, _is_not("}"), str): + break + + if not accept(parser, _is("}"), str): + return parser.discard() + + return parser.accept() + +def accept_digits(parser): + """Accept multiple digits (or placeholders). + + Args: + parser: The normalizer. + + Returns: + whether some digits (or placeholders) were accepted. + """ + + ctx = parser.open_context() + start = ctx["start"] + + for i in range(start, len(parser.input) + 1): + if not accept(parser, _isdigit, str) and not accept_placeholder(parser): + if i - start >= 1: + if ctx["norm"].isdigit(): + # PEP 440: Integer Normalization + ctx["norm"] = str(int(ctx["norm"])) + return parser.accept() + break + + return parser.discard() + +def accept_string(parser, string, replacement): + """Accept a `string` in the input. Output `replacement`. + + Args: + parser: The normalizer. + string: The string to search for in the parser input. + replacement: The normalized string to use if the string was found. + + Returns: + whether the string was accepted. + """ + ctx = parser.open_context() + + for character in string.elems(): + if not accept(parser, _in([character, character.upper()]), ""): + return parser.discard() + + ctx["norm"] = replacement + + return parser.accept() + +def accept_alnum(parser): + """Accept an alphanumeric sequence. + + Args: + parser: The normalizer. + + Returns: + whether an alphanumeric sequence was accepted. + """ + + ctx = parser.open_context() + start = ctx["start"] + + for i in range(start, len(parser.input) + 1): + if not accept(parser, _isalnum, _lower) and not accept_placeholder(parser): + if i - start >= 1: + return parser.accept() + break + + return parser.discard() + +def accept_dot_number(parser): + """Accept a dot followed by digits. + + Args: + parser: The normalizer. + + Returns: + whether a dot+digits pair was accepted. + """ + parser.open_context() + + if accept(parser, _is("."), ".") and accept_digits(parser): + return parser.accept() + else: + return parser.discard() + +def accept_dot_number_sequence(parser): + """Accept a sequence of dot+digits. + + Args: + parser: The normalizer. + + Returns: + whether a sequence of dot+digits pairs was accepted. + """ + ctx = parser.context() + start = ctx["start"] + i = start + + for i in range(start, len(parser.input) + 1): + if not accept_dot_number(parser): + break + return i - start >= 1 + +def accept_separator_alnum(parser): + """Accept a separator followed by an alphanumeric string. + + Args: + parser: The normalizer. + + Returns: + whether a separator and an alphanumeric string were accepted. + """ + parser.open_context() + + # PEP 440: Local version segments + if ( + accept(parser, _in([".", "-", "_"]), ".") and + (accept_digits(parser) or accept_alnum(parser)) + ): + return parser.accept() + + return parser.discard() + +def accept_separator_alnum_sequence(parser): + """Accept a sequence of separator+alphanumeric. + + Args: + parser: The normalizer. + + Returns: + whether a sequence of separator+alphanumerics was accepted. + """ + ctx = parser.context() + start = ctx["start"] + i = start + + for i in range(start, len(parser.input) + 1): + if not accept_separator_alnum(parser): + break + + return i - start >= 1 + +def accept_epoch(parser): + """PEP 440: Version epochs. + + Args: + parser: The normalizer. + + Returns: + whether a PEP 440 epoch identifier was accepted. + """ + ctx = parser.open_context() + if accept_digits(parser) and accept(parser, _is("!"), "!"): + if ctx["norm"] == "0!": + ctx["norm"] = "" + return parser.accept() + else: + return parser.discard() + +def accept_release(parser): + """Accept the release segment, numbers separated by dots. + + Args: + parser: The normalizer. + + Returns: + whether a release segment was accepted. + """ + parser.open_context() + + if not accept_digits(parser): + return parser.discard() + + accept_dot_number_sequence(parser) + return parser.accept() + +def accept_pre_l(parser): + """PEP 440: Pre-release spelling. + + Args: + parser: The normalizer. + + Returns: + whether a prerelease keyword was accepted. + """ + parser.open_context() + + if ( + accept_string(parser, "alpha", "a") or + accept_string(parser, "a", "a") or + accept_string(parser, "beta", "b") or + accept_string(parser, "b", "b") or + accept_string(parser, "c", "rc") or + accept_string(parser, "preview", "rc") or + accept_string(parser, "pre", "rc") or + accept_string(parser, "rc", "rc") + ): + return parser.accept() + else: + return parser.discard() + +def accept_prerelease(parser): + """PEP 440: Pre-releases. + + Args: + parser: The normalizer. + + Returns: + whether a prerelease identifier was accepted. + """ + ctx = parser.open_context() + + # PEP 440: Pre-release separators + accept(parser, _in(["-", "_", "."]), "") + + if not accept_pre_l(parser): + return parser.discard() + + accept(parser, _in(["-", "_", "."]), "") + + if not accept_digits(parser): + # PEP 440: Implicit pre-release number + ctx["norm"] += "0" + + return parser.accept() + +def accept_implicit_postrelease(parser): + """PEP 440: Implicit post releases. + + Args: + parser: The normalizer. + + Returns: + whether an implicit postrelease identifier was accepted. + """ + ctx = parser.open_context() + + if accept(parser, _is("-"), "") and accept_digits(parser): + ctx["norm"] = ".post" + ctx["norm"] + return parser.accept() + + return parser.discard() + +def accept_explicit_postrelease(parser): + """PEP 440: Post-releases. + + Args: + parser: The normalizer. + + Returns: + whether an explicit postrelease identifier was accepted. + """ + ctx = parser.open_context() + + # PEP 440: Post release separators + if not accept(parser, _in(["-", "_", "."]), "."): + ctx["norm"] += "." + + # PEP 440: Post release spelling + if ( + accept_string(parser, "post", "post") or + accept_string(parser, "rev", "post") or + accept_string(parser, "r", "post") + ): + accept(parser, _in(["-", "_", "."]), "") + + if not accept_digits(parser): + # PEP 440: Implicit post release number + ctx["norm"] += "0" + + return parser.accept() + + return parser.discard() + +def accept_postrelease(parser): + """PEP 440: Post-releases. + + Args: + parser: The normalizer. + + Returns: + whether a postrelease identifier was accepted. + """ + parser.open_context() + + if accept_implicit_postrelease(parser) or accept_explicit_postrelease(parser): + return parser.accept() + + return parser.discard() + +def accept_devrelease(parser): + """PEP 440: Developmental releases. + + Args: + parser: The normalizer. + + Returns: + whether a developmental release identifier was accepted. + """ + ctx = parser.open_context() + + # PEP 440: Development release separators + if not accept(parser, _in(["-", "_", "."]), "."): + ctx["norm"] += "." + + if accept_string(parser, "dev", "dev"): + accept(parser, _in(["-", "_", "."]), "") + + if not accept_digits(parser): + # PEP 440: Implicit development release number + ctx["norm"] += "0" + + return parser.accept() + + return parser.discard() + +def accept_local(parser): + """PEP 440: Local version identifiers. + + Args: + parser: The normalizer. + + Returns: + whether a local version identifier was accepted. + """ + parser.open_context() + + if accept(parser, _is("+"), "+") and accept_alnum(parser): + accept_separator_alnum_sequence(parser) + return parser.accept() + + return parser.discard() + +def normalize_pep440(version): + """Escape the version component of a filename. + + See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode + and https://peps.python.org/pep-0440/ + + Args: + version: version string to be normalized according to PEP 440. + + Returns: + string containing the normalized version. + """ + parser = _new(version.strip()) # PEP 440: Leading and Trailing Whitespace + accept(parser, _is("v"), "") # PEP 440: Preceding v character + accept_epoch(parser) + accept_release(parser) + accept_prerelease(parser) + accept_postrelease(parser) + accept_devrelease(parser) + accept_local(parser) + if parser.input[parser.context()["start"]:]: + fail( + "Failed to parse PEP 440 version identifier '%s'." % parser.input, + "Parse error at '%s'" % parser.input[parser.context()["start"]:], + ) + return parser.context()["norm"] diff --git a/python/private/python_bootstrap_template.txt b/python/private/python_bootstrap_template.txt new file mode 100644 index 0000000000..92dd6b82fa --- /dev/null +++ b/python/private/python_bootstrap_template.txt @@ -0,0 +1,559 @@ +%shebang% + +# This script must retain compatibility with a wide variety of Python versions +# since it is run for every py_binary target. Currently we guarantee support +# going back to Python 2.7, and try to support even Python 2.6 on a best-effort +# basis. We might abandon 2.6 support once users have the ability to control the +# above shebang string via the Python toolchain (#8685). + +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +import sys + +# The Python interpreter unconditionally prepends the directory containing this +# script (following symlinks) to the import path. This is the cause of #9239, +# and is a special case of #7091. We therefore explicitly delete that entry. +# TODO(#7091): Remove this hack when no longer necessary. +del sys.path[0] + +import os +import subprocess + +def IsRunningFromZip(): + return %is_zipfile% + +if IsRunningFromZip(): + import shutil + import tempfile + import zipfile +else: + import re + +# Return True if running on Windows +def IsWindows(): + return os.name == 'nt' + +def GetWindowsPathWithUNCPrefix(path): + """Adds UNC prefix after getting a normalized absolute Windows path. + + No-op for non-Windows platforms or if running under python2. + """ + path = path.strip() + + # No need to add prefix for non-Windows platforms. + # And \\?\ doesn't work in python 2 or on mingw + if not IsWindows() or sys.version_info[0] < 3: + return path + + # Starting in Windows 10, version 1607(OS build 14393), MAX_PATH limitations have been + # removed from common Win32 file and directory functions. + # Related doc: https://docs.microsoft.com/en-us/windows/win32/fileio/maximum-file-path-limitation?tabs=cmd#enable-long-paths-in-windows-10-version-1607-and-later + import platform + if platform.win32_ver()[1] >= '10.0.14393': + return path + + # import sysconfig only now to maintain python 2.6 compatibility + import sysconfig + if sysconfig.get_platform() == 'mingw': + return path + + # Lets start the unicode fun + unicode_prefix = '\\\\?\\' + if path.startswith(unicode_prefix): + return path + + # os.path.abspath returns a normalized absolute path + return unicode_prefix + os.path.abspath(path) + +def HasWindowsExecutableExtension(path): + return path.endswith('.exe') or path.endswith('.com') or path.endswith('.bat') + +PYTHON_BINARY = '%python_binary%' +if IsWindows() and not HasWindowsExecutableExtension(PYTHON_BINARY): + PYTHON_BINARY = PYTHON_BINARY + '.exe' + +def SearchPath(name): + """Finds a file in a given search path.""" + search_path = os.getenv('PATH', os.defpath).split(os.pathsep) + for directory in search_path: + if directory: + path = os.path.join(directory, name) + if os.path.isfile(path) and os.access(path, os.X_OK): + return path + return None + +def FindPythonBinary(module_space): + """Finds the real Python binary if it's not a normal absolute path.""" + return FindBinary(module_space, PYTHON_BINARY) + +def PrintVerboseCoverage(*args): + """Print output if VERBOSE_COVERAGE is non-empty in the environment.""" + if os.environ.get("VERBOSE_COVERAGE"): + print(*args, file=sys.stderr) + +def FindCoverageEntryPoint(module_space): + cov_tool = '%coverage_tool%' + if cov_tool: + PrintVerboseCoverage('Using toolchain coverage_tool %r' % cov_tool) + else: + cov_tool = os.environ.get('PYTHON_COVERAGE') + if cov_tool: + PrintVerboseCoverage('PYTHON_COVERAGE: %r' % cov_tool) + if cov_tool: + return FindBinary(module_space, cov_tool) + return None + +def FindBinary(module_space, bin_name): + """Finds the real binary if it's not a normal absolute path.""" + if not bin_name: + return None + if bin_name.startswith("//"): + # Case 1: Path is a label. Not supported yet. + raise AssertionError( + "Bazel does not support execution of Python interpreters via labels yet" + ) + elif os.path.isabs(bin_name): + # Case 2: Absolute path. + return bin_name + # Use normpath() to convert slashes to os.sep on Windows. + elif os.sep in os.path.normpath(bin_name): + # Case 3: Path is relative to the repo root. + return os.path.join(module_space, bin_name) + else: + # Case 4: Path has to be looked up in the search path. + return SearchPath(bin_name) + +def CreatePythonPathEntries(python_imports, module_space): + parts = python_imports.split(':') + return [module_space] + ['%s/%s' % (module_space, path) for path in parts] + +def FindModuleSpace(main_rel_path): + """Finds the runfiles tree.""" + # When the calling process used the runfiles manifest to resolve the + # location of this stub script, the path may be expanded. This means + # argv[0] may no longer point to a location inside the runfiles + # directory. We should therefore respect RUNFILES_DIR and + # RUNFILES_MANIFEST_FILE set by the caller. + runfiles_dir = os.environ.get('RUNFILES_DIR', None) + if not runfiles_dir: + runfiles_manifest_file = os.environ.get('RUNFILES_MANIFEST_FILE', '') + if (runfiles_manifest_file.endswith('.runfiles_manifest') or + runfiles_manifest_file.endswith('.runfiles/MANIFEST')): + runfiles_dir = runfiles_manifest_file[:-9] + # Be defensive: the runfiles dir should contain our main entry point. If + # it doesn't, then it must not be our runfiles directory. + if runfiles_dir and os.path.exists(os.path.join(runfiles_dir, main_rel_path)): + return runfiles_dir + + stub_filename = sys.argv[0] + if not os.path.isabs(stub_filename): + stub_filename = os.path.join(os.getcwd(), stub_filename) + + while True: + module_space = stub_filename + ('.exe' if IsWindows() else '') + '.runfiles' + if os.path.isdir(module_space): + return module_space + + runfiles_pattern = r'(.*\.runfiles)' + (r'\\' if IsWindows() else '/') + '.*' + matchobj = re.match(runfiles_pattern, stub_filename) + if matchobj: + return matchobj.group(1) + + if not os.path.islink(stub_filename): + break + target = os.readlink(stub_filename) + if os.path.isabs(target): + stub_filename = target + else: + stub_filename = os.path.join(os.path.dirname(stub_filename), target) + + raise AssertionError('Cannot find .runfiles directory for %s' % sys.argv[0]) + +def ExtractZip(zip_path, dest_dir): + """Extracts the contents of a zip file, preserving the unix file mode bits. + + These include the permission bits, and in particular, the executable bit. + + Ideally the zipfile module should set these bits, but it doesn't. See: + https://bugs.python.org/issue15795. + + Args: + zip_path: The path to the zip file to extract + dest_dir: The path to the destination directory + """ + zip_path = GetWindowsPathWithUNCPrefix(zip_path) + dest_dir = GetWindowsPathWithUNCPrefix(dest_dir) + with zipfile.ZipFile(zip_path) as zf: + for info in zf.infolist(): + zf.extract(info, dest_dir) + # UNC-prefixed paths must be absolute/normalized. See + # https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file#maximum-path-length-limitation + file_path = os.path.abspath(os.path.join(dest_dir, info.filename)) + # The Unix st_mode bits (see "man 7 inode") are stored in the upper 16 + # bits of external_attr. Of those, we set the lower 12 bits, which are the + # file mode bits (since the file type bits can't be set by chmod anyway). + attrs = info.external_attr >> 16 + if attrs != 0: # Rumor has it these can be 0 for zips created on Windows. + os.chmod(file_path, attrs & 0o7777) + +# Create the runfiles tree by extracting the zip file +def CreateModuleSpace(): + temp_dir = tempfile.mkdtemp('', 'Bazel.runfiles_') + ExtractZip(os.path.dirname(__file__), temp_dir) + # IMPORTANT: Later code does `rm -fr` on dirname(module_space) -- it's + # important that deletion code be in sync with this directory structure + return os.path.join(temp_dir, 'runfiles') + +# Returns repository roots to add to the import path. +def GetRepositoriesImports(module_space, import_all): + if import_all: + repo_dirs = [os.path.join(module_space, d) for d in os.listdir(module_space)] + repo_dirs.sort() + return [d for d in repo_dirs if os.path.isdir(d)] + return [os.path.join(module_space, '%workspace_name%')] + +def RunfilesEnvvar(module_space): + """Finds the runfiles manifest or the runfiles directory. + + Returns: + A tuple of (var_name, var_value) where var_name is either 'RUNFILES_DIR' or + 'RUNFILES_MANIFEST_FILE' and var_value is the path to that directory or + file, or (None, None) if runfiles couldn't be found. + """ + # If this binary is the data-dependency of another one, the other sets + # RUNFILES_MANIFEST_FILE or RUNFILES_DIR for our sake. + runfiles = os.environ.get('RUNFILES_MANIFEST_FILE', None) + if runfiles: + return ('RUNFILES_MANIFEST_FILE', runfiles) + + runfiles = os.environ.get('RUNFILES_DIR', None) + if runfiles: + return ('RUNFILES_DIR', runfiles) + + # If running from a zip, there's no manifest file. + if IsRunningFromZip(): + return ('RUNFILES_DIR', module_space) + + # Look for the runfiles "output" manifest, argv[0] + ".runfiles_manifest" + runfiles = module_space + '_manifest' + if os.path.exists(runfiles): + return ('RUNFILES_MANIFEST_FILE', runfiles) + + # Look for the runfiles "input" manifest, argv[0] + ".runfiles/MANIFEST" + # Normally .runfiles_manifest and MANIFEST are both present, but the + # former will be missing for zip-based builds or if someone copies the + # runfiles tree elsewhere. + runfiles = os.path.join(module_space, 'MANIFEST') + if os.path.exists(runfiles): + return ('RUNFILES_MANIFEST_FILE', runfiles) + + # If running in a sandbox and no environment variables are set, then + # Look for the runfiles next to the binary. + if module_space.endswith('.runfiles') and os.path.isdir(module_space): + return ('RUNFILES_DIR', module_space) + + return (None, None) + +def Deduplicate(items): + """Efficiently filter out duplicates, keeping the first element only.""" + seen = set() + for it in items: + if it not in seen: + seen.add(it) + yield it + +def InstrumentedFilePaths(): + """Yields tuples of realpath of each instrumented file with the relative path.""" + manifest_filename = os.environ.get('COVERAGE_MANIFEST') + if not manifest_filename: + return + with open(manifest_filename, "r") as manifest: + for line in manifest: + filename = line.strip() + if not filename: + continue + try: + realpath = os.path.realpath(filename) + except OSError: + print( + "Could not find instrumented file {}".format(filename), + file=sys.stderr) + continue + if realpath != filename: + PrintVerboseCoverage("Fixing up {} -> {}".format(realpath, filename)) + yield (realpath, filename) + +def UnresolveSymlinks(output_filename): + # type: (str) -> None + """Replace realpath of instrumented files with the relative path in the lcov output. + + Though we are asking coveragepy to use relative file names, currently + ignore that for purposes of generating the lcov report (and other reports + which are not the XML report), so we need to go and fix up the report. + + This function is a workaround for that issue. Once that issue is fixed + upstream and the updated version is widely in use, this should be removed. + + See https://github.com/nedbat/coveragepy/issues/963. + """ + substitutions = list(InstrumentedFilePaths()) + if substitutions: + unfixed_file = output_filename + '.tmp' + os.rename(output_filename, unfixed_file) + with open(unfixed_file, "r") as unfixed: + with open(output_filename, "w") as output_file: + for line in unfixed: + if line.startswith('SF:'): + for (realpath, filename) in substitutions: + line = line.replace(realpath, filename) + output_file.write(line) + os.unlink(unfixed_file) + +def ExecuteFile(python_program, main_filename, args, env, module_space, + coverage_entrypoint, workspace, delete_module_space): + # type: (str, str, list[str], dict[str, str], str, str|None, str|None) -> ... + """Executes the given Python file using the various environment settings. + + This will not return, and acts much like os.execv, except is much + more restricted, and handles Bazel-related edge cases. + + Args: + python_program: (str) Path to the Python binary to use for execution + main_filename: (str) The Python file to execute + args: (list[str]) Additional args to pass to the Python file + env: (dict[str, str]) A dict of environment variables to set for the execution + module_space: (str) Path to the module space/runfiles tree directory + coverage_entrypoint: (str|None) Path to the coverage tool entry point file. + workspace: (str|None) Name of the workspace to execute in. This is expected to be a + directory under the runfiles tree. + delete_module_space: (bool), True if the module space should be deleted + after a successful (exit code zero) program run, False if not. + """ + # We want to use os.execv instead of subprocess.call, which causes + # problems with signal passing (making it difficult to kill + # Bazel). However, these conditions force us to run via + # subprocess.call instead: + # + # - On Windows, os.execv doesn't handle arguments with spaces + # correctly, and it actually starts a subprocess just like + # subprocess.call. + # - When running in a workspace or zip file, we need to clean up the + # workspace after the process finishes so control must return here. + # - If we may need to emit a host config warning after execution, we + # can't execv because we need control to return here. This only + # happens for targets built in the host config. + # - For coverage targets, at least coveragepy requires running in + # two invocations, which also requires control to return here. + # + if not (IsWindows() or workspace or coverage_entrypoint or delete_module_space): + _RunExecv(python_program, main_filename, args, env) + + if coverage_entrypoint is not None: + ret_code = _RunForCoverage(python_program, main_filename, args, env, + coverage_entrypoint, workspace) + else: + ret_code = subprocess.call( + [python_program, main_filename] + args, + env=env, + cwd=workspace + ) + + if delete_module_space: + # NOTE: dirname() is called because CreateModuleSpace() creates a + # sub-directory within a temporary directory, and we want to remove the + # whole temporary directory. + shutil.rmtree(os.path.dirname(module_space), True) + sys.exit(ret_code) + +def _RunExecv(python_program, main_filename, args, env): + # type: (str, str, list[str], dict[str, str]) -> ... + """Executes the given Python file using the various environment settings.""" + os.environ.update(env) + os.execv(python_program, [python_program, main_filename] + args) + +def _RunForCoverage(python_program, main_filename, args, env, + coverage_entrypoint, workspace): + # type: (str, str, list[str], dict[str, str], str, str|None) -> int + """Collects coverage infomration for the given Python file. + + Args: + python_program: (str) Path to the Python binary to use for execution + main_filename: (str) The Python file to execute + args: (list[str]) Additional args to pass to the Python file + env: (dict[str, str]) A dict of environment variables to set for the execution + coverage_entrypoint: (str|None) Path to the coverage entry point to execute with. + workspace: (str|None) Name of the workspace to execute in. This is expected to be a + directory under the runfiles tree, and will recursively delete the + runfiles directory if set. + """ + # We need for coveragepy to use relative paths. This can only be configured + # via an rc file, so we need to make one. + rcfile_name = os.path.join(os.environ['COVERAGE_DIR'], '.coveragerc') + with open(rcfile_name, "w") as rcfile: + rcfile.write('''[run] +relative_files = True +''') + PrintVerboseCoverage('Coverage entrypoint:', coverage_entrypoint) + # First run the target Python file via coveragepy to create a .coverage + # database file, from which we can later export lcov. + ret_code = subprocess.call( + [ + python_program, + coverage_entrypoint, + "run", + "--rcfile=" + rcfile_name, + "--append", + "--branch", + main_filename + ] + args, + env=env, + cwd=workspace + ) + output_filename = os.path.join(os.environ['COVERAGE_DIR'], 'pylcov.dat') + + PrintVerboseCoverage('Converting coveragepy database to lcov:', output_filename) + # Run coveragepy again to convert its .coverage database file into lcov. + ret_code = subprocess.call( + [ + python_program, + coverage_entrypoint, + "lcov", + "--rcfile=" + rcfile_name, + "-o", + output_filename + ], + env=env, + cwd=workspace + ) or ret_code + try: + os.unlink(rcfile_name) + except OSError as err: + # It's possible that the profiled program might execute another Python + # binary through a wrapper that would then delete the rcfile. Not much + # we can do about that, besides ignore the failure here. + PrintVerboseCoverage('Error removing temporary coverage rc file:', err) + if os.path.isfile(output_filename): + UnresolveSymlinks(output_filename) + return ret_code + +def Main(): + args = sys.argv[1:] + + new_env = {} + + # The main Python source file. + # The magic string percent-main-percent is replaced with the runfiles-relative + # filename of the main file of the Python binary in BazelPythonSemantics.java. + main_rel_path = '%main%' + if IsWindows(): + main_rel_path = main_rel_path.replace('/', os.sep) + + if IsRunningFromZip(): + module_space = CreateModuleSpace() + delete_module_space = True + else: + module_space = FindModuleSpace(main_rel_path) + delete_module_space = False + + python_imports = '%imports%' + python_path_entries = CreatePythonPathEntries(python_imports, module_space) + python_path_entries += GetRepositoriesImports(module_space, %import_all%) + # Remove duplicates to avoid overly long PYTHONPATH (#10977). Preserve order, + # keep first occurrence only. + python_path_entries = [ + GetWindowsPathWithUNCPrefix(d) + for d in python_path_entries + ] + + old_python_path = os.environ.get('PYTHONPATH') + if old_python_path: + python_path_entries += old_python_path.split(os.pathsep) + + python_path = os.pathsep.join(Deduplicate(python_path_entries)) + + if IsWindows(): + python_path = python_path.replace('/', os.sep) + + new_env['PYTHONPATH'] = python_path + runfiles_envkey, runfiles_envvalue = RunfilesEnvvar(module_space) + if runfiles_envkey: + new_env[runfiles_envkey] = runfiles_envvalue + + # Don't prepend a potentially unsafe path to sys.path + # See: https://docs.python.org/3.11/using/cmdline.html#envvar-PYTHONSAFEPATH + new_env['PYTHONSAFEPATH'] = '1' + + main_filename = os.path.join(module_space, main_rel_path) + main_filename = GetWindowsPathWithUNCPrefix(main_filename) + assert os.path.exists(main_filename), \ + 'Cannot exec() %r: file not found.' % main_filename + assert os.access(main_filename, os.R_OK), \ + 'Cannot exec() %r: file not readable.' % main_filename + + program = python_program = FindPythonBinary(module_space) + if python_program is None: + raise AssertionError('Could not find python binary: ' + PYTHON_BINARY) + + # COVERAGE_DIR is set if coverage is enabled and instrumentation is configured + # for something, though it could be another program executing this one or + # one executed by this one (e.g. an extension module). + if os.environ.get('COVERAGE_DIR'): + cov_tool = FindCoverageEntryPoint(module_space) + if cov_tool is None: + PrintVerboseCoverage('Coverage was enabled, but python coverage tool was not configured.') + else: + # Inhibit infinite recursion: + if 'PYTHON_COVERAGE' in os.environ: + del os.environ['PYTHON_COVERAGE'] + + if not os.path.exists(cov_tool): + raise EnvironmentError( + 'Python coverage tool %r not found. ' + 'Try running with VERBOSE_COVERAGE=1 to collect more information.' + % cov_tool + ) + + # coverage library expects sys.path[0] to contain the library, and replaces + # it with the directory of the program it starts. Our actual sys.path[0] is + # the runfiles directory, which must not be replaced. + # CoverageScript.do_execute() undoes this sys.path[0] setting. + # + # Update sys.path such that python finds the coverage package. The coverage + # entry point is coverage.coverage_main, so we need to do twice the dirname. + python_path_entries = new_env['PYTHONPATH'].split(os.pathsep) + python_path_entries.append(os.path.dirname(os.path.dirname(cov_tool))) + new_env['PYTHONPATH'] = os.pathsep.join(Deduplicate(python_path_entries)) + else: + cov_tool = None + + new_env.update((key, val) for key, val in os.environ.items() if key not in new_env) + + workspace = None + if IsRunningFromZip(): + # If RUN_UNDER_RUNFILES equals 1, it means we need to + # change directory to the right runfiles directory. + # (So that the data files are accessible) + if os.environ.get('RUN_UNDER_RUNFILES') == '1': + workspace = os.path.join(module_space, '%workspace_name%') + + try: + sys.stdout.flush() + # NOTE: ExecuteFile may call execve() and lines after this will never run. + ExecuteFile( + python_program, main_filename, args, new_env, module_space, + cov_tool, workspace, + delete_module_space = delete_module_space, + ) + + except EnvironmentError: + # This works from Python 2.4 all the way to 3.x. + e = sys.exc_info()[1] + # This exception occurs when os.execv() fails for some reason. + if not getattr(e, 'filename', None): + e.filename = program # Add info to error message + raise + +if __name__ == '__main__': + Main() diff --git a/python/private/toolchains_repo.bzl b/python/private/toolchains_repo.bzl index b2919c1041..20dc9763e0 100644 --- a/python/private/toolchains_repo.bzl +++ b/python/private/toolchains_repo.bzl @@ -182,7 +182,15 @@ alias(name = "pip", actual = select({{":" + item: "@{py_repository}_ rctx.file("defs.bzl", content = """\ # Generated by python/private/toolchains_repo.bzl -load("{rules_python}//python/config_settings:transition.bzl", _py_binary = "py_binary", _py_test = "py_test") +load( + "{rules_python}//python/config_settings:transition.bzl", + _py_binary = "py_binary", + _py_test = "py_test", +) +load( + "{rules_python}//python/entry_points:py_console_script_binary.bzl", + _py_console_script_binary = "py_console_script_binary", +) load("{rules_python}//python:pip.bzl", _compile_pip_requirements = "compile_pip_requirements") host_platform = "{host_platform}" @@ -195,6 +203,13 @@ def py_binary(name, **kwargs): **kwargs ) +def py_console_script_binary(name, **kwargs): + return _py_console_script_binary( + name = name, + binary_rule = py_binary, + **kwargs + ) + def py_test(name, **kwargs): return _py_test( name = name, @@ -247,6 +262,7 @@ load( _host_platform = "host_platform", _interpreter = "interpreter", _py_binary = "py_binary", + _py_console_script_binary = "py_console_script_binary", _py_test = "py_test", ) @@ -254,6 +270,7 @@ compile_pip_requirements = _compile_pip_requirements host_platform = _host_platform interpreter = _interpreter py_binary = _py_binary +py_console_script_binary = _py_console_script_binary py_test = _py_test """.format( repository_name = repository_name, diff --git a/tools/build_defs/python/tests/BUILD.bazel b/python/proto/BUILD.bazel similarity index 75% rename from tools/build_defs/python/tests/BUILD.bazel rename to python/proto/BUILD.bazel index e271850834..9f60574f26 100644 --- a/tools/build_defs/python/tests/BUILD.bazel +++ b/python/proto/BUILD.bazel @@ -12,16 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -platform( - name = "mac", - constraint_values = [ - "@platforms//os:macos", - ], -) +package(default_visibility = ["//visibility:public"]) -platform( - name = "linux", - constraint_values = [ - "@platforms//os:linux", - ], -) +# Toolchain type provided by proto_lang_toolchain rule and used by py_proto_library +toolchain_type(name = "toolchain_type") diff --git a/python/py_binary.bzl b/python/py_binary.bzl index 6b6f7e0f8a..6dcb4ad40c 100644 --- a/python/py_binary.bzl +++ b/python/py_binary.bzl @@ -14,7 +14,12 @@ """Public entry point for py_binary.""" +load("@rules_python_internal//:rules_python_config.bzl", "config") load("//python/private:util.bzl", "add_migration_tag") +load("//python/private/common:py_binary_macro_bazel.bzl", _starlark_py_binary = "py_binary") + +# buildifier: disable=native-python +_py_binary_impl = _starlark_py_binary if config.enable_pystar else native.py_binary def py_binary(**attrs): """See the Bazel core [py_binary](https://docs.bazel.build/versions/master/be/python.html#py_binary) documentation. @@ -27,5 +32,4 @@ def py_binary(**attrs): if attrs.get("srcs_version") in ("PY2", "PY2ONLY"): fail("Python 2 is no longer supported: https://github.com/bazelbuild/rules_python/issues/886") - # buildifier: disable=native-python - native.py_binary(**add_migration_tag(attrs)) + _py_binary_impl(**add_migration_tag(attrs)) diff --git a/python/py_info.bzl b/python/py_info.bzl index 2c3997dee2..cbf145d07d 100644 --- a/python/py_info.bzl +++ b/python/py_info.bzl @@ -14,6 +14,8 @@ """Public entry point for PyInfo.""" +load("@rules_python_internal//:rules_python_config.bzl", "config") load("//python/private:reexports.bzl", "internal_PyInfo") +load("//python/private/common:providers.bzl", _starlark_PyInfo = "PyInfo") -PyInfo = internal_PyInfo +PyInfo = _starlark_PyInfo if config.enable_pystar else internal_PyInfo diff --git a/python/py_library.bzl b/python/py_library.bzl index d54cbb2958..ef4c3c3969 100644 --- a/python/py_library.bzl +++ b/python/py_library.bzl @@ -14,7 +14,12 @@ """Public entry point for py_library.""" +load("@rules_python_internal//:rules_python_config.bzl", "config") load("//python/private:util.bzl", "add_migration_tag") +load("//python/private/common:py_library_macro_bazel.bzl", _starlark_py_library = "py_library") + +# buildifier: disable=native-python +_py_library_impl = _starlark_py_library if config.enable_pystar else native.py_library def py_library(**attrs): """See the Bazel core [py_library](https://docs.bazel.build/versions/master/be/python.html#py_library) documentation. @@ -25,5 +30,4 @@ def py_library(**attrs): if attrs.get("srcs_version") in ("PY2", "PY2ONLY"): fail("Python 2 is no longer supported: https://github.com/bazelbuild/rules_python/issues/886") - # buildifier: disable=native-python - native.py_library(**add_migration_tag(attrs)) + _py_library_impl(**add_migration_tag(attrs)) diff --git a/python/py_runtime.bzl b/python/py_runtime.bzl index b70f9d4ec4..ac8b090c94 100644 --- a/python/py_runtime.bzl +++ b/python/py_runtime.bzl @@ -14,7 +14,12 @@ """Public entry point for py_runtime.""" +load("@rules_python_internal//:rules_python_config.bzl", "config") load("//python/private:util.bzl", "add_migration_tag") +load("//python/private/common:py_runtime_macro.bzl", _starlark_py_runtime = "py_runtime") + +# buildifier: disable=native-python +_py_runtime_impl = _starlark_py_runtime if config.enable_pystar else native.py_runtime def py_runtime(**attrs): """See the Bazel core [py_runtime](https://docs.bazel.build/versions/master/be/python.html#py_runtime) documentation. @@ -25,5 +30,4 @@ def py_runtime(**attrs): if attrs.get("python_version") == "PY2": fail("Python 2 is no longer supported: see https://github.com/bazelbuild/rules_python/issues/886") - # buildifier: disable=native-python - native.py_runtime(**add_migration_tag(attrs)) + _py_runtime_impl(**add_migration_tag(attrs)) diff --git a/python/py_runtime_info.bzl b/python/py_runtime_info.bzl index 15598ee903..699b31d6df 100644 --- a/python/py_runtime_info.bzl +++ b/python/py_runtime_info.bzl @@ -14,6 +14,8 @@ """Public entry point for PyRuntimeInfo.""" +load("@rules_python_internal//:rules_python_config.bzl", "config") load("//python/private:reexports.bzl", "internal_PyRuntimeInfo") +load("//python/private/common:providers.bzl", _starlark_PyRuntimeInfo = "PyRuntimeInfo") -PyRuntimeInfo = internal_PyRuntimeInfo +PyRuntimeInfo = _starlark_PyRuntimeInfo if config.enable_pystar else internal_PyRuntimeInfo diff --git a/python/py_runtime_pair.bzl b/python/py_runtime_pair.bzl index 951c606f4a..c80994c963 100644 --- a/python/py_runtime_pair.bzl +++ b/python/py_runtime_pair.bzl @@ -14,7 +14,11 @@ """Public entry point for py_runtime_pair.""" -load("@bazel_tools//tools/python:toolchain.bzl", _py_runtime_pair = "py_runtime_pair") +load("@bazel_tools//tools/python:toolchain.bzl", _bazel_tools_impl = "py_runtime_pair") +load("@rules_python_internal//:rules_python_config.bzl", "config") +load("//python/private:py_runtime_pair_macro.bzl", _starlark_impl = "py_runtime_pair") + +_py_runtime_pair = _bazel_tools_impl if not config.enable_pystar else _starlark_impl # NOTE: This doc is copy/pasted from the builtin py_runtime_pair rule so our # doc generator gives useful API docs. diff --git a/python/py_test.bzl b/python/py_test.bzl index 09580c01c4..ad9bdc06ad 100644 --- a/python/py_test.bzl +++ b/python/py_test.bzl @@ -14,7 +14,12 @@ """Public entry point for py_test.""" +load("@rules_python_internal//:rules_python_config.bzl", "config") load("//python/private:util.bzl", "add_migration_tag") +load("//python/private/common:py_test_macro_bazel.bzl", _starlark_py_test = "py_test") + +# buildifier: disable=native-python +_py_test_impl = _starlark_py_test if config.enable_pystar else native.py_test def py_test(**attrs): """See the Bazel core [py_test](https://docs.bazel.build/versions/master/be/python.html#py_test) documentation. @@ -28,4 +33,4 @@ def py_test(**attrs): fail("Python 2 is no longer supported: https://github.com/bazelbuild/rules_python/issues/886") # buildifier: disable=native-python - native.py_test(**add_migration_tag(attrs)) + _py_test_impl(**add_migration_tag(attrs)) diff --git a/python/repositories.bzl b/python/repositories.bzl index bd06f0b3d0..050ba14a76 100644 --- a/python/repositories.bzl +++ b/python/repositories.bzl @@ -18,9 +18,11 @@ For historic reasons, pip_repositories() is defined in //python:pip.bzl. """ load("@bazel_tools//tools/build_defs/repo:http.bzl", _http_archive = "http_archive") -load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") +load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe", "read_netrc", "read_user_netrc", "use_netrc") load("//python/private:bzlmod_enabled.bzl", "BZLMOD_ENABLED") load("//python/private:coverage_deps.bzl", "coverage_dep") +load("//python/private:full_version.bzl", "full_version") +load("//python/private:internal_config_repo.bzl", "internal_config_repo") load( "//python/private:toolchains_repo.bzl", "multi_toolchain_aliases", @@ -31,7 +33,6 @@ load("//python/private:which.bzl", "which_with_fail") load( ":versions.bzl", "DEFAULT_RELEASE_BASE_URL", - "MINOR_MAPPING", "PLATFORMS", "TOOL_VERSIONS", "get_release_info", @@ -46,6 +47,10 @@ def py_repositories(): This function should be loaded and called in the user's WORKSPACE. With bzlmod enabled, this function is not needed since MODULE.bazel handles transitive deps. """ + maybe( + internal_config_repo, + name = "rules_python_internal", + ) http_archive( name = "bazel_skylib", sha256 = "74d544d96f4a5bb630d465ca8bbcfe231e3594e5aae57e1edbf17a6eb3ca2506", @@ -61,43 +66,52 @@ def py_repositories(): STANDALONE_INTERPRETER_FILENAME = "STANDALONE_INTERPRETER" -def get_interpreter_dirname(rctx, python_interpreter_target): - """Get a python interpreter target dirname. - - Args: - rctx (repository_ctx): The repository rule's context object. - python_interpreter_target (Target): A target representing a python interpreter. - - Returns: - str: The Python interpreter directory. - """ - - return rctx.path(Label("{}//:WORKSPACE".format(str(python_interpreter_target).split("//")[0]))).dirname - -def is_standalone_interpreter(rctx, python_interpreter_target): +def is_standalone_interpreter(rctx, python_interpreter_path): """Query a python interpreter target for whether or not it's a rules_rust provided toolchain Args: rctx (repository_ctx): The repository rule's context object. - python_interpreter_target (Target): A target representing a python interpreter. + python_interpreter_path (path): A path representing the interpreter. Returns: bool: Whether or not the target is from a rules_python generated toolchain. """ # Only update the location when using a hermetic toolchain. - if not python_interpreter_target: + if not python_interpreter_path: return False # This is a rules_python provided toolchain. return rctx.execute([ "ls", "{}/{}".format( - get_interpreter_dirname(rctx, python_interpreter_target), + python_interpreter_path.dirname, STANDALONE_INTERPRETER_FILENAME, ), ]).return_code == 0 +def _get_auth(rctx, urls): + """Utility for retrieving netrc-based authentication parameters for repository download rules used in python_repository. + + The implementation below is copied directly from Bazel's implementation of `http_archive`. + Accordingly, the return value of this function should be used identically as the `auth` parameter of `http_archive`. + Reference: https://github.com/bazelbuild/bazel/blob/6.3.2/tools/build_defs/repo/http.bzl#L109 + + Args: + rctx (repository_ctx): The repository rule's context object. + urls: A list of URLs from which assets will be downloaded. + + Returns: + dict: A map of authentication parameters by URL. + """ + if rctx.attr.netrc: + netrc = read_netrc(rctx, rctx.attr.netrc) + elif "NETRC" in rctx.os.environ: + netrc = read_netrc(rctx, rctx.os.environ["NETRC"]) + else: + netrc = read_user_netrc(rctx) + return use_netrc(netrc, urls, rctx.attr.auth_patterns) + def _python_repository_impl(rctx): if rctx.attr.distutils and rctx.attr.distutils_content: fail("Only one of (distutils, distutils_content) should be set.") @@ -109,12 +123,14 @@ def _python_repository_impl(rctx): python_short_version = python_version.rpartition(".")[0] release_filename = rctx.attr.release_filename urls = rctx.attr.urls or [rctx.attr.url] + auth = _get_auth(rctx, urls) if release_filename.endswith(".zst"): rctx.download( url = urls, sha256 = rctx.attr.sha256, output = release_filename, + auth = auth, ) unzstd = rctx.which("unzstd") if not unzstd: @@ -122,6 +138,7 @@ def _python_repository_impl(rctx): rctx.download_and_extract( url = url, sha256 = rctx.attr.zstd_sha256, + auth = auth, ) working_directory = "zstd-{version}".format(version = rctx.attr.zstd_version) @@ -159,6 +176,7 @@ def _python_repository_impl(rctx): url = urls, sha256 = rctx.attr.sha256, stripPrefix = rctx.attr.strip_prefix, + auth = auth, ) patches = rctx.attr.patches @@ -361,11 +379,13 @@ py_cc_toolchain( rctx.file("BUILD.bazel", build_content) attrs = { + "auth_patterns": rctx.attr.auth_patterns, "coverage_tool": rctx.attr.coverage_tool, "distutils": rctx.attr.distutils, "distutils_content": rctx.attr.distutils_content, "ignore_root_user_error": rctx.attr.ignore_root_user_error, "name": rctx.attr.name, + "netrc": rctx.attr.netrc, "patches": rctx.attr.patches, "platform": platform, "python_version": python_version, @@ -385,6 +405,9 @@ python_repository = repository_rule( _python_repository_impl, doc = "Fetches the external tools needed for the Python toolchain.", attrs = { + "auth_patterns": attr.string_dict( + doc = "Override mapping of hostnames to authorization patterns; mirrors the eponymous attribute from http_archive", + ), "coverage_tool": attr.string( # Mirrors the definition at # https://github.com/bazelbuild/bazel/blob/master/src/main/starlark/builtins_bzl/common/python/py_runtime_rule.bzl @@ -425,6 +448,9 @@ For more information see the official bazel docs doc = "Whether the check for root should be ignored or not. This causes cache misses with .pyc files.", mandatory = False, ), + "netrc": attr.string( + doc = ".netrc file to use for authentication; mirrors the eponymous attribute from http_archive", + ), "patches": attr.label_list( doc = "A list of patch files to apply to the unpacked interpreter", mandatory = False, @@ -508,8 +534,7 @@ def python_register_toolchains( base_url = kwargs.pop("base_url", DEFAULT_RELEASE_BASE_URL) - if python_version in MINOR_MAPPING: - python_version = MINOR_MAPPING[python_version] + python_version = full_version(python_version) toolchain_repo_name = "{name}_toolchains".format(name = name) diff --git a/python/tests/toolchains/workspace_template/WORKSPACE.tmpl b/python/tests/toolchains/workspace_template/WORKSPACE.tmpl index 973e020c1e..3335f4b063 100644 --- a/python/tests/toolchains/workspace_template/WORKSPACE.tmpl +++ b/python/tests/toolchains/workspace_template/WORKSPACE.tmpl @@ -19,7 +19,9 @@ local_repository( path = "", ) -load("@rules_python//python:repositories.bzl", "python_register_toolchains") +load("@rules_python//python:repositories.bzl", "python_register_toolchains", "py_repositories") + +py_repositories() python_register_toolchains( name = "python", diff --git a/python/versions.bzl b/python/versions.bzl index 1ef3172588..6c9bf252bf 100644 --- a/python/versions.bzl +++ b/python/versions.bzl @@ -97,6 +97,28 @@ TOOL_VERSIONS = { }, "strip_prefix": "python", }, + "3.8.17": { + "url": "20230826/cpython-{python_version}+20230826-{platform}-{build}.tar.gz", + "sha256": { + "aarch64-apple-darwin": "c6f7a130d0044a78e39648f4dae56dcff5a41eba91888a99f6e560507162e6a1", + "aarch64-unknown-linux-gnu": "9f6d585091fe26906ff1dbb80437a3fe37a1e3db34d6ecc0098f3d6a78356682", + "x86_64-apple-darwin": "155b06821607bae1a58ecc60a7d036b358c766f19e493b8876190765c883a5c2", + "x86_64-pc-windows-msvc": "6428e1b4e0b4482d390828de7d4c82815257443416cb786abe10cb2466ca68cd", + "x86_64-unknown-linux-gnu": "8d3e1826c0bb7821ec63288038644808a2d45553245af106c685ef5892fabcd8", + }, + "strip_prefix": "python", + }, + "3.8.18": { + "url": "20231002/cpython-{python_version}+20231002-{platform}-{build}.tar.gz", + "sha256": { + "aarch64-apple-darwin": "1825b1f7220bc93ff143f2e70b5c6a79c6469e0eeb40824e07a7277f59aabfda", + "aarch64-unknown-linux-gnu": "236a300f386ead02ca98dbddbc026ff4ef4de6701a394106e291ff8b75445ee1", + "x86_64-apple-darwin": "fcf04532e644644213977242cd724fe5e84c0a5ac92ae038e07f1b01b474fca3", + "x86_64-pc-windows-msvc": "a9d203e78caed94de368d154e841610cef6f6b484738573f4ae9059d37e898a5", + "x86_64-unknown-linux-gnu": "1e8a3babd1500111359b0f5675d770984bcbcb2cc8890b117394f0ed342fb9ec", + }, + "strip_prefix": "python", + }, "3.9.10": { "url": "20220227/cpython-{python_version}+20220227-{platform}-{build}.tar.gz", "sha256": { @@ -166,6 +188,19 @@ TOOL_VERSIONS = { }, "strip_prefix": "python", }, + "3.9.18": { + "url": "20231002/cpython-{python_version}+20231002-{platform}-{build}.tar.gz", + "sha256": { + "aarch64-apple-darwin": "fdc4054837e37b69798c2ef796222a480bc1f80e8ad3a01a95d0168d8282a007", + "aarch64-unknown-linux-gnu": "1e0a3e8ce8e58901a259748c0ab640d2b8294713782d14229e882c6898b2fb36", + "ppc64le-unknown-linux-gnu": "101c38b22fb2f5a0945156da4259c8e9efa0c08de9d7f59afa51e7ce6e22a1cc", + "s390x-unknown-linux-gnu": "eee31e55ffbc1f460d7b17f05dd89e45a2636f374a6f8dc29ea13d0497f7f586", + "x86_64-apple-darwin": "82231cb77d4a5c8081a1a1d5b8ae440abe6993514eb77a926c826e9a69a94fb1", + "x86_64-pc-windows-msvc": "02ea7bb64524886bd2b05d6b6be4401035e4ba4319146f274f0bcd992822cd75", + "x86_64-unknown-linux-gnu": "f3ff38b1ccae7dcebd8bbf2e533c9a984fac881de0ffd1636fbb61842bd924de", + }, + "strip_prefix": "python", + }, "3.10.2": { "url": "20220227/cpython-{python_version}+20220227-{platform}-{build}.tar.gz", "sha256": { @@ -246,6 +281,19 @@ TOOL_VERSIONS = { }, "strip_prefix": "python", }, + "3.10.13": { + "url": "20231002/cpython-{python_version}+20231002-{platform}-{build}.tar.gz", + "sha256": { + "aarch64-apple-darwin": "fd027b1dedf1ea034cdaa272e91771bdf75ddef4c8653b05d224a0645aa2ca3c", + "aarch64-unknown-linux-gnu": "8675915ff454ed2f1597e27794bc7df44f5933c26b94aa06af510fe91b58bb97", + "ppc64le-unknown-linux-gnu": "f3f9c43eec1a0c3f72845d0b705da17a336d3906b7df212d2640b8f47e8ff375", + "s390x-unknown-linux-gnu": "859f6cfe9aedb6e8858892fdc124037e83ab05f28d42a7acd314c6a16d6bd66c", + "x86_64-apple-darwin": "be0b19b6af1f7d8c667e5abef5505ad06cf72e5a11bb5844970c395a7e5b1275", + "x86_64-pc-windows-msvc": "b8d930ce0d04bda83037ad3653d7450f8907c88e24bb8255a29b8dab8930d6f1", + "x86_64-unknown-linux-gnu": "5d0429c67c992da19ba3eb58b3acd0b35ec5e915b8cae9a4aa8ca565c423847a", + }, + "strip_prefix": "python", + }, "3.11.1": { "url": "20230116/cpython-{python_version}+20230116-{platform}-{build}.tar.gz", "sha256": { @@ -282,14 +330,54 @@ TOOL_VERSIONS = { }, "strip_prefix": "python", }, + "3.11.5": { + "url": "20230826/cpython-{python_version}+20230826-{platform}-{build}.tar.gz", + "sha256": { + "aarch64-apple-darwin": "dab64b3580118ad2073babd7c29fd2053b616479df5c107d31fe2af1f45e948b", + "aarch64-unknown-linux-gnu": "bb5c5d1ea0f199fe2d3f0996fff4b48ca6ddc415a3dbd98f50bff7fce48aac80", + "ppc64le-unknown-linux-gnu": "14121b53e9c8c6d0741f911ae00102a35adbcf5c3cdf732687ef7617b7d7304d", + "s390x-unknown-linux-gnu": "fe459da39874443579d6fe88c68777c6d3e331038e1fb92a0451879fb6beb16d", + "x86_64-apple-darwin": "4a4efa7378c72f1dd8ebcce1afb99b24c01b07023aa6b8fea50eaedb50bf2bfc", + "x86_64-pc-windows-msvc": "00f002263efc8aea896bcfaaf906b1f4dab3e5cd3db53e2b69ab9a10ba220b97", + "x86_64-unknown-linux-gnu": "fbed6f7694b2faae5d7c401a856219c945397f772eea5ca50c6eb825cbc9d1e1", + }, + "strip_prefix": "python", + }, + "3.11.6": { + "url": "20231002/cpython-{python_version}+20231002-{platform}-{build}.tar.gz", + "sha256": { + "aarch64-apple-darwin": "916c35125b5d8323a21526d7a9154ca626453f63d0878e95b9f613a95006c990", + "aarch64-unknown-linux-gnu": "3e26a672df17708c4dc928475a5974c3fb3a34a9b45c65fb4bd1e50504cc84ec", + "ppc64le-unknown-linux-gnu": "7937035f690a624dba4d014ffd20c342e843dd46f89b0b0a1e5726b85deb8eaf", + "s390x-unknown-linux-gnu": "f9f19823dba3209cedc4647b00f46ed0177242917db20fb7fb539970e384531c", + "x86_64-apple-darwin": "178cb1716c2abc25cb56ae915096c1a083e60abeba57af001996e8bc6ce1a371", + "x86_64-pc-windows-msvc": "3933545e6d41462dd6a47e44133ea40995bc6efeed8c2e4cbdf1a699303e95ea", + "x86_64-unknown-linux-gnu": "ee37a7eae6e80148c7e3abc56e48a397c1664f044920463ad0df0fc706eacea8", + }, + "strip_prefix": "python", + }, + "3.12.0": { + "url": "20231002/cpython-{python_version}+20231002-{platform}-{build}.tar.gz", + "sha256": { + "aarch64-apple-darwin": "4734a2be2becb813830112c780c9879ac3aff111a0b0cd590e65ec7465774d02", + "aarch64-unknown-linux-gnu": "bccfe67cf5465a3dfb0336f053966e2613a9bc85a6588c2fcf1366ef930c4f88", + "ppc64le-unknown-linux-gnu": "b5dae075467ace32c594c7877fe6ebe0837681f814601d5d90ba4c0dfd87a1f2", + "s390x-unknown-linux-gnu": "5681621349dd85d9726d1b67c84a9686ce78f72e73a6f9e4cc4119911655759e", + "x86_64-apple-darwin": "5a9e88c8aa52b609d556777b52ebde464ae4b4f77e4aac4eb693af57395c9abf", + "x86_64-pc-windows-msvc": "facfaa1fbc8653f95057f3c4a0f8aa833dab0e0b316e24ee8686bc761d4b4f8d", + "x86_64-unknown-linux-gnu": "e51a5293f214053ddb4645b2c9f84542e2ef86870b8655704367bd4b29d39fe9", + }, + "strip_prefix": "python", + }, } # buildifier: disable=unsorted-dict-items MINOR_MAPPING = { - "3.8": "3.8.15", - "3.9": "3.9.17", - "3.10": "3.10.12", - "3.11": "3.11.4", + "3.8": "3.8.18", + "3.9": "3.9.18", + "3.10": "3.10.13", + "3.11": "3.11.6", + "3.12": "3.12.0", } PLATFORMS = { diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index 2dd2282146..70dfa48857 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -30,5 +30,6 @@ build_test( "//python:py_test_bzl", "//python/cc:py_cc_toolchain_bzl", "//python/cc:py_cc_toolchain_info_bzl", + "//python/entry_points:py_console_script_binary_bzl", ], ) diff --git a/tests/base_rules/BUILD.bazel b/tests/base_rules/BUILD.bazel new file mode 100644 index 0000000000..aa21042e25 --- /dev/null +++ b/tests/base_rules/BUILD.bazel @@ -0,0 +1,13 @@ +# 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. diff --git a/tools/build_defs/python/tests/base_tests.bzl b/tests/base_rules/base_tests.bzl similarity index 96% rename from tools/build_defs/python/tests/base_tests.bzl rename to tests/base_rules/base_tests.bzl index 467611fcd8..53001639f6 100644 --- a/tools/build_defs/python/tests/base_tests.bzl +++ b/tests/base_rules/base_tests.bzl @@ -17,8 +17,8 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:truth.bzl", "matching") load("@rules_testing//lib:util.bzl", "PREVENT_IMPLICIT_BUILDING_TAGS", rt_util = "util") load("//python:defs.bzl", "PyInfo") -load("//tools/build_defs/python/tests:py_info_subject.bzl", "py_info_subject") -load("//tools/build_defs/python/tests:util.bzl", pt_util = "util") +load("//tests/base_rules:py_info_subject.bzl", "py_info_subject") +load("//tests/base_rules:util.bzl", pt_util = "util") _tests = [] diff --git a/tools/build_defs/python/tests/py_binary/BUILD.bazel b/tests/base_rules/py_binary/BUILD.bazel similarity index 100% rename from tools/build_defs/python/tests/py_binary/BUILD.bazel rename to tests/base_rules/py_binary/BUILD.bazel diff --git a/tools/build_defs/python/tests/py_binary/py_binary_tests.bzl b/tests/base_rules/py_binary/py_binary_tests.bzl similarity index 92% rename from tools/build_defs/python/tests/py_binary/py_binary_tests.bzl rename to tests/base_rules/py_binary/py_binary_tests.bzl index 8d32632610..571955d3c6 100644 --- a/tools/build_defs/python/tests/py_binary/py_binary_tests.bzl +++ b/tests/base_rules/py_binary/py_binary_tests.bzl @@ -15,7 +15,7 @@ load("//python:defs.bzl", "py_binary") load( - "//tools/build_defs/python/tests:py_executable_base_tests.bzl", + "//tests/base_rules:py_executable_base_tests.bzl", "create_executable_tests", ) diff --git a/tools/build_defs/python/tests/py_executable_base_tests.bzl b/tests/base_rules/py_executable_base_tests.bzl similarity index 84% rename from tools/build_defs/python/tests/py_executable_base_tests.bzl rename to tests/base_rules/py_executable_base_tests.bzl index c66ea11e00..b5dea172c3 100644 --- a/tools/build_defs/python/tests/py_executable_base_tests.bzl +++ b/tests/base_rules/py_executable_base_tests.bzl @@ -13,14 +13,52 @@ # limitations under the License. """Tests common to py_binary and py_test (executable rules).""" +load("@rules_python_internal//:rules_python_config.bzl", rp_config = "config") load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:truth.bzl", "matching") load("@rules_testing//lib:util.bzl", rt_util = "util") -load("//tools/build_defs/python/tests:base_tests.bzl", "create_base_tests") -load("//tools/build_defs/python/tests:util.bzl", "WINDOWS_ATTR", pt_util = "util") +load("//tests/base_rules:base_tests.bzl", "create_base_tests") +load("//tests/base_rules:util.bzl", "WINDOWS_ATTR", pt_util = "util") +load("//tests/support:test_platforms.bzl", "WINDOWS") _tests = [] +def _test_basic_windows(name, config): + if rp_config.enable_pystar: + target_compatible_with = [] + else: + target_compatible_with = ["@platforms//:incompatible"] + rt_util.helper_target( + config.rule, + name = name + "_subject", + srcs = ["main.py"], + main = "main.py", + ) + analysis_test( + name = name, + impl = _test_basic_windows_impl, + target = name + "_subject", + config_settings = { + "//command_line_option:cpu": "windows_x86_64", + "//command_line_option:crosstool_top": Label("//tests/cc:cc_toolchain_suite"), + "//command_line_option:extra_toolchains": [str(Label("//tests/cc:all"))], + "//command_line_option:platforms": [WINDOWS], + }, + attr_values = {"target_compatible_with": target_compatible_with}, + ) + +def _test_basic_windows_impl(env, target): + target = env.expect.that_target(target) + target.executable().path().contains(".exe") + target.runfiles().contains_predicate(matching.str_endswith( + target.meta.format_str("/{name}"), + )) + target.runfiles().contains_predicate(matching.str_endswith( + target.meta.format_str("/{name}.exe"), + )) + +_tests.append(_test_basic_windows) + def _test_executable_in_runfiles(name, config): rt_util.helper_target( config.rule, diff --git a/tools/build_defs/python/tests/py_info_subject.bzl b/tests/base_rules/py_info_subject.bzl similarity index 100% rename from tools/build_defs/python/tests/py_info_subject.bzl rename to tests/base_rules/py_info_subject.bzl diff --git a/tools/build_defs/python/tests/py_library/BUILD.bazel b/tests/base_rules/py_library/BUILD.bazel similarity index 100% rename from tools/build_defs/python/tests/py_library/BUILD.bazel rename to tests/base_rules/py_library/BUILD.bazel diff --git a/tools/build_defs/python/tests/py_library/py_library_tests.bzl b/tests/base_rules/py_library/py_library_tests.bzl similarity index 96% rename from tools/build_defs/python/tests/py_library/py_library_tests.bzl rename to tests/base_rules/py_library/py_library_tests.bzl index 1fcb0c19b9..526735af71 100644 --- a/tools/build_defs/python/tests/py_library/py_library_tests.bzl +++ b/tests/base_rules/py_library/py_library_tests.bzl @@ -4,8 +4,8 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:truth.bzl", "matching") load("@rules_testing//lib:util.bzl", rt_util = "util") load("//python:defs.bzl", "PyRuntimeInfo", "py_library") -load("//tools/build_defs/python/tests:base_tests.bzl", "create_base_tests") -load("//tools/build_defs/python/tests:util.bzl", pt_util = "util") +load("//tests/base_rules:base_tests.bzl", "create_base_tests") +load("//tests/base_rules:util.bzl", pt_util = "util") _tests = [] diff --git a/tools/build_defs/python/tests/py_test/BUILD.bazel b/tests/base_rules/py_test/BUILD.bazel similarity index 100% rename from tools/build_defs/python/tests/py_test/BUILD.bazel rename to tests/base_rules/py_test/BUILD.bazel diff --git a/tools/build_defs/python/tests/py_test/py_test_tests.bzl b/tests/base_rules/py_test/py_test_tests.bzl similarity index 89% rename from tools/build_defs/python/tests/py_test/py_test_tests.bzl rename to tests/base_rules/py_test/py_test_tests.bzl index 1ecb2524bf..f4b704e6ca 100644 --- a/tools/build_defs/python/tests/py_test/py_test_tests.bzl +++ b/tests/base_rules/py_test/py_test_tests.bzl @@ -17,17 +17,16 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:util.bzl", rt_util = "util") load("//python:defs.bzl", "py_test") load( - "//tools/build_defs/python/tests:py_executable_base_tests.bzl", + "//tests/base_rules:py_executable_base_tests.bzl", "create_executable_tests", ) -load("//tools/build_defs/python/tests:util.bzl", pt_util = "util") +load("//tests/base_rules:util.bzl", pt_util = "util") +load("//tests/support:test_platforms.bzl", "LINUX", "MAC") -# Explicit Label() calls are required so that it resolves in @rules_python context instead of -# @rules_testing context. +# Explicit Label() calls are required so that it resolves in @rules_python +# context instead of @rules_testing context. _FAKE_CC_TOOLCHAIN = Label("//tests/cc:cc_toolchain_suite") _FAKE_CC_TOOLCHAINS = [str(Label("//tests/cc:all"))] -_PLATFORM_MAC = Label("//tools/build_defs/python/tests:mac") -_PLATFORM_LINUX = Label("//tools/build_defs/python/tests:linux") _tests = [] @@ -53,7 +52,7 @@ def _test_mac_requires_darwin_for_execution(name, config): "//command_line_option:cpu": "darwin_x86_64", "//command_line_option:crosstool_top": _FAKE_CC_TOOLCHAIN, "//command_line_option:extra_toolchains": _FAKE_CC_TOOLCHAINS, - "//command_line_option:platforms": [_PLATFORM_MAC], + "//command_line_option:platforms": [MAC], }, ) @@ -85,7 +84,7 @@ def _test_non_mac_doesnt_require_darwin_for_execution(name, config): "//command_line_option:cpu": "k8", "//command_line_option:crosstool_top": _FAKE_CC_TOOLCHAIN, "//command_line_option:extra_toolchains": _FAKE_CC_TOOLCHAINS, - "//command_line_option:platforms": [_PLATFORM_LINUX], + "//command_line_option:platforms": [LINUX], }, ) diff --git a/tools/build_defs/python/tests/util.bzl b/tests/base_rules/util.bzl similarity index 100% rename from tools/build_defs/python/tests/util.bzl rename to tests/base_rules/util.bzl diff --git a/tests/cc/BUILD.bazel b/tests/cc/BUILD.bazel index 3f7925d631..ef64d6dbef 100644 --- a/tests/cc/BUILD.bazel +++ b/tests/cc/BUILD.bazel @@ -50,6 +50,7 @@ cc_toolchain_suite( toolchains = { "darwin_x86_64": ":mac_toolchain", "k8": ":linux_toolchain", + "windows_x86_64": ":windows_toolchain", }, ) @@ -106,3 +107,29 @@ fake_cc_toolchain_config( target_cpu = "k8", toolchain_identifier = "linux-toolchain", ) + +cc_toolchain( + name = "windows_toolchain", + all_files = ":empty", + compiler_files = ":empty", + dwp_files = ":empty", + linker_files = ":empty", + objcopy_files = ":empty", + strip_files = ":empty", + supports_param_files = 0, + toolchain_config = ":windows_toolchain_config", + toolchain_identifier = "windows-toolchain", +) + +toolchain( + name = "windows_toolchain_definition", + target_compatible_with = ["@platforms//os:windows"], + toolchain = ":windows_toolchain", + toolchain_type = "@bazel_tools//tools/cpp:toolchain_type", +) + +fake_cc_toolchain_config( + name = "windows_toolchain_config", + target_cpu = "windows_x86_64", + toolchain_identifier = "windows-toolchain", +) diff --git a/tests/compile_pip_requirements/BUILD.bazel b/tests/compile_pip_requirements/BUILD.bazel index ad5ee1a9d7..cadb59a3e8 100644 --- a/tests/compile_pip_requirements/BUILD.bazel +++ b/tests/compile_pip_requirements/BUILD.bazel @@ -25,10 +25,6 @@ compile_pip_requirements( "requirements.in", "requirements_extra.in", ], - extra_args = [ - "--allow-unsafe", - "--resolver=backtracking", - ], requirements_in = "requirements.txt", requirements_txt = "requirements_lock.txt", ) @@ -39,10 +35,6 @@ compile_pip_requirements( "requirements.in", "requirements_extra.in", ], - extra_args = [ - "--allow-unsafe", - "--resolver=backtracking", - ], generate_hashes = False, requirements_in = "requirements.txt", requirements_txt = "requirements_nohashes_lock.txt", @@ -67,10 +59,6 @@ compile_pip_requirements( "requirements_extra.in", "requirements_os_specific.in", ], - extra_args = [ - "--allow-unsafe", - "--resolver=backtracking", - ], requirements_darwin = "requirements_lock_darwin.txt", requirements_in = "requirements_os_specific.in", requirements_linux = "requirements_lock_linux.txt", diff --git a/tests/compile_pip_requirements/requirements_lock.txt b/tests/compile_pip_requirements/requirements_lock.txt index 4ca4a11f3e..5ce7d3bf71 100644 --- a/tests/compile_pip_requirements/requirements_lock.txt +++ b/tests/compile_pip_requirements/requirements_lock.txt @@ -4,6 +4,8 @@ # # bazel run //:requirements.update # + +# The following packages are considered to be unsafe in a requirements file: pip==22.3.1 \ --hash=sha256:65fd48317359f3af8e593943e6ae1506b66325085ea64b706a998c6e83eeaf38 \ --hash=sha256:908c78e6bc29b676ede1c4d57981d490cb892eb45cd8c214ab6298125119e077 diff --git a/tests/compile_pip_requirements/requirements_lock_darwin.txt b/tests/compile_pip_requirements/requirements_lock_darwin.txt index 7b580a2a03..0428dc05b2 100644 --- a/tests/compile_pip_requirements/requirements_lock_darwin.txt +++ b/tests/compile_pip_requirements/requirements_lock_darwin.txt @@ -4,6 +4,8 @@ # # bazel run //:os_specific_requirements.update # + +# The following packages are considered to be unsafe in a requirements file: pip==22.2.2 ; sys_platform == "darwin" \ --hash=sha256:3fd1929db052f056d7a998439176d3333fa1b3f6c1ad881de1885c0717608a4b \ --hash=sha256:b61a374b5bc40a6e982426aede40c9b5a08ff20e640f5b56977f4f91fed1e39a diff --git a/tests/compile_pip_requirements/requirements_lock_linux.txt b/tests/compile_pip_requirements/requirements_lock_linux.txt index 54eca176c5..37c4d49839 100644 --- a/tests/compile_pip_requirements/requirements_lock_linux.txt +++ b/tests/compile_pip_requirements/requirements_lock_linux.txt @@ -4,6 +4,8 @@ # # bazel run //:os_specific_requirements.update # + +# The following packages are considered to be unsafe in a requirements file: pip==22.3 ; sys_platform == "linux" \ --hash=sha256:1daab4b8d3b97d1d763caeb01a4640a2250a0ea899e257b1e44b9eded91e15ab \ --hash=sha256:8182aec21dad6c0a49a2a3d121a87cd524b950e0b6092b181625f07ebdde7530 diff --git a/tests/compile_pip_requirements/requirements_lock_windows.txt b/tests/compile_pip_requirements/requirements_lock_windows.txt index 5803d8e620..5f8e0faa6c 100644 --- a/tests/compile_pip_requirements/requirements_lock_windows.txt +++ b/tests/compile_pip_requirements/requirements_lock_windows.txt @@ -4,6 +4,8 @@ # # bazel run //:os_specific_requirements.update # + +# The following packages are considered to be unsafe in a requirements file: pip==22.2.1 ; sys_platform == "win32" \ --hash=sha256:0bbbc87dfbe6eed217beff0021f8b7dea04c8f4a0baa9d31dc4cff281ffc5b2b \ --hash=sha256:50516e47a2b79e77446f0d05649f0d53772c192571486236b1905492bfc24bac diff --git a/tests/compile_pip_requirements/requirements_nohashes_lock.txt b/tests/compile_pip_requirements/requirements_nohashes_lock.txt index 2b08a8eb6c..f6f0d86ceb 100644 --- a/tests/compile_pip_requirements/requirements_nohashes_lock.txt +++ b/tests/compile_pip_requirements/requirements_nohashes_lock.txt @@ -4,6 +4,8 @@ # # bazel run //:requirements_nohashes.update # + +# The following packages are considered to be unsafe in a requirements file: pip==22.3.1 # via -r requirements.in setuptools==65.6.3 diff --git a/tests/entry_points/BUILD.bazel b/tests/entry_points/BUILD.bazel new file mode 100644 index 0000000000..7a22d3c92b --- /dev/null +++ b/tests/entry_points/BUILD.bazel @@ -0,0 +1,39 @@ +# 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. + +load("@bazel_skylib//rules:build_test.bzl", "build_test") +load("//python:py_test.bzl", "py_test") +load(":simple_macro.bzl", "py_console_script_binary_in_a_macro") + +py_test( + name = "py_console_script_gen_test", + srcs = ["py_console_script_gen_test.py"], + main = "py_console_script_gen_test.py", + visibility = ["//visibility:private"], + deps = [ + "//python/private:py_console_script_gen_lib", + ], +) + +py_console_script_binary_in_a_macro( + name = "twine", + pkg = "@publish_deps_twine//:pkg", +) + +build_test( + name = "build_entry_point", + targets = [ + ":twine", + ], +) diff --git a/tests/entry_points/py_console_script_gen_test.py b/tests/entry_points/py_console_script_gen_test.py new file mode 100644 index 0000000000..80b5f20bde --- /dev/null +++ b/tests/entry_points/py_console_script_gen_test.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# 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. + +import pathlib +import tempfile +import textwrap +import unittest + +from python.private.py_console_script_gen import run + + +class RunTest(unittest.TestCase): + def setUp(self): + self.maxDiff = None + + def test_no_console_scripts_error(self): + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = pathlib.Path(tmpdir) + outfile = tmpdir / "out.py" + given_contents = ( + textwrap.dedent( + """ + [non_console_scripts] + foo = foo.bar:fizz + """ + ).strip() + + "\n" + ) + entry_points = tmpdir / "entry_points.txt" + entry_points.write_text(given_contents) + + with self.assertRaises(RuntimeError) as cm: + run( + entry_points=entry_points, + out=outfile, + console_script=None, + console_script_guess="", + ) + + self.assertEqual( + "The package does not provide any console_scripts in it's entry_points.txt", + cm.exception.args[0], + ) + + def test_no_entry_point_selected_error(self): + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = pathlib.Path(tmpdir) + outfile = tmpdir / "out.py" + given_contents = ( + textwrap.dedent( + """ + [console_scripts] + foo = foo.bar:fizz + """ + ).strip() + + "\n" + ) + entry_points = tmpdir / "entry_points.txt" + entry_points.write_text(given_contents) + + with self.assertRaises(RuntimeError) as cm: + run( + entry_points=entry_points, + out=outfile, + console_script=None, + console_script_guess="bar-baz", + ) + + self.assertEqual( + "Tried to guess that you wanted 'bar-baz', but could not find it. Please select one of the following console scripts: foo", + cm.exception.args[0], + ) + + def test_incorrect_entry_point(self): + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = pathlib.Path(tmpdir) + outfile = tmpdir / "out.py" + given_contents = ( + textwrap.dedent( + """ + [console_scripts] + foo = foo.bar:fizz + bar = foo.bar:buzz + """ + ).strip() + + "\n" + ) + entry_points = tmpdir / "entry_points.txt" + entry_points.write_text(given_contents) + + with self.assertRaises(RuntimeError) as cm: + run( + entry_points=entry_points, + out=outfile, + console_script="baz", + console_script_guess="", + ) + + self.assertEqual( + "The console_script 'baz' was not found, only the following are available: bar, foo", + cm.exception.args[0], + ) + + def test_a_single_entry_point(self): + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = pathlib.Path(tmpdir) + given_contents = ( + textwrap.dedent( + """ + [console_scripts] + foo = foo.bar:baz + """ + ).strip() + + "\n" + ) + entry_points = tmpdir / "entry_points.txt" + entry_points.write_text(given_contents) + out = tmpdir / "foo.py" + + run( + entry_points=entry_points, + out=out, + console_script=None, + console_script_guess="foo", + ) + + got = out.read_text() + + want = textwrap.dedent( + """\ + import sys + + # See @rules_python//python/private:py_console_script_gen.py for explanation + if getattr(sys.flags, "safe_path", False): + # We are running on Python 3.11 and we don't need this workaround + pass + elif ".runfiles" not in sys.path[0]: + sys.path = sys.path[1:] + + try: + from foo.bar import baz + except ImportError: + entries = "\\n".join(sys.path) + print("Printing sys.path entries for easier debugging:", file=sys.stderr) + print(f"sys.path is:\\n{entries}", file=sys.stderr) + raise + + if __name__ == "__main__": + sys.exit(baz()) + """ + ) + self.assertEqual(want, got) + + def test_a_second_entry_point_class_method(self): + with tempfile.TemporaryDirectory() as tmpdir: + tmpdir = pathlib.Path(tmpdir) + given_contents = ( + textwrap.dedent( + """ + [console_scripts] + foo = foo.bar:Bar.baz + bar = foo.baz:Bar.baz + """ + ).strip() + + "\n" + ) + entry_points = tmpdir / "entry_points.txt" + entry_points.write_text(given_contents) + out = tmpdir / "out.py" + + run( + entry_points=entry_points, + out=out, + console_script="bar", + console_script_guess="", + ) + + got = out.read_text() + + self.assertRegex(got, "from foo\.baz import Bar") + self.assertRegex(got, "sys\.exit\(Bar\.baz\(\)\)") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/entry_points/simple_macro.bzl b/tests/entry_points/simple_macro.bzl new file mode 100644 index 0000000000..4764a3ff2e --- /dev/null +++ b/tests/entry_points/simple_macro.bzl @@ -0,0 +1,31 @@ +# 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. + +""" +A simple test macro. +""" + +load("//python/entry_points:py_console_script_binary.bzl", "py_console_script_binary") + +def py_console_script_binary_in_a_macro(name, pkg): + """A simple macro to see that we can use our macro in a macro. + + Args: + name, str: the name of the target + pkg, str: the pkg target + """ + py_console_script_binary( + name = name, + pkg = Label(pkg), + ) diff --git a/tests/ignore_root_user_error/WORKSPACE b/tests/ignore_root_user_error/WORKSPACE index e0528e4047..d1249feab2 100644 --- a/tests/ignore_root_user_error/WORKSPACE +++ b/tests/ignore_root_user_error/WORKSPACE @@ -3,7 +3,9 @@ local_repository( path = "../..", ) -load("@rules_python//python:repositories.bzl", "python_register_toolchains") +load("@rules_python//python:repositories.bzl", "py_repositories", "python_register_toolchains") + +py_repositories() python_register_toolchains( name = "python39", diff --git a/tests/pip_repository_entry_points/BUILD.bazel b/tests/pip_repository_entry_points/BUILD.bazel index 81c01c316c..2e2e2dcf99 100644 --- a/tests/pip_repository_entry_points/BUILD.bazel +++ b/tests/pip_repository_entry_points/BUILD.bazel @@ -6,7 +6,6 @@ load("@rules_python//python:pip.bzl", "compile_pip_requirements") # This rule adds a convenient way to update the requirements file. compile_pip_requirements( name = "requirements", - extra_args = ["--allow-unsafe"], requirements_windows = ":requirements_windows.txt", ) diff --git a/tests/pip_repository_entry_points/requirements.in b/tests/pip_repository_entry_points/requirements.in index 2cc4625577..7f999c8837 100644 --- a/tests/pip_repository_entry_points/requirements.in +++ b/tests/pip_repository_entry_points/requirements.in @@ -1,5 +1,8 @@ sphinx==4.3.2 yamllint>=1.28.0 -# Last avialable for ubuntu python3.6 +# Last available for Ubuntu python3.6 setuptools==59.6.0 + +certifi>=2023.7.22 # https://security.snyk.io/vuln/SNYK-PYTHON-CERTIFI-5805047 + diff --git a/tests/pip_repository_entry_points/requirements.txt b/tests/pip_repository_entry_points/requirements.txt index a93facc03b..d663c358f3 100644 --- a/tests/pip_repository_entry_points/requirements.txt +++ b/tests/pip_repository_entry_points/requirements.txt @@ -12,10 +12,12 @@ babel==2.9.1 \ --hash=sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9 \ --hash=sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0 # via sphinx -certifi==2021.10.8 \ - --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \ - --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 - # via requests +certifi==2023.7.22 \ + --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ + --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 + # via + # -r requirements.in + # requests charset-normalizer==2.0.10 \ --hash=sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd \ --hash=sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455 @@ -166,13 +168,6 @@ requests==2.27.1 \ --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 \ --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d # via sphinx -setuptools==59.6.0 \ - --hash=sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373 \ - --hash=sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e - # via - # -r requirements.in - # sphinx - # yamllint snowballstemmer==2.2.0 \ --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a @@ -213,3 +208,12 @@ yamllint==1.28.0 \ --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b # via -r requirements.in + +# The following packages are considered to be unsafe in a requirements file: +setuptools==59.6.0 \ + --hash=sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373 \ + --hash=sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e + # via + # -r requirements.in + # sphinx + # yamllint diff --git a/tests/pip_repository_entry_points/requirements_windows.txt b/tests/pip_repository_entry_points/requirements_windows.txt index 651e2b5e56..fc5779bebd 100644 --- a/tests/pip_repository_entry_points/requirements_windows.txt +++ b/tests/pip_repository_entry_points/requirements_windows.txt @@ -12,10 +12,12 @@ babel==2.9.1 \ --hash=sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9 \ --hash=sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0 # via sphinx -certifi==2021.10.8 \ - --hash=sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872 \ - --hash=sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569 - # via requests +certifi==2023.7.22 \ + --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \ + --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9 + # via + # -r requirements.in + # requests charset-normalizer==2.0.10 \ --hash=sha256:876d180e9d7432c5d1dfd4c5d26b72f099d503e8fcc0feb7532c9289be60fcbd \ --hash=sha256:cb957888737fc0bbcd78e3df769addb41fd1ff8cf950dc9e7ad7793f1bf44455 @@ -170,13 +172,6 @@ requests==2.27.1 \ --hash=sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61 \ --hash=sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d # via sphinx -setuptools==59.6.0 \ - --hash=sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373 \ - --hash=sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e - # via - # -r requirements.in - # sphinx - # yamllint snowballstemmer==2.2.0 \ --hash=sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1 \ --hash=sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a @@ -217,3 +212,12 @@ yamllint==1.28.0 \ --hash=sha256:89bb5b5ac33b1ade059743cf227de73daa34d5e5a474b06a5e17fc16583b0cf2 \ --hash=sha256:9e3d8ddd16d0583214c5fdffe806c9344086721f107435f68bad990e5a88826b # via -r requirements.in + +# The following packages are considered to be unsafe in a requirements file: +setuptools==59.6.0 \ + --hash=sha256:22c7348c6d2976a52632c67f7ab0cdf40147db7789f9aed18734643fe9cf3373 \ + --hash=sha256:4ce92f1e1f8f01233ee9952c04f6b81d1e02939d6e1b488428154974a4d0783e + # via + # -r requirements.in + # sphinx + # yamllint diff --git a/tests/py_runtime/BUILD.bazel b/tests/py_runtime/BUILD.bazel new file mode 100644 index 0000000000..e097f0df08 --- /dev/null +++ b/tests/py_runtime/BUILD.bazel @@ -0,0 +1,17 @@ +# 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. + +load(":py_runtime_tests.bzl", "py_runtime_test_suite") + +py_runtime_test_suite(name = "py_runtime_tests") diff --git a/tests/py_runtime/py_runtime_tests.bzl b/tests/py_runtime/py_runtime_tests.bzl new file mode 100644 index 0000000000..662909cca2 --- /dev/null +++ b/tests/py_runtime/py_runtime_tests.bzl @@ -0,0 +1,262 @@ +# 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. +"""Starlark tests for py_runtime rule.""" + +load("@rules_python_internal//:rules_python_config.bzl", "config") +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:truth.bzl", "matching") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python:py_runtime.bzl", "py_runtime") +load("//python:py_runtime_info.bzl", "PyRuntimeInfo") +load("//tests:py_runtime_info_subject.bzl", "py_runtime_info_subject") +load("//tests/base_rules:util.bzl", br_util = "util") + +_tests = [] + +_SKIP_TEST = { + "target_compatible_with": ["@platforms//:incompatible"], +} + +def _test_bootstrap_template(name): + # The bootstrap_template arg isn't present in older Bazel versions, so + # we have to conditionally pass the arg and mark the test incompatible. + if config.enable_pystar: + py_runtime_kwargs = {"bootstrap_template": "bootstrap.txt"} + attr_values = {} + else: + py_runtime_kwargs = {} + attr_values = _SKIP_TEST + + rt_util.helper_target( + py_runtime, + name = name + "_subject", + interpreter_path = "/py", + python_version = "PY3", + **py_runtime_kwargs + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_bootstrap_template_impl, + attr_values = attr_values, + ) + +def _test_bootstrap_template_impl(env, target): + env.expect.that_target(target).provider( + PyRuntimeInfo, + factory = py_runtime_info_subject, + ).bootstrap_template().path().contains("bootstrap.txt") + +_tests.append(_test_bootstrap_template) + +def _test_cannot_have_both_inbuild_and_system_interpreter(name): + if br_util.is_bazel_6_or_higher(): + py_runtime_kwargs = { + "interpreter": "fake_interpreter", + "interpreter_path": "/some/path", + } + attr_values = {} + else: + py_runtime_kwargs = { + "interpreter_path": "/some/path", + } + attr_values = _SKIP_TEST + rt_util.helper_target( + py_runtime, + name = name + "_subject", + python_version = "PY3", + **py_runtime_kwargs + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_cannot_have_both_inbuild_and_system_interpreter_impl, + expect_failure = True, + attr_values = attr_values, + ) + +def _test_cannot_have_both_inbuild_and_system_interpreter_impl(env, target): + env.expect.that_target(target).failures().contains_predicate( + matching.str_matches("one of*interpreter*interpreter_path"), + ) + +_tests.append(_test_cannot_have_both_inbuild_and_system_interpreter) + +def _test_cannot_specify_files_for_system_interpreter(name): + if br_util.is_bazel_6_or_higher(): + py_runtime_kwargs = {"files": ["foo.txt"]} + attr_values = {} + else: + py_runtime_kwargs = {} + attr_values = _SKIP_TEST + rt_util.helper_target( + py_runtime, + name = name + "_subject", + interpreter_path = "/foo", + python_version = "PY3", + **py_runtime_kwargs + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_cannot_specify_files_for_system_interpreter_impl, + expect_failure = True, + attr_values = attr_values, + ) + +def _test_cannot_specify_files_for_system_interpreter_impl(env, target): + env.expect.that_target(target).failures().contains_predicate( + matching.str_matches("files*must be empty"), + ) + +_tests.append(_test_cannot_specify_files_for_system_interpreter) + +def _test_in_build_interpreter(name): + rt_util.helper_target( + py_runtime, + name = name + "_subject", + interpreter = "fake_interpreter", + python_version = "PY3", + files = ["file1.txt"], + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_in_build_interpreter_impl, + ) + +def _test_in_build_interpreter_impl(env, target): + info = env.expect.that_target(target).provider(PyRuntimeInfo, factory = py_runtime_info_subject) + info.python_version().equals("PY3") + info.files().contains_predicate(matching.file_basename_equals("file1.txt")) + info.interpreter().path().contains("fake_interpreter") + +_tests.append(_test_in_build_interpreter) + +def _test_must_have_either_inbuild_or_system_interpreter(name): + if br_util.is_bazel_6_or_higher(): + py_runtime_kwargs = {} + attr_values = {} + else: + py_runtime_kwargs = { + "interpreter_path": "/some/path", + } + attr_values = _SKIP_TEST + rt_util.helper_target( + py_runtime, + name = name + "_subject", + python_version = "PY3", + **py_runtime_kwargs + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_must_have_either_inbuild_or_system_interpreter_impl, + expect_failure = True, + attr_values = attr_values, + ) + +def _test_must_have_either_inbuild_or_system_interpreter_impl(env, target): + env.expect.that_target(target).failures().contains_predicate( + matching.str_matches("one of*interpreter*interpreter_path"), + ) + +_tests.append(_test_must_have_either_inbuild_or_system_interpreter) + +def _test_python_version_required(name): + # Bazel 5.4 will entirely crash when python_version is missing. + if br_util.is_bazel_6_or_higher(): + py_runtime_kwargs = {} + attr_values = {} + else: + py_runtime_kwargs = {"python_version": "PY3"} + attr_values = _SKIP_TEST + rt_util.helper_target( + py_runtime, + name = name + "_subject", + interpreter_path = "/math/pi", + **py_runtime_kwargs + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_python_version_required_impl, + expect_failure = True, + attr_values = attr_values, + ) + +def _test_python_version_required_impl(env, target): + env.expect.that_target(target).failures().contains_predicate( + matching.str_matches("must be set*PY2*PY3"), + ) + +_tests.append(_test_python_version_required) + +def _test_system_interpreter(name): + rt_util.helper_target( + py_runtime, + name = name + "_subject", + interpreter_path = "/system/python", + python_version = "PY3", + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_system_interpreter_impl, + ) + +def _test_system_interpreter_impl(env, target): + env.expect.that_target(target).provider( + PyRuntimeInfo, + factory = py_runtime_info_subject, + ).interpreter_path().equals("/system/python") + +_tests.append(_test_system_interpreter) + +def _test_system_interpreter_must_be_absolute(name): + # Bazel 5.4 will entirely crash when an invalid interpreter_path + # is given. + if br_util.is_bazel_6_or_higher(): + py_runtime_kwargs = {"interpreter_path": "relative/path"} + attr_values = {} + else: + py_runtime_kwargs = {"interpreter_path": "/junk/value/for/bazel5.4"} + attr_values = _SKIP_TEST + rt_util.helper_target( + py_runtime, + name = name + "_subject", + python_version = "PY3", + **py_runtime_kwargs + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_system_interpreter_must_be_absolute_impl, + expect_failure = True, + attr_values = attr_values, + ) + +def _test_system_interpreter_must_be_absolute_impl(env, target): + env.expect.that_target(target).failures().contains_predicate( + matching.str_matches("must be*absolute"), + ) + +_tests.append(_test_system_interpreter_must_be_absolute) + +def py_runtime_test_suite(name): + test_suite( + name = name, + tests = _tests, + ) diff --git a/tests/py_runtime_info_subject.bzl b/tests/py_runtime_info_subject.bzl new file mode 100644 index 0000000000..9f42d3a839 --- /dev/null +++ b/tests/py_runtime_info_subject.bzl @@ -0,0 +1,101 @@ +# 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. +"""PyRuntimeInfo testing subject.""" + +load("@rules_testing//lib:truth.bzl", "subjects") + +def py_runtime_info_subject(info, *, meta): + """Creates a new `PyRuntimeInfoSubject` for a PyRuntimeInfo provider instance. + + Method: PyRuntimeInfoSubject.new + + Args: + info: The PyRuntimeInfo object + meta: ExpectMeta object. + + Returns: + A `PyRuntimeInfoSubject` struct + """ + + # buildifier: disable=uninitialized + public = struct( + # go/keep-sorted start + bootstrap_template = lambda *a, **k: _py_runtime_info_subject_bootstrap_template(self, *a, **k), + coverage_files = lambda *a, **k: _py_runtime_info_subject_coverage_files(self, *a, **k), + coverage_tool = lambda *a, **k: _py_runtime_info_subject_coverage_tool(self, *a, **k), + files = lambda *a, **k: _py_runtime_info_subject_files(self, *a, **k), + interpreter = lambda *a, **k: _py_runtime_info_subject_interpreter(self, *a, **k), + interpreter_path = lambda *a, **k: _py_runtime_info_subject_interpreter_path(self, *a, **k), + python_version = lambda *a, **k: _py_runtime_info_subject_python_version(self, *a, **k), + stub_shebang = lambda *a, **k: _py_runtime_info_subject_stub_shebang(self, *a, **k), + # go/keep-sorted end + ) + self = struct( + actual = info, + meta = meta, + ) + return public + +def _py_runtime_info_subject_bootstrap_template(self): + return subjects.file( + self.actual.bootstrap_template, + meta = self.meta.derive("bootstrap_template()"), + ) + +def _py_runtime_info_subject_coverage_files(self): + """Returns a `DepsetFileSubject` for the `coverage_files` attribute. + + Args: + self: implicitly added. + """ + return subjects.depset_file( + self.actual.coverage_files, + meta = self.meta.derive("coverage_files()"), + ) + +def _py_runtime_info_subject_coverage_tool(self): + return subjects.file( + self.actual.coverage_tool, + meta = self.meta.derive("coverage_tool()"), + ) + +def _py_runtime_info_subject_files(self): + return subjects.depset_file( + self.actual.files, + meta = self.meta.derive("files()"), + ) + +def _py_runtime_info_subject_interpreter(self): + return subjects.file( + self.actual.interpreter, + meta = self.meta.derive("interpreter()"), + ) + +def _py_runtime_info_subject_interpreter_path(self): + return subjects.str( + self.actual.interpreter_path, + meta = self.meta.derive("interpreter_path()"), + ) + +def _py_runtime_info_subject_python_version(self): + return subjects.str( + self.actual.python_version, + meta = self.meta.derive("python_version()"), + ) + +def _py_runtime_info_subject_stub_shebang(self): + return subjects.str( + self.actual.stub_shebang, + meta = self.meta.derive("stub_shebang()"), + ) diff --git a/tests/py_runtime_pair/BUILD.bazel b/tests/py_runtime_pair/BUILD.bazel new file mode 100644 index 0000000000..6a6a4b91f0 --- /dev/null +++ b/tests/py_runtime_pair/BUILD.bazel @@ -0,0 +1,17 @@ +# 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. + +load(":py_runtime_pair_tests.bzl", "py_runtime_pair_test_suite") + +py_runtime_pair_test_suite(name = "py_runtime_pair_tests") diff --git a/tests/py_runtime_pair/py_runtime_pair_tests.bzl b/tests/py_runtime_pair/py_runtime_pair_tests.bzl new file mode 100644 index 0000000000..e1ff19ee3a --- /dev/null +++ b/tests/py_runtime_pair/py_runtime_pair_tests.bzl @@ -0,0 +1,66 @@ +# 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. +"""Starlark tests for py_runtime_pair rule.""" + +load("@rules_testing//lib:analysis_test.bzl", "analysis_test") +load("@rules_testing//lib:test_suite.bzl", "test_suite") +load("@rules_testing//lib:truth.bzl", "matching", "subjects") +load("@rules_testing//lib:util.bzl", rt_util = "util") +load("//python:py_runtime.bzl", "py_runtime") +load("//python:py_runtime_pair.bzl", "py_runtime_pair") +load("//tests:py_runtime_info_subject.bzl", "py_runtime_info_subject") + +_tests = [] + +def _test_basic(name): + rt_util.helper_target( + py_runtime, + name = name + "_runtime", + interpreter = "fake_interpreter", + python_version = "PY3", + files = ["file1.txt"], + ) + rt_util.helper_target( + py_runtime_pair, + name = name + "_subject", + py3_runtime = name + "_runtime", + ) + analysis_test( + name = name, + target = name + "_subject", + impl = _test_basic_impl, + ) + +def _test_basic_impl(env, target): + toolchain = env.expect.that_target(target).provider( + platform_common.ToolchainInfo, + factory = lambda value, meta: subjects.struct( + value, + meta = meta, + attrs = { + "py3_runtime": py_runtime_info_subject, + }, + ), + ) + toolchain.py3_runtime().python_version().equals("PY3") + toolchain.py3_runtime().files().contains_predicate(matching.file_basename_equals("file1.txt")) + toolchain.py3_runtime().interpreter().path().contains("fake_interpreter") + +_tests.append(_test_basic) + +def py_runtime_pair_test_suite(name): + test_suite( + name = name, + tests = _tests, + ) diff --git a/tools/build_defs/python/tests/py_wheel/BUILD.bazel b/tests/py_wheel/py_wheel/BUILD.bazel similarity index 100% rename from tools/build_defs/python/tests/py_wheel/BUILD.bazel rename to tests/py_wheel/py_wheel/BUILD.bazel diff --git a/tools/build_defs/python/tests/py_wheel/py_wheel_tests.bzl b/tests/py_wheel/py_wheel/py_wheel_tests.bzl similarity index 94% rename from tools/build_defs/python/tests/py_wheel/py_wheel_tests.bzl rename to tests/py_wheel/py_wheel/py_wheel_tests.bzl index 4408592d32..c70163ef37 100644 --- a/tools/build_defs/python/tests/py_wheel/py_wheel_tests.bzl +++ b/tests/py_wheel/py_wheel/py_wheel_tests.bzl @@ -4,7 +4,7 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test") load("@rules_testing//lib:truth.bzl", "matching") load("@rules_testing//lib:util.bzl", rt_util = "util") load("//python:packaging.bzl", "py_wheel") -load("//tools/build_defs/python/tests:util.bzl", pt_util = "util") +load("//tests/base_rules:util.bzl", pt_util = "util") _tests = [] diff --git a/tests/py_wheel/py_wheel_tests.bzl b/tests/py_wheel/py_wheel_tests.bzl index e580732aac..3c03a1b8e4 100644 --- a/tests/py_wheel/py_wheel_tests.bzl +++ b/tests/py_wheel/py_wheel_tests.bzl @@ -16,7 +16,9 @@ load("@rules_testing//lib:analysis_test.bzl", "analysis_test", "test_suite") load("@rules_testing//lib:util.bzl", rt_util = "util") load("//python:packaging.bzl", "py_wheel") +load("//python/private:py_wheel_normalize_pep440.bzl", "normalize_pep440") # buildifier: disable=bzl-visibility +_basic_tests = [] _tests = [] def _test_metadata(name): @@ -92,8 +94,109 @@ def _test_content_type_from_description_impl(env, target): _tests.append(_test_content_type_from_description) +def _test_pep440_normalization(env): + prefixes = ["v", " v", " \t\r\nv"] + epochs = { + "": ["", "0!", "00!"], + "1!": ["1!", "001!"], + "200!": ["200!", "00200!"], + } + releases = { + "0.1": ["0.1", "0.01"], + "2023.7.19": ["2023.7.19", "2023.07.19"], + } + pres = { + "": [""], + "a0": ["a", ".a", "-ALPHA0", "_alpha0", ".a0"], + "a4": ["alpha4", ".a04"], + "b0": ["b", ".b", "-BETA0", "_beta0", ".b0"], + "b5": ["beta05", ".b5"], + "rc0": ["C", "_c0", "RC", "_rc0", "-preview_0"], + } + explicit_posts = { + "": [""], + ".post0": [], + ".post1": [".post1", "-r1", "_rev1"], + } + implicit_posts = [[".post1", "-1"], [".post2", "-2"]] + devs = { + "": [""], + ".dev0": ["dev", "-DEV", "_Dev-0"], + ".dev9": ["DEV9", ".dev09", ".dev9"], + ".dev{BUILD_TIMESTAMP}": [ + "-DEV{BUILD_TIMESTAMP}", + "_dev_{BUILD_TIMESTAMP}", + ], + } + locals = { + "": [""], + "+ubuntu.7": ["+Ubuntu_7", "+ubuntu-007"], + "+ubuntu.r007": ["+Ubuntu_R007"], + } + epochs = [ + [normalized_epoch, input_epoch] + for normalized_epoch, input_epochs in epochs.items() + for input_epoch in input_epochs + ] + releases = [ + [normalized_release, input_release] + for normalized_release, input_releases in releases.items() + for input_release in input_releases + ] + pres = [ + [normalized_pre, input_pre] + for normalized_pre, input_pres in pres.items() + for input_pre in input_pres + ] + explicit_posts = [ + [normalized_post, input_post] + for normalized_post, input_posts in explicit_posts.items() + for input_post in input_posts + ] + pres_and_posts = [ + [normalized_pre + normalized_post, input_pre + input_post] + for normalized_pre, input_pre in pres + for normalized_post, input_post in explicit_posts + ] + [ + [normalized_pre + normalized_post, input_pre + input_post] + for normalized_pre, input_pre in pres + for normalized_post, input_post in implicit_posts + if input_pre == "" or input_pre[-1].isdigit() + ] + devs = [ + [normalized_dev, input_dev] + for normalized_dev, input_devs in devs.items() + for input_dev in input_devs + ] + locals = [ + [normalized_local, input_local] + for normalized_local, input_locals in locals.items() + for input_local in input_locals + ] + postfixes = ["", " ", " \t\r\n"] + i = 0 + for nepoch, iepoch in epochs: + for nrelease, irelease in releases: + for nprepost, iprepost in pres_and_posts: + for ndev, idev in devs: + for nlocal, ilocal in locals: + prefix = prefixes[i % len(prefixes)] + postfix = postfixes[(i // len(prefixes)) % len(postfixes)] + env.expect.that_str( + normalize_pep440( + prefix + iepoch + irelease + iprepost + + idev + ilocal + postfix, + ), + ).equals( + nepoch + nrelease + nprepost + ndev + nlocal, + ) + i += 1 + +_basic_tests.append(_test_pep440_normalization) + def py_wheel_test_suite(name): test_suite( name = name, + basic_tests = _basic_tests, tests = _tests, ) diff --git a/tests/pycross/BUILD.bazel b/tests/pycross/BUILD.bazel new file mode 100644 index 0000000000..4f01272b7c --- /dev/null +++ b/tests/pycross/BUILD.bazel @@ -0,0 +1,34 @@ +# 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. + +load("//python:defs.bzl", "py_test") +load("//third_party/rules_pycross/pycross/private:wheel_library.bzl", "py_wheel_library") # buildifier: disable=bzl-visibility + +py_wheel_library( + name = "extracted_wheel_for_testing", + wheel = "@wheel_for_testing//file", +) + +py_test( + name = "py_wheel_library_test", + srcs = [ + "py_wheel_library_test.py", + ], + data = [ + ":extracted_wheel_for_testing", + ], + deps = [ + "//python/runfiles", + ], +) diff --git a/tests/pycross/py_wheel_library_test.py b/tests/pycross/py_wheel_library_test.py new file mode 100644 index 0000000000..fa8e20e563 --- /dev/null +++ b/tests/pycross/py_wheel_library_test.py @@ -0,0 +1,48 @@ +# 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. + +import unittest +from pathlib import Path + +from python.runfiles import runfiles + +RUNFILES = runfiles.Create() + + +class TestPyWheelLibrary(unittest.TestCase): + def setUp(self): + self.extraction_dir = Path( + RUNFILES.Rlocation( + "rules_python/tests/pycross/extracted_wheel_for_testing" + ) + ) + self.assertTrue(self.extraction_dir.exists(), self.extraction_dir) + self.assertTrue(self.extraction_dir.is_dir(), self.extraction_dir) + + def test_file_presence(self): + """Validate that the basic file layout looks good.""" + for path in ( + "bin/f2py", + "site-packages/numpy.libs/libgfortran-daac5196.so.5.0.0", + "site-packages/numpy/dtypes.py", + "site-packages/numpy/core/_umath_tests.cpython-311-aarch64-linux-gnu.so", + ): + print(self.extraction_dir / path) + self.assertTrue( + (self.extraction_dir / path).exists(), f"{path} does not exist" + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/support/BUILD.bazel b/tests/support/BUILD.bazel new file mode 100644 index 0000000000..316e9abbf1 --- /dev/null +++ b/tests/support/BUILD.bazel @@ -0,0 +1,39 @@ +# 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. + +# ==================== +# NOTE: You probably want to use the constants in test_platforms.bzl +# Otherwise, you'll probably have to manually call Label() on these targets +# to force them to resolve in the proper context. +# ==================== +platform( + name = "mac", + constraint_values = [ + "@platforms//os:macos", + ], +) + +platform( + name = "linux", + constraint_values = [ + "@platforms//os:linux", + ], +) + +platform( + name = "windows", + constraint_values = [ + "@platforms//os:windows", + ], +) diff --git a/tests/support/test_platforms.bzl b/tests/support/test_platforms.bzl new file mode 100644 index 0000000000..3ff3c507fc --- /dev/null +++ b/tests/support/test_platforms.bzl @@ -0,0 +1,20 @@ +# 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. +"""Constants for referring to platforms.""" + +# Explicit Label() calls are required so that it resolves in @rules_python +# context instead of e.g. the @rules_testing context. +MAC = Label("//tests/support:mac") +LINUX = Label("//tests/support:linux") +WINDOWS = Label("//tests/support:windows") diff --git a/third_party/rules_pycross/LICENSE b/third_party/rules_pycross/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/third_party/rules_pycross/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/third_party/rules_pycross/pycross/private/BUILD.bazel b/third_party/rules_pycross/pycross/private/BUILD.bazel new file mode 100644 index 0000000000..f59b087027 --- /dev/null +++ b/third_party/rules_pycross/pycross/private/BUILD.bazel @@ -0,0 +1,14 @@ +# Copyright 2023 Jeremy Volkman. All rights reserved. +# 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. diff --git a/third_party/rules_pycross/pycross/private/providers.bzl b/third_party/rules_pycross/pycross/private/providers.bzl new file mode 100644 index 0000000000..47fc9f7271 --- /dev/null +++ b/third_party/rules_pycross/pycross/private/providers.bzl @@ -0,0 +1,32 @@ +# Copyright 2023 Jeremy Volkman. All rights reserved. +# 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. + +"""Python providers.""" + +PyWheelInfo = provider( + doc = "Information about a Python wheel.", + fields = { + "name_file": "File: A file containing the canonical name of the wheel.", + "wheel_file": "File: The wheel file itself.", + }, +) + +PyTargetEnvironmentInfo = provider( + doc = "A target environment description.", + fields = { + "file": "The JSON file containing target environment information.", + "python_compatible_with": "A list of constraints used to select this platform.", + }, +) diff --git a/third_party/rules_pycross/pycross/private/tools/BUILD.bazel b/third_party/rules_pycross/pycross/private/tools/BUILD.bazel new file mode 100644 index 0000000000..a87e6aa67e --- /dev/null +++ b/third_party/rules_pycross/pycross/private/tools/BUILD.bazel @@ -0,0 +1,26 @@ +# Copyright 2023 Jeremy Volkman. All rights reserved. +# 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. + +load("//python:defs.bzl", "py_binary") + +py_binary( + name = "wheel_installer", + srcs = ["wheel_installer.py"], + visibility = ["//visibility:public"], + deps = [ + "//python/pip_install/tools/wheel_installer:lib", + "@pypi__installer//:lib", + ], +) diff --git a/third_party/rules_pycross/pycross/private/tools/wheel_installer.py b/third_party/rules_pycross/pycross/private/tools/wheel_installer.py new file mode 100644 index 0000000000..8367f08d41 --- /dev/null +++ b/third_party/rules_pycross/pycross/private/tools/wheel_installer.py @@ -0,0 +1,138 @@ +# Copyright 2023 Jeremy Volkman. All rights reserved. +# 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. + +""" +A tool that invokes pypa/build to build the given sdist tarball. +""" + +import argparse +import os +import shutil +import sys +import tempfile +from pathlib import Path +from typing import Any + +from installer import install +from installer.destinations import SchemeDictionaryDestination +from installer.sources import WheelFile + +from python.pip_install.tools.wheel_installer import namespace_pkgs + + +def setup_namespace_pkg_compatibility(wheel_dir: Path) -> None: + """Converts native namespace packages to pkgutil-style packages + + Namespace packages can be created in one of three ways. They are detailed here: + https://packaging.python.org/guides/packaging-namespace-packages/#creating-a-namespace-package + + 'pkgutil-style namespace packages' (2) and 'pkg_resources-style namespace packages' (3) works in Bazel, but + 'native namespace packages' (1) do not. + + We ensure compatibility with Bazel of method 1 by converting them into method 2. + + Args: + wheel_dir: the directory of the wheel to convert + """ + + namespace_pkg_dirs = namespace_pkgs.implicit_namespace_packages( + str(wheel_dir), + ignored_dirnames=["%s/bin" % wheel_dir], + ) + + for ns_pkg_dir in namespace_pkg_dirs: + namespace_pkgs.add_pkgutil_style_namespace_pkg_init(ns_pkg_dir) + + +def main(args: Any) -> None: + dest_dir = args.directory + lib_dir = dest_dir / "site-packages" + destination = SchemeDictionaryDestination( + scheme_dict={ + "platlib": str(lib_dir), + "purelib": str(lib_dir), + "headers": str(dest_dir / "include"), + "scripts": str(dest_dir / "bin"), + "data": str(dest_dir / "data"), + }, + interpreter="/usr/bin/env python3", # Generic; it's not feasible to run these scripts directly. + script_kind="posix", + bytecode_optimization_levels=[0, 1], + ) + + link_dir = Path(tempfile.mkdtemp()) + if args.wheel_name_file: + with open(args.wheel_name_file, "r") as f: + wheel_name = f.read().strip() + else: + wheel_name = os.path.basename(args.wheel) + + link_path = link_dir / wheel_name + os.symlink(os.path.join(os.getcwd(), args.wheel), link_path) + + try: + with WheelFile.open(link_path) as source: + install( + source=source, + destination=destination, + # Additional metadata that is generated by the installation tool. + additional_metadata={ + "INSTALLER": b"https://github.com/bazelbuild/rules_python/tree/main/third_party/rules_pycross", + }, + ) + finally: + shutil.rmtree(link_dir, ignore_errors=True) + + setup_namespace_pkg_compatibility(lib_dir) + + +def parse_flags(argv) -> Any: + parser = argparse.ArgumentParser(description="Extract a Python wheel.") + + parser.add_argument( + "--wheel", + type=Path, + required=True, + help="The wheel file path.", + ) + + parser.add_argument( + "--wheel-name-file", + type=Path, + required=False, + help="A file containing the canonical name of the wheel.", + ) + + parser.add_argument( + "--enable-implicit-namespace-pkgs", + action="store_true", + help="If true, disables conversion of implicit namespace packages and will unzip as-is.", + ) + + parser.add_argument( + "--directory", + type=Path, + help="The output path.", + ) + + return parser.parse_args(argv[1:]) + + +if __name__ == "__main__": + # When under `bazel run`, change to the actual working dir. + if "BUILD_WORKING_DIRECTORY" in os.environ: + os.chdir(os.environ["BUILD_WORKING_DIRECTORY"]) + + main(parse_flags(sys.argv)) diff --git a/third_party/rules_pycross/pycross/private/wheel_library.bzl b/third_party/rules_pycross/pycross/private/wheel_library.bzl new file mode 100644 index 0000000000..381511a2f1 --- /dev/null +++ b/third_party/rules_pycross/pycross/private/wheel_library.bzl @@ -0,0 +1,137 @@ +# Copyright 2023 Jeremy Volkman. All rights reserved. +# 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. + +"""Implementation of the py_wheel_library rule.""" + +load("@bazel_skylib//lib:paths.bzl", "paths") +load("//python:defs.bzl", "PyInfo") +load(":providers.bzl", "PyWheelInfo") + +def _py_wheel_library_impl(ctx): + out = ctx.actions.declare_directory(ctx.attr.name) + + wheel_target = ctx.attr.wheel + if PyWheelInfo in wheel_target: + wheel_file = wheel_target[PyWheelInfo].wheel_file + name_file = wheel_target[PyWheelInfo].name_file + else: + wheel_file = ctx.file.wheel + name_file = None + + args = ctx.actions.args().use_param_file("--flagfile=%s") + args.add("--wheel", wheel_file) + args.add("--directory", out.path) + + inputs = [wheel_file] + if name_file: + inputs.append(name_file) + args.add("--wheel-name-file", name_file) + + if ctx.attr.enable_implicit_namespace_pkgs: + args.add("--enable-implicit-namespace-pkgs") + + ctx.actions.run( + inputs = inputs, + outputs = [out], + executable = ctx.executable._tool, + arguments = [args], + # Set environment variables to make generated .pyc files reproducible. + env = { + "PYTHONHASHSEED": "0", + "SOURCE_DATE_EPOCH": "315532800", + }, + mnemonic = "WheelInstall", + progress_message = "Installing %s" % ctx.file.wheel.basename, + ) + + has_py2_only_sources = ctx.attr.python_version == "PY2" + has_py3_only_sources = ctx.attr.python_version == "PY3" + if not has_py2_only_sources: + for d in ctx.attr.deps: + if d[PyInfo].has_py2_only_sources: + has_py2_only_sources = True + break + if not has_py3_only_sources: + for d in ctx.attr.deps: + if d[PyInfo].has_py3_only_sources: + has_py3_only_sources = True + break + + # TODO: Is there a more correct way to get this runfiles-relative import path? + imp = paths.join( + ctx.label.workspace_name or ctx.workspace_name, # Default to the local workspace. + ctx.label.package, + ctx.label.name, + "site-packages", # we put lib files in this subdirectory. + ) + + imports = depset( + direct = [imp], + transitive = [d[PyInfo].imports for d in ctx.attr.deps], + ) + transitive_sources = depset( + direct = [out], + transitive = [dep[PyInfo].transitive_sources for dep in ctx.attr.deps if PyInfo in dep], + ) + runfiles = ctx.runfiles(files = [out]) + for d in ctx.attr.deps: + runfiles = runfiles.merge(d[DefaultInfo].default_runfiles) + + return [ + DefaultInfo( + files = depset(direct = [out]), + runfiles = runfiles, + ), + PyInfo( + has_py2_only_sources = has_py2_only_sources, + has_py3_only_sources = has_py3_only_sources, + imports = imports, + transitive_sources = transitive_sources, + uses_shared_libraries = True, # Docs say this is unused + ), + ] + +py_wheel_library = rule( + implementation = _py_wheel_library_impl, + attrs = { + "deps": attr.label_list( + doc = "A list of this wheel's Python library dependencies.", + providers = [DefaultInfo, PyInfo], + ), + "enable_implicit_namespace_pkgs": attr.bool( + default = True, + doc = """ +If true, disables conversion of native namespace packages into pkg-util style namespace packages. When set all py_binary +and py_test targets must specify either `legacy_create_init=False` or the global Bazel option +`--incompatible_default_to_explicit_init_py` to prevent `__init__.py` being automatically generated in every directory. +This option is required to support some packages which cannot handle the conversion to pkg-util style. + """, + ), + "python_version": attr.string( + doc = "The python version required for this wheel ('PY2' or 'PY3')", + values = ["PY2", "PY3", ""], + ), + "wheel": attr.label( + doc = "The wheel file.", + allow_single_file = [".whl"], + mandatory = True, + ), + "_tool": attr.label( + default = Label("//third_party/rules_pycross/pycross/private/tools:wheel_installer"), + cfg = "exec", + executable = True, + ), + }, +) diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel index fd951d9086..51bd56df0a 100644 --- a/tools/BUILD.bazel +++ b/tools/BUILD.bazel @@ -21,6 +21,7 @@ licenses(["notice"]) py_binary( name = "wheelmaker", srcs = ["wheelmaker.py"], + deps = ["@pypi__packaging//:lib"], ) filegroup( diff --git a/tools/build_defs/python/private/BUILD.bazel b/tools/build_defs/python/private/BUILD.bazel new file mode 100644 index 0000000000..0a7f308f02 --- /dev/null +++ b/tools/build_defs/python/private/BUILD.bazel @@ -0,0 +1,27 @@ +# 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. + +load("@bazel_skylib//:bzl_library.bzl", "bzl_library") + +filegroup( + name = "distribution", + srcs = glob(["**"]), + visibility = ["//python:__subpackages__"], +) + +bzl_library( + name = "py_internal_renamed_bzl", + srcs = ["py_internal_renamed.bzl"], + visibility = ["@rules_python_internal//:__subpackages__"], +) diff --git a/tools/build_defs/python/private/py_internal_renamed.bzl b/tools/build_defs/python/private/py_internal_renamed.bzl new file mode 100644 index 0000000000..a12fc2d14e --- /dev/null +++ b/tools/build_defs/python/private/py_internal_renamed.bzl @@ -0,0 +1,30 @@ +# 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. +"""PYTHON RULE IMPLEMENTATION ONLY: Do not use outside of the rule implementations and their tests. + +NOTE: This file is only loaded by @rules_python_internal//:py_internal.bzl. This +is because the `py_internal` global symbol is only present in Bazel 7+, so +a repo rule has to conditionally load this depending on the Bazel version. + +Re-exports the restricted-use py_internal helper under another name. This is +necessary because `py_internal = py_internal` results in an error (trying +to bind a local symbol to itself before its defined). + +This is to allow the rule implementation in the //python directory to access +the internal helpers only rules_python is allowed to use. + +These may change at any time and are closely coupled to the rule implementation. +""" + +py_internal_renamed = py_internal diff --git a/tools/wheelmaker.py b/tools/wheelmaker.py index 63b833fc5d..dce5406093 100644 --- a/tools/wheelmaker.py +++ b/tools/wheelmaker.py @@ -33,10 +33,67 @@ def commonpath(path1, path2): def escape_filename_segment(segment): - """Escapes a filename segment per https://www.python.org/dev/peps/pep-0427/#escaping-and-unicode""" + """Escapes a filename segment per https://www.python.org/dev/peps/pep-0427/#escaping-and-unicode + + This is a legacy function, kept for backwards compatibility, + and may be removed in the future. See `escape_filename_distribution_name` + and `normalize_pep440` for the modern alternatives. + """ return re.sub(r"[^\w\d.]+", "_", segment, re.UNICODE) +def normalize_package_name(name): + """Normalize a package name according to the Python Packaging User Guide. + + See https://packaging.python.org/en/latest/specifications/name-normalization/ + """ + return re.sub(r"[-_.]+", "-", name).lower() + + +def escape_filename_distribution_name(name): + """Escape the distribution name component of a filename. + + See https://packaging.python.org/en/latest/specifications/binary-distribution-format/#escaping-and-unicode + """ + return normalize_package_name(name).replace("-", "_") + + +def normalize_pep440(version): + """Normalize version according to PEP 440, with fallback for placeholders. + + If there's a placeholder in braces, such as {BUILD_TIMESTAMP}, + replace it with 0. Such placeholders can be used with stamping, in + which case they would have been resolved already by now; if they + haven't, we're doing an unstamped build, but we still need to + produce a valid version. If such replacements are made, the + original version string, sanitized to dot-separated alphanumerics, + is appended as a local version segment, so you understand what + placeholder was involved. + + If that still doesn't produce a valid version, use version 0 and + append the original version string, sanitized to dot-separated + alphanumerics, as a local version segment. + + """ + + import packaging.version + + try: + return str(packaging.version.Version(version)) + except packaging.version.InvalidVersion: + pass + + sanitized = re.sub(r'[^a-z0-9]+', '.', version.lower()).strip('.') + substituted = re.sub(r'\{\w+\}', '0', version) + delimiter = '.' if '+' in substituted else '+' + try: + return str( + packaging.version.Version(f'{substituted}{delimiter}{sanitized}') + ) + except packaging.version.InvalidVersion: + return str(packaging.version.Version(f'0+{sanitized}')) + + class WheelMaker(object): def __init__( self, @@ -48,6 +105,8 @@ def __init__( platform, outfile=None, strip_path_prefixes=None, + incompatible_normalize_name=False, + incompatible_normalize_version=False, ): self._name = name self._version = version @@ -60,12 +119,30 @@ def __init__( strip_path_prefixes if strip_path_prefixes is not None else [] ) - self._distinfo_dir = ( - escape_filename_segment(self._name) - + "-" - + escape_filename_segment(self._version) - + ".dist-info/" - ) + if incompatible_normalize_version: + self._version = normalize_pep440(self._version) + self._escaped_version = self._version + else: + self._escaped_version = escape_filename_segment(self._version) + + if incompatible_normalize_name: + escaped_name = escape_filename_distribution_name(self._name) + self._distinfo_dir = ( + escaped_name + "-" + self._escaped_version + ".dist-info/" + ) + self._wheelname_fragment_distribution_name = escaped_name + else: + # The legacy behavior escapes the distinfo dir but not the + # wheel name. Enable incompatible_normalize_name to fix it. + # https://github.com/bazelbuild/rules_python/issues/1132 + self._distinfo_dir = ( + escape_filename_segment(self._name) + + "-" + + self._escaped_version + + ".dist-info/" + ) + self._wheelname_fragment_distribution_name = self._name + self._zipfile = None # Entries for the RECORD file as (filename, hash, size) tuples. self._record = [] @@ -81,7 +158,10 @@ def __exit__(self, type, value, traceback): self._zipfile = None def wheelname(self) -> str: - components = [self._name, self._version] + components = [ + self._wheelname_fragment_distribution_name, + self._version, + ] if self._build_tag: components.append(self._build_tag) components += [self._python_tag, self._abi, self._platform] @@ -330,6 +410,10 @@ def parse_args() -> argparse.Namespace: help="Pass in the stamp info file for stamping", ) + feature_group = parser.add_argument_group("Feature flags") + feature_group.add_argument("--incompatible_normalize_name", action="store_true") + feature_group.add_argument("--incompatible_normalize_version", action="store_true") + return parser.parse_args(sys.argv[1:]) @@ -386,6 +470,8 @@ def main() -> None: platform=arguments.platform, outfile=arguments.out, strip_path_prefixes=strip_prefixes, + incompatible_normalize_name=arguments.incompatible_normalize_name, + incompatible_normalize_version=arguments.incompatible_normalize_version, ) as maker: for package_filename, real_filename in all_files: maker.add_file(package_filename, real_filename) @@ -410,8 +496,15 @@ def main() -> None: with open(arguments.metadata_file, "rt", encoding="utf-8") as metadata_file: metadata = metadata_file.read() + if arguments.incompatible_normalize_version: + version_in_metadata = normalize_pep440(version) + else: + version_in_metadata = version maker.add_metadata( - metadata=metadata, name=name, description=description, version=version + metadata=metadata, + name=name, + description=description, + version=version_in_metadata, ) if arguments.entry_points_file: pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy