From de825a1715f21d9b0e6d8c8028413044016a09c2 Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Fri, 1 May 2026 11:23:44 -0700 Subject: [PATCH 1/9] fix: stop using broken buck-resources crate from crates.io Instead: use the one in-tree! The problem with the one on crates.io is that it got published with a BUCK file in the source tarball, so it breaks Buck when you `reindeer vendor` it as the sources are no longer visible. The crates.io publication should certainly be fixed, but there's no reason to use the crates.io version in-repo to begin with. Fixes: https://github.com/facebook/buck2/issues/1226 --- shed/completion_verify/BUCK | 2 +- shim/third-party/rust/Cargo.toml | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/shed/completion_verify/BUCK b/shed/completion_verify/BUCK index 8d5f788f274ad..3720bd6c2d1fc 100644 --- a/shed/completion_verify/BUCK +++ b/shed/completion_verify/BUCK @@ -18,7 +18,7 @@ rust_binary( }, }), deps = [ - "fbsource//third-party/rust:buck-resources", + "fbcode//buck2/integrations/resources/rust:buck_resources", "fbsource//third-party/rust:clap", "fbsource//third-party/rust:ptyprocess", "fbsource//third-party/rust:tempfile", diff --git a/shim/third-party/rust/Cargo.toml b/shim/third-party/rust/Cargo.toml index 9265aab9ddef0..84a76e7ff4bf6 100644 --- a/shim/third-party/rust/Cargo.toml +++ b/shim/third-party/rust/Cargo.toml @@ -46,7 +46,6 @@ bincode = { version = "2", features = ["serde"] } bitflags = "2.9" blake3 = { version = "=1.8.2", features = ["mmap", "rayon", "traits-preview"] } # Check overlay before updating bstr = { version = "1.10.0", features = ["serde", "std", "unicode"] } -buck-resources = "1" bumpalo = { version = "3.20.2", features = ["allocator_api", "collections"] } byteorder = "1.5" bytemuck = { version = "1.25", features = ["const_zeroed", "derive", "min_const_generics", "must_cast", "nightly_portable_simd", "nightly_stdsimd"] } From 98c4be129f8b34262909b9cb2c9f9fcfd8d5d92a Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Fri, 1 May 2026 11:23:44 -0700 Subject: [PATCH 2/9] shim: add tokio-retry, used by buck2_execute_impl --- shim/third-party/rust/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/shim/third-party/rust/Cargo.toml b/shim/third-party/rust/Cargo.toml index 84a76e7ff4bf6..cb8b6ff87d7c3 100644 --- a/shim/third-party/rust/Cargo.toml +++ b/shim/third-party/rust/Cargo.toml @@ -223,6 +223,7 @@ textwrap = { version = "0.16.0", features = ["terminal_size"] } thiserror = "2.0.18" threadpool = "1.8.1" tokio = { version = "1.52.1", features = ["full", "test-util", "tracing"] } +tokio-retry = "0.3" tokio-rustls = { version = "0.26.4", default-features = false, features = ["logging", "ring", "tls12"] } tokio-stream = { version = "0.1.18", features = ["fs", "io-util", "net", "signal", "sync", "time"] } tokio-util = { version = "0.7.18", features = ["full"] } From 583fd349d6795fa01afd603af0e922afc2b14ac3 Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Wed, 8 Apr 2026 16:14:49 -0700 Subject: [PATCH 3/9] Unfinished cut of pydeer !! VIBE CODING WARNING !! This was aggressively clauded. It works. The code isn't quite to my standard of quality, but it's Fine. It probably needs some tests. Fortunately we have Claude for those too. ---- This package introduces a basic infrastructure for PyPI packages, necessary to make headway on running Buck2's E2E test suite outside of Meta. It's sort of modeled after Reindeer for Rust, but outputs to separate folders. On account of wanting to get something working fast, I haven't cared about building from source because that would require dealing with Python build systems. We consume wheels. --- shim/.buckconfig | 2 +- shim/third-party/pypi/.gitignore | 1 + shim/third-party/pypi/README.md | 4 + shim/third-party/pypi/pyproject.toml | 14 ++ shim/third-party/pypi/uv.lock | 208 ++++++++++++++++ shim/third-party/python/pydeer/BUCK | 31 +++ shim/third-party/python/pydeer/README.md | 32 +++ shim/third-party/python/pydeer/generator.py | 164 ++++++++++++ shim/third-party/python/pydeer/lockfile.py | 144 +++++++++++ shim/third-party/python/pydeer/main.py | 235 ++++++++++++++++++ shim/third-party/python/pydeer/pyproject.toml | 6 + shim/third-party/python/pydeer/wheels.py | 203 +++++++++++++++ 12 files changed, 1043 insertions(+), 1 deletion(-) create mode 100644 shim/third-party/pypi/.gitignore create mode 100644 shim/third-party/pypi/README.md create mode 100644 shim/third-party/pypi/pyproject.toml create mode 100644 shim/third-party/pypi/uv.lock create mode 100644 shim/third-party/python/pydeer/BUCK create mode 100644 shim/third-party/python/pydeer/README.md create mode 100644 shim/third-party/python/pydeer/generator.py create mode 100644 shim/third-party/python/pydeer/lockfile.py create mode 100644 shim/third-party/python/pydeer/main.py create mode 100644 shim/third-party/python/pydeer/pyproject.toml create mode 100644 shim/third-party/python/pydeer/wheels.py diff --git a/shim/.buckconfig b/shim/.buckconfig index 6ad23bd4e9a8b..7926b2ed51f2d 100644 --- a/shim/.buckconfig +++ b/shim/.buckconfig @@ -10,7 +10,7 @@ # error messages when the user doesn't run "reindeer" much more obvious, # because the default BUCK file just errors out. We then make reindeer generate # BUCK.reindeer to override that. -name = BUCK.reindeer,BUCK +name = BUCK.pydeer,BUCK.reindeer,BUCK [cells] gh_facebook_buck2_shims_meta = . diff --git a/shim/third-party/pypi/.gitignore b/shim/third-party/pypi/.gitignore new file mode 100644 index 0000000000000..7856327756e26 --- /dev/null +++ b/shim/third-party/pypi/.gitignore @@ -0,0 +1 @@ +**/BUCK.pydeer diff --git a/shim/third-party/pypi/README.md b/shim/third-party/pypi/README.md new file mode 100644 index 0000000000000..aa4966bc0dc68 --- /dev/null +++ b/shim/third-party/pypi/README.md @@ -0,0 +1,4 @@ +# PyPI dependencies + +These have generated `BUCK.pydeer` files. +If they're missing, you can generate them with `bootstrap/buck2 run shim//third-party/python/pydeer:buckify`. diff --git a/shim/third-party/pypi/pyproject.toml b/shim/third-party/pypi/pyproject.toml new file mode 100644 index 0000000000000..6ea8196e33a04 --- /dev/null +++ b/shim/third-party/pypi/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "buck2-third-party-deps" +version = "0.1.0" +requires-python = ">=3.11" +dependencies = [ + "pytest", + "pytest-asyncio", + "decorator", + "setuptools", + "typed-argument-parser", + "importlib-resources", + "dataclasses-json", + "packaging", +] diff --git a/shim/third-party/pypi/uv.lock b/shim/third-party/pypi/uv.lock new file mode 100644 index 0000000000000..4c248fb2223a2 --- /dev/null +++ b/shim/third-party/pypi/uv.lock @@ -0,0 +1,208 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "buck2-third-party-deps" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "dataclasses-json" }, + { name = "decorator" }, + { name = "importlib-resources" }, + { name = "packaging" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "setuptools" }, + { name = "typed-argument-parser" }, +] + +[package.metadata] +requires-dist = [ + { name = "dataclasses-json" }, + { name = "decorator" }, + { name = "importlib-resources" }, + { name = "packaging" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, + { name = "setuptools" }, + { name = "typed-argument-parser" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "dataclasses-json" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "marshmallow" }, + { name = "typing-inspect" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[package]] +name = "docstring-parser" +version = "0.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, +] + +[[package]] +name = "importlib-resources" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/06/b56dfa750b44e86157093bc8fca0ab81dccbf5260510de4eaf1cb69b5b99/importlib_resources-7.1.0.tar.gz", hash = "sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708", size = 44985, upload-time = "2026-04-12T16:36:09.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1", size = 37232, upload-time = "2026-04-12T16:36:08.219Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "marshmallow" +version = "3.26.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + +[[package]] +name = "setuptools" +version = "82.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, +] + +[[package]] +name = "typed-argument-parser" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docstring-parser" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/d0/5240b74b8d76e20eadbd79fe7ce5daeb1bf86d70d923817563728ccf297a/typed_argument_parser-1.12.0.tar.gz", hash = "sha256:cf5f7cafac869fb9627c4c90394e321c895d58872d865ad2c62e65668d259fa0", size = 244627, upload-time = "2026-03-28T21:01:46.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/0c/0607f5ac258aa36621a18cec7e14b87b49c972348a6937ea19cfea81d7b7/typed_argument_parser-1.12.0-py3-none-any.whl", hash = "sha256:9cdf26c710b9cde2992ff0763288598628ac852d9a1accf5269837cd6e24a930", size = 32833, upload-time = "2026-03-28T21:01:44.999Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspect" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, +] diff --git a/shim/third-party/python/pydeer/BUCK b/shim/third-party/python/pydeer/BUCK new file mode 100644 index 0000000000000..4d252fc326332 --- /dev/null +++ b/shim/third-party/python/pydeer/BUCK @@ -0,0 +1,31 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under both the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree and the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. + +oncall("open_source") + +python_bootstrap_library( + name = "pydeer-lib", + srcs = [ + "generator.py", + "lockfile.py", + "wheels.py", + ], +) + +python_bootstrap_binary( + name = "pydeer", + main = "main.py", + deps = [":pydeer-lib"], + visibility = ["PUBLIC"], +) + +command_alias( + name = "buckify", + exe = ":pydeer", + args = ["buckify", "--third-party-dir", "shim/third-party/pypi", "--cell-root", "shim"], + visibility = ["PUBLIC"], +) diff --git a/shim/third-party/python/pydeer/README.md b/shim/third-party/python/pydeer/README.md new file mode 100644 index 0000000000000..f16627c98074b --- /dev/null +++ b/shim/third-party/python/pydeer/README.md @@ -0,0 +1,32 @@ +# pydeer + +This is a Python equivalent of Reindeer, the Cargo.lock -> BUCK file generator. + +Usage: +``` +$ buck2 run shim//third-party/python/pydeer:pydeer -- \ + buckify --third-party-dir shim/third-party/pypi --cell-root shim +``` + +This will read `uv.lock` in `shim/third-party/pypi` and create directories for each package with `BUCK.pydeer` files therein. + +Each package directory has an eponymous `python_library` target e.g. `shim//third-party/pypi/pytest:pytest`, which consumers depend on. +It also has a `http_file` target which fetches a wheel for the package. + +## Flags + +`buckify`: + +- `--third-party-dir ` - directory containing `pyproject.toml` and `uv.lock`; this is also the output directory. +- `--cell-root ` - path to the Buck2 cell root that contains `--third-party-dir`. Generated labels are relative to this. Defaults to `$BUCK2_OSS_REPO_DIR` or cwd. Pass this when the third-party dir lives in a non-root cell (e.g. `shim`). +- `--platforms linux-arm64,linux-x86_64,macos-arm64` - platforms to emit build rules for wheels for. By default, includes `linux-{arm64,x86_64}` and `macos-arm64`. +- `--python 3.11` - target Python version; determines which wheels are used. +- `--no-lock` - error out if uv.lock looks possibly outdated, rather than running `uv lock` +- `-v` - verbose logging + +## Limitations + +We do not support building packages from source yet: for now, we use the wheels for each platform and `select()` over them. + +We don't emit dependency edges for extras (optional dependencies). +We ignore environment markers (e.g. `tomli ; python_version < "3.11"`) and emit the dependency edge unconditionally. diff --git a/shim/third-party/python/pydeer/generator.py b/shim/third-party/python/pydeer/generator.py new file mode 100644 index 0000000000000..b3f48e960a4e9 --- /dev/null +++ b/shim/third-party/python/pydeer/generator.py @@ -0,0 +1,164 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under both the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree and the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. + +"""Emit BUCK.pydeer files for each package in the lockfile.""" + +from __future__ import annotations + +import json +import logging +from dataclasses import dataclass +from io import StringIO +from pathlib import Path + +from lockfile import Package +from wheels import ParsedWheel, Target + +log = logging.getLogger(__name__) + + +_LICENSE = """\ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under both the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree and the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. +""" + +_GENERATED_MARKER = "# @" + "generated by `pydeer buckify`" + + +@dataclass(frozen=True) +class PackageBuild: + package: Package + wheels_by_target: dict[str, ParsedWheel] + third_party_dir_label: str + """Repo-relative label prefix for cross-package deps, e.g. + "//shim/third-party/pypi".""" + + +def _q(s: str) -> str: + """Render `s` as a double-quoted Starlark string.""" + return json.dumps(s) + + +def _render_string_list(items: list[str], indent: str) -> str: + if not items: + return "[]" + inner = "\n".join(f"{indent} {_q(s)}," for s in items) + return "[\n" + inner + f"\n{indent}]" + + +def _render_http_file(name: str, url: str, sha256: str) -> str: + return ( + "http_file(\n" + f" name = {_q(name)},\n" + f" urls = [{_q(url)}],\n" + f" sha256 = {_q(sha256)},\n" + f" out = {_q(name)},\n" + ")\n" + ) + + +def _render_binary_src( + wheels_by_target: dict[str, ParsedWheel], + targets: tuple[Target, ...], +) -> str: + unique_wheel_urls = {pw.wheel.url for pw in wheels_by_target.values()} + if len(unique_wheel_urls) == 1: + only = next(iter(wheels_by_target.values())) + return _q(":" + only.wheel.filename) + + by_name = {t.name: t for t in targets} + os_groups: dict[str, list[Target]] = {} + for tname in sorted(wheels_by_target): + target = by_name[tname] + os_key = target.select_keys[0] + os_groups.setdefault(os_key, []).append(target) + + lines: list[str] = ["select({"] + for os_key in sorted(os_groups): + targets_in_group = os_groups[os_key] + is_flat = all(len(t.select_keys) == 1 for t in targets_in_group) + if is_flat: + t = targets_in_group[0] + wheel = wheels_by_target[t.name].wheel + lines.append( + f" {_q(os_key)}: {_q(':' + wheel.filename)}," + ) + else: + lines.append(f" {_q(os_key)}: select({{") + for t in targets_in_group: + cpu_key = t.select_keys[1] + wheel = wheels_by_target[t.name].wheel + lines.append( + f" {_q(cpu_key)}: {_q(':' + wheel.filename)}," + ) + lines.append(" }),") + lines.append(" })") + return "\n".join(lines) + + +def _render_deps(package: Package, third_party_dir_label: str) -> list[str]: + return sorted( + f"{third_party_dir_label}/{d.name}:{d.name}" for d in package.deps + ) + + +def render_buck_file( + build: PackageBuild, + targets: tuple[Target, ...], +) -> str: + out = StringIO() + out.write(_LICENSE) + out.write("\n") + out.write(_GENERATED_MARKER + "\n\n") + out.write( + 'load("@prelude//:rules.bzl", "http_file", "prebuilt_python_library")\n\n' + ) + out.write('oncall("open_source")\n\n') + + seen_urls: set[str] = set() + ordered_wheels: list[ParsedWheel] = [] + for tname in sorted(build.wheels_by_target): + pw = build.wheels_by_target[tname] + if pw.wheel.url in seen_urls: + continue + seen_urls.add(pw.wheel.url) + ordered_wheels.append(pw) + ordered_wheels.sort(key=lambda pw: pw.wheel.filename) + + for pw in ordered_wheels: + out.write(_render_http_file(pw.wheel.filename, pw.wheel.url, pw.wheel.sha256)) + out.write("\n") + + binary_src = _render_binary_src(build.wheels_by_target, targets) + deps = _render_deps(build.package, build.third_party_dir_label) + + out.write("prebuilt_python_library(\n") + out.write(f" name = {_q(build.package.name)},\n") + out.write(f" binary_src = {binary_src},\n") + out.write(f" deps = {_render_string_list(deps, ' ')},\n") + out.write(' visibility = ["PUBLIC"],\n') + out.write(")\n") + + return out.getvalue() + + +def write_package( + build: PackageBuild, + targets: tuple[Target, ...], + out_root: Path, +) -> Path: + pkg_dir = out_root / build.package.name + pkg_dir.mkdir(parents=True, exist_ok=True) + out_path = pkg_dir / "BUCK.pydeer" + rendered = render_buck_file(build, targets) + out_path.write_text(rendered) + log.debug("wrote %s", out_path) + return out_path diff --git a/shim/third-party/python/pydeer/lockfile.py b/shim/third-party/python/pydeer/lockfile.py new file mode 100644 index 0000000000000..0380f39a84be7 --- /dev/null +++ b/shim/third-party/python/pydeer/lockfile.py @@ -0,0 +1,144 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under both the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree and the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. + +"""Parse uv.lock into typed dataclasses.""" + +from __future__ import annotations + +import logging +import re +import tomllib +from dataclasses import dataclass +from pathlib import Path +from urllib.parse import unquote, urlparse + +log = logging.getLogger(__name__) + +_SHA256_PREFIX = "sha256:" + + +@dataclass(frozen=True) +class Wheel: + url: str + sha256: str + filename: str + + +@dataclass(frozen=True) +class Sdist: + url: str + sha256: str + + +@dataclass(frozen=True) +class Dependency: + name: str + + +@dataclass(frozen=True) +class Package: + name: str + version: str + source: str + deps: tuple[Dependency, ...] + wheels: tuple[Wheel, ...] + sdist: Sdist | None + + +@dataclass(frozen=True) +class Lockfile: + requires_python: str + packages: tuple[Package, ...] + + +def canonicalize(name: str) -> str: + """PEP 503 name canonicalization.""" + return re.sub(r"[-_.]+", "-", name).lower() + + +def _strip_sha256(hash_value: str) -> str: + if not hash_value.startswith(_SHA256_PREFIX): + raise ValueError(f"Expected sha256: prefix on hash, got {hash_value!r}") + return hash_value[len(_SHA256_PREFIX) :] + + +def _filename_from_url(url: str) -> str: + path = urlparse(url).path + return unquote(path.rsplit("/", 1)[-1]) + + +def _classify_source(source: dict[str, str]) -> str: + for key in ("registry", "virtual", "editable", "git", "path", "url", "directory"): + if key in source: + return key + return "unknown" + + +def _parse_wheel(entry: dict[str, object]) -> Wheel: + url = str(entry["url"]) + return Wheel( + url=url, + sha256=_strip_sha256(str(entry["hash"])), + filename=_filename_from_url(url), + ) + + +def _parse_sdist(entry: dict[str, object]) -> Sdist: + return Sdist( + url=str(entry["url"]), + sha256=_strip_sha256(str(entry["hash"])), + ) + + +def _parse_package(raw: dict[str, object]) -> Package: + name = canonicalize(str(raw["name"])) + version = str(raw["version"]) + source = _classify_source(raw.get("source") or {}) # type: ignore[arg-type] + + raw_deps = raw.get("dependencies") or [] + deps = tuple( + Dependency(name=canonicalize(str(d["name"]))) + for d in raw_deps # type: ignore[union-attr] + ) + + raw_wheels = raw.get("wheels") or [] + wheels = tuple(_parse_wheel(w) for w in raw_wheels) # type: ignore[arg-type] + + raw_sdist = raw.get("sdist") + sdist = _parse_sdist(raw_sdist) if raw_sdist else None # type: ignore[arg-type] + + return Package( + name=name, + version=version, + source=source, + deps=deps, + wheels=wheels, + sdist=sdist, + ) + + +def parse(path: Path) -> Lockfile: + log.debug("parsing %s", path) + with path.open("rb") as f: + data = tomllib.load(f) + + requires_python = str(data.get("requires-python", "")) + raw_packages = data.get("package") or [] + + packages: list[Package] = [] + for raw in raw_packages: + pkg = _parse_package(raw) + if pkg.source in ("virtual", "editable", "directory", "path"): + log.debug("skipping workspace package %s (source=%s)", pkg.name, pkg.source) + continue + packages.append(pkg) + + log.debug("parsed %d packages from %s", len(packages), path) + return Lockfile( + requires_python=requires_python, + packages=tuple(packages), + ) diff --git a/shim/third-party/python/pydeer/main.py b/shim/third-party/python/pydeer/main.py new file mode 100644 index 0000000000000..3d2cbc6b5e63c --- /dev/null +++ b/shim/third-party/python/pydeer/main.py @@ -0,0 +1,235 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under both the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree and the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. + +"""pydeer: generate BUCK files from a uv.lock.""" + +from __future__ import annotations + +import argparse +import logging +import os +import subprocess +import sys +from pathlib import Path + +import generator +import lockfile +import wheels + +log = logging.getLogger("pydeer") + + +class PydeerError(Exception): + pass + + +def _ensure_lockfile(third_party_dir: Path, allow_lock: bool) -> Path: + pyproject = third_party_dir / "pyproject.toml" + uv_lock = third_party_dir / "uv.lock" + + if not pyproject.is_file(): + raise PydeerError(f"Missing pyproject.toml at {pyproject}") + + needs_lock = ( + not uv_lock.is_file() + or uv_lock.stat().st_mtime < pyproject.stat().st_mtime + ) + if not needs_lock: + return uv_lock + if not allow_lock: + raise PydeerError( + f"{uv_lock} is missing or stale and --no-lock was given. " + "Run `uv lock` manually." + ) + + log.info("running `uv lock` in %s", third_party_dir) + try: + subprocess.run(["uv", "lock"], cwd=third_party_dir, check=True) + except FileNotFoundError as e: + raise PydeerError( + "`uv` was not found on PATH. Install it via `pip install uv` " + "or `brew install uv`." + ) from e + except subprocess.CalledProcessError as e: + raise PydeerError(f"`uv lock` failed with exit code {e.returncode}") from e + return uv_lock + + +def _parse_python_version(value: str) -> tuple[int, int]: + parts = value.split(".") + if len(parts) != 2 or not all(p.isdigit() for p in parts): + raise argparse.ArgumentTypeError( + f"--python expects MAJOR.MINOR (e.g. 3.11), got {value!r}" + ) + return int(parts[0]), int(parts[1]) + + +def _select_targets(names: list[str]) -> tuple[wheels.Target, ...]: + by_name = {t.name: t for t in wheels.DEFAULT_TARGETS} + chosen: list[wheels.Target] = [] + unknown: list[str] = [] + for n in names: + if n in by_name: + chosen.append(by_name[n]) + else: + unknown.append(n) + if unknown: + raise PydeerError( + f"Unknown platform(s): {', '.join(unknown)}. " + f"Known: {', '.join(t.name for t in wheels.DEFAULT_TARGETS)}" + ) + return tuple(chosen) + + +def _label_for(third_party_dir: Path, cell_root: Path | None) -> str: + """Compute the cell-relative directory label used as a label prefix. + + For example cell_root=shim, third_party_dir=shim/third-party/pypi + yields //third-party/pypi. + + `cell_root` defaults to $BUCK2_OSS_REPO_DIR if set, else cwd.""" + if cell_root is None: + cell_root = Path(os.environ.get("BUCK2_OSS_REPO_DIR", os.getcwd())) + cell_root = cell_root.resolve() + abs_dir = third_party_dir.resolve() + try: + rel = abs_dir.relative_to(cell_root) + except ValueError as e: + raise PydeerError( + f"--third-party-dir {third_party_dir} is not under cell root " + f"{cell_root}; pass --cell-root or set BUCK2_OSS_REPO_DIR." + ) from e + return "//" + rel.as_posix() + + +def buckify(args: argparse.Namespace) -> None: + third_party_dir = Path(args.third_party_dir).resolve() + cell_root = Path(args.cell_root).resolve() if args.cell_root else None + targets = _select_targets([n.strip() for n in args.platforms.split(",") if n.strip()]) + py_major, py_minor = args.python + + uv_lock_path = _ensure_lockfile(third_party_dir, allow_lock=not args.no_lock) + lock = lockfile.parse(uv_lock_path) + log.info("parsed %d packages from %s", len(lock.packages), uv_lock_path) + + label_prefix = _label_for(third_party_dir, cell_root) + log.debug("third-party label prefix: %s", label_prefix) + + sdist_only: list[tuple[str, str]] = [] + no_wheel_match: list[tuple[str, str]] = [] + builds: list[generator.PackageBuild] = [] + + for pkg in lock.packages: + if not pkg.wheels: + sdist_only.append((pkg.name, pkg.version)) + continue + selection = wheels.select_for_package(pkg, targets, py_major, py_minor) + if not selection: + no_wheel_match.append((pkg.name, pkg.version)) + continue + builds.append( + generator.PackageBuild( + package=pkg, + wheels_by_target=selection, + third_party_dir_label=label_prefix, + ) + ) + + errors: list[str] = [] + if sdist_only: + rows = "\n".join(f" - {n} {v}" for n, v in sorted(sdist_only)) + errors.append( + "The following packages have no wheels (sdist-only). pydeer does " + f"not yet support building from source:\n{rows}" + ) + if no_wheel_match: + rows = "\n".join(f" - {n} {v}" for n, v in sorted(no_wheel_match)) + errors.append( + "The following packages have wheels, but none match Python " + f"{py_major}.{py_minor} on any of the requested platforms " + f"({', '.join(t.name for t in targets)}):\n{rows}" + ) + if errors: + raise PydeerError("\n\n".join(errors)) + + third_party_dir.mkdir(parents=True, exist_ok=True) + for build in builds: + generator.write_package(build, targets, third_party_dir) + + log.info( + "generated %d BUCK.pydeer files in %s for platforms: %s", + len(builds), + third_party_dir, + ", ".join(t.name for t in targets), + ) + + +def lock_only(args: argparse.Namespace) -> None: + third_party_dir = Path(args.third_party_dir).resolve() + _ensure_lockfile(third_party_dir, allow_lock=True) + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser( + prog="pydeer", + description="Generate BUCK files from a uv.lock.", + ) + sub = parser.add_subparsers(dest="command", required=True) + + p_buckify = sub.add_parser("buckify", help="Generate BUCK files from uv.lock.") + p_buckify.add_argument("--third-party-dir", required=True) + p_buckify.add_argument( + "--cell-root", + default=None, + help="Path to the Buck2 cell root containing --third-party-dir. " + "Generated labels are relative to this. " + "Defaults to $BUCK2_OSS_REPO_DIR or cwd.", + ) + p_buckify.add_argument( + "--platforms", + default="linux-arm64,linux-x86_64,macos-arm64", + help="Comma-separated list of target platforms.", + ) + p_buckify.add_argument( + "--python", + type=_parse_python_version, + default=(3, 11), + help="Target Python version as MAJOR.MINOR (default: 3.11).", + ) + p_buckify.add_argument( + "--no-lock", + action="store_true", + help="Do not run `uv lock`; require uv.lock to be present.", + ) + p_buckify.add_argument("-v", "--verbose", action="store_true") + p_buckify.set_defaults(func=buckify) + + p_lock = sub.add_parser("lock", help="Run `uv lock` only; do not generate BUCK files.") + p_lock.add_argument("--third-party-dir", required=True) + p_lock.add_argument("-v", "--verbose", action="store_true") + p_lock.set_defaults(func=lock_only) + + return parser + + +def main(argv: list[str] | None = None) -> int: + parser = build_parser() + args = parser.parse_args(argv) + logging.basicConfig( + level=logging.DEBUG if args.verbose else logging.INFO, + format="%(levelname)s %(name)s: %(message)s", + ) + try: + args.func(args) + except PydeerError as e: + log.error("%s", e) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/shim/third-party/python/pydeer/pyproject.toml b/shim/third-party/python/pydeer/pyproject.toml new file mode 100644 index 0000000000000..28aaa867fd7d1 --- /dev/null +++ b/shim/third-party/python/pydeer/pyproject.toml @@ -0,0 +1,6 @@ +[project] +name = "pydeer" +version = "0.1.0" +description = "Generate Buck2 BUCK files from a uv.lock" +requires-python = ">=3.11" +dependencies = [] diff --git a/shim/third-party/python/pydeer/wheels.py b/shim/third-party/python/pydeer/wheels.py new file mode 100644 index 0000000000000..a76d301e7bbff --- /dev/null +++ b/shim/third-party/python/pydeer/wheels.py @@ -0,0 +1,203 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under both the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree and the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. + +"""PEP 425/427 wheel tag parsing and per-platform wheel selection.""" + +from __future__ import annotations + +import logging +import re +from dataclasses import dataclass + +from lockfile import Package, Wheel + +log = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class Target: + """A target platform pydeer generates a wheel selection for.""" + + name: str + """Stable identifier (e.g. "linux-arm64").""" + + select_keys: tuple[str, ...] + """Buck select() keys to express this target. A single key means a flat + select; multiple keys imply a nested os→cpu select.""" + + platform_pattern: re.Pattern[str] + """Regex matched against the platform_tag of a wheel.""" + + +_LINUX_ARM64 = re.compile(r"^(manylinux\w*_aarch64|linux_aarch64|musllinux\w*_aarch64)$") +_LINUX_X86_64 = re.compile(r"^(manylinux\w*_x86_64|linux_x86_64|musllinux\w*_x86_64)$") +_MACOS_ARM64 = re.compile(r"^macosx_\d+_\d+_(arm64|universal2)$") + +DEFAULT_TARGETS: tuple[Target, ...] = ( + Target( + name="linux-arm64", + select_keys=("prelude//os:linux-arm64",), + platform_pattern=_LINUX_ARM64, + ), + Target( + name="linux-x86_64", + select_keys=("prelude//os:linux", "prelude//cpu:x86_64"), + platform_pattern=_LINUX_X86_64, + ), + Target( + name="macos-arm64", + select_keys=("prelude//os:macos", "prelude//cpu:arm64"), + platform_pattern=_MACOS_ARM64, + ), +) + + +@dataclass(frozen=True) +class WheelTags: + python_tags: frozenset[str] + abi_tags: frozenset[str] + platform_tags: frozenset[str] + + +@dataclass(frozen=True) +class ParsedWheel: + wheel: Wheel + tags: WheelTags + + +def parse_filename(filename: str) -> WheelTags: + """Split a wheel filename into its compatibility tag sets per PEP 427.""" + if not filename.endswith(".whl"): + raise ValueError(f"Not a wheel filename: {filename!r}") + stem = filename[: -len(".whl")] + parts = stem.split("-") + if len(parts) not in (5, 6): + raise ValueError( + f"Wheel filename {filename!r} has {len(parts)} dash-separated " + "components; expected 5 (no build tag) or 6 (with build tag)" + ) + python_tag, abi_tag, platform_tag = parts[-3], parts[-2], parts[-1] + return WheelTags( + python_tags=frozenset(python_tag.split(".")), + abi_tags=frozenset(abi_tag.split(".")), + platform_tags=frozenset(platform_tag.split(".")), + ) + + +_PYTHON_VERSION_RE = re.compile(r"^(?:py|cp)(\d+)(\d)?$") + + +def _python_supports(python_tag: str, py_major: int, py_minor: int) -> bool: + """Whether a python_tag covers the target Python version. + + `py3` and `cp3` match any 3.x. `py311` and `cp311` match 3.11 exactly. + `py310` does NOT match 3.11 (different minor).""" + m = _PYTHON_VERSION_RE.match(python_tag) + if not m: + return False + major = int(m.group(1)[0]) + if major != py_major: + return False + if m.group(2) is None and len(m.group(1)) == 1: + return True + minor_part = m.group(1)[1:] + if not minor_part: + return True + return int(minor_part) == py_minor + + +def _abi3_supports(python_tag: str, py_major: int, py_minor: int) -> bool: + """abi3 wheels are forward-compatible: a cp310 abi3 wheel works on 3.10+.""" + m = _PYTHON_VERSION_RE.match(python_tag) + if not m or not python_tag.startswith("cp"): + return False + major = int(m.group(1)[0]) + if major != py_major: + return False + minor_part = m.group(1)[1:] + if not minor_part: + return True + return int(minor_part) <= py_minor + + +def _platform_matches(platform_tags: frozenset[str], target: Target) -> bool: + if "any" in platform_tags: + return True + return any(target.platform_pattern.match(t) for t in platform_tags) + + +def _wheel_priority( + tags: WheelTags, + target: Target, + py_major: int, + py_minor: int, +) -> int | None: + """Return a priority for this wheel against the target. Lower is better. + None means the wheel does not satisfy the target.""" + if not _platform_matches(tags.platform_tags, target): + return None + + exact = f"cp{py_major}{py_minor}" + if exact in tags.python_tags and exact in tags.abi_tags: + return 0 + + if "abi3" in tags.abi_tags and any( + _abi3_supports(p, py_major, py_minor) for p in tags.python_tags + ): + return 1 + + if "none" in tags.abi_tags and "any" in tags.platform_tags and any( + _python_supports(p, py_major, py_minor) for p in tags.python_tags + ): + return 2 + + if "none" in tags.abi_tags and any( + _python_supports(p, py_major, py_minor) for p in tags.python_tags + ): + return 3 + + return None + + +def select_wheel( + parsed: list[ParsedWheel], + target: Target, + py_major: int, + py_minor: int, +) -> ParsedWheel | None: + """Pick the best wheel for the target, or None if none match.""" + candidates: list[tuple[int, ParsedWheel]] = [] + for pw in parsed: + priority = _wheel_priority(pw.tags, target, py_major, py_minor) + if priority is not None: + candidates.append((priority, pw)) + if not candidates: + return None + candidates.sort(key=lambda x: (x[0], x[1].wheel.filename)) + return candidates[0][1] + + +def select_for_package( + package: Package, + targets: tuple[Target, ...], + py_major: int, + py_minor: int, +) -> dict[str, ParsedWheel]: + """Return target.name -> chosen wheel for every target that has a match.""" + parsed: list[ParsedWheel] = [] + for w in package.wheels: + try: + parsed.append(ParsedWheel(wheel=w, tags=parse_filename(w.filename))) + except ValueError as e: + log.warning("skipping unparseable wheel for %s: %s", package.name, e) + + selection: dict[str, ParsedWheel] = {} + for target in targets: + chosen = select_wheel(parsed, target, py_major, py_minor) + if chosen is not None: + selection[target.name] = chosen + return selection From c248d76db5d126fb0933e345926a02721222ba80 Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Fri, 1 May 2026 11:23:44 -0700 Subject: [PATCH 4/9] buck2_e2e: basic support for running in OSS This requires two things: 1. Producing worthwhile pytest output outside of TPX: we don't have TPX to show individual test case status, so we need to enable default pytest output to get acceptable diagnostics. 2. Allowing disabling certain tests in OSS. For example, tests/core/help will always fail in OSS because the checked-in output uses a internalfb.com url for the homepage, for instance. --- tests/buck_e2e.bzl | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/buck_e2e.bzl b/tests/buck_e2e.bzl index d7f20e8a39256..18bcc30ac854e 100644 --- a/tests/buck_e2e.bzl +++ b/tests/buck_e2e.bzl @@ -11,6 +11,7 @@ load("@fbcode_macros//build_defs:native_rules.bzl", "buck_filegroup") load("@fbcode_macros//build_defs:python_pytest.bzl", "python_pytest") load("@fbsource//tools/target_determinator/macros:ci.bzl", "ci") load("@fbsource//tools/target_determinator/macros:ci_hint.bzl", "ci_hint") +load("@prelude//:is_full_meta_repo.bzl", "is_full_meta_repo") def buck_e2e_test( name, @@ -35,10 +36,13 @@ def buck_e2e_test( cfg_modifiers = None, ci_srcs = [], ci_deps = [], - compatible_with = None): + compatible_with = None, + meta_only = False): """ Custom macro for buck2/buckaemon end-to-end tests using pytest. """ + if meta_only and not is_full_meta_repo(): + return srcs = srcs or [] labels = labels or [] deps = deps or [] @@ -60,7 +64,12 @@ def buck_e2e_test( # ---tb=native shows python native traceback instead of default pytest traceback with source code. # --no-header disables headers printed after "test session starts" on output # --no-summary disables pytest summary printed after each test run on output - env["PYTEST_ADDOPTS"] = "-vv --tb=native --no-header --no-summary" + if is_full_meta_repo(): + # tpx captures per-test output, so the pytest header/summary is redundant. + env["PYTEST_ADDOPTS"] = "-vv --tb=native --no-header --no-summary" + else: + # OSS has no per-test capture; keep the summary so failures are visible. + env["PYTEST_ADDOPTS"] = "-vv --tb=native" # For autodeps read_package_value = getattr(native, "read_package_value", None) @@ -205,7 +214,8 @@ def buck2_e2e_test( require_nano_prelude = None, ci_srcs = [], ci_deps = [], - compatible_with = None): + compatible_with = None, + meta_only = False): """ Custom macro for buck2 end-to-end tests using pytest. All tests are run against buck2 compiled in-repo (compiled buck2). @@ -231,6 +241,8 @@ def buck2_e2e_test( dependency for Target Determinator purposes (e.g., bxl tests that test Starlark logic). Default is False. """ + if meta_only and not is_full_meta_repo(): + return kwargs = { "base_module": base_module, "ci_deps": ci_deps, From a389794d9758fd131d2bc0031d1aa8a12e0876d5 Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Fri, 1 May 2026 11:23:44 -0700 Subject: [PATCH 5/9] OSS: make fbcode//buck2:buck2 accessible to tests Quoth tests/buck_e2e.bzl: ``` if use_compiled_buck2_client_and_tpx: base_exe = "$(location fbcode//buck2:symlinked_buck2_and_tpx)/buck2" exe = select({ "DEFAULT": base_exe, "ovr_config//os:windows": base_exe + ".exe", }) else: exe = "$(location fbcode//buck2:buck2)" ``` So this stuff needs to be accessible from other files. I would totally believe that this is the wrong way, or that this file exists somewhere else in fbcode than it does in OSS such that this is the wrong target (and it's undesirable to mark it as public), but this is ultimately what target we get at this path once we apply the transformations used in OSS. --- BUCK | 1 + defs.bzl | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/BUCK b/BUCK index 95dc5ffdf5af2..e5496d8a9de16 100644 --- a/BUCK +++ b/BUCK @@ -10,6 +10,7 @@ pagable_transition_alias( name = "buck2", actual = "//buck2/app/buck2:buck2-bin", labels = [ci.aarch64(ci.skip_test())], + visibility = ["PUBLIC"], ) buck2_bundle( diff --git a/defs.bzl b/defs.bzl index 8c8c9c32bf7f9..e42862b3cd392 100644 --- a/defs.bzl +++ b/defs.bzl @@ -102,7 +102,7 @@ _pagable_transition_alias = rule( cfg = _pagable_transition, ) -def pagable_transition_alias(name: str, actual, labels): +def pagable_transition_alias(name: str, actual, labels, visibility = None): platform = platform_utils.get_cxx_platform_for_base_path(native.package_name()) default_target_platform = platform.target_platform _pagable_transition_alias( @@ -110,4 +110,5 @@ def pagable_transition_alias(name: str, actual, labels): actual = translate_target(actual), labels = labels, default_target_platform = default_target_platform, + visibility = visibility, ) From f7fc7bbd2dbc1165eedf6c26c6a7dd287b1d68d1 Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Fri, 1 May 2026 11:23:44 -0700 Subject: [PATCH 6/9] tests/core: add PACKAGE This is *surely* going to super blow up when this gets back into fbcode. I am guessing this is just not exported? Quoth tests/e2e_util/buck_workspace.py: ``` if os.environ.get("BUCK2_E2E_TEST_FLAVOR") == "isolated": if inplace is not None: raise Exception( "Don't set `inplace` in `tests/core` - these tests are always isolated" ) inplace = False else: if inplace is None: raise Exception("`inplace` must be set for `buck_test()`") ``` And BUCK2_E2E_TEST_FLAVOR comes from the package value, so I think we need a PACKAGE here. --- tests/core/PACKAGE | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/core/PACKAGE diff --git a/tests/core/PACKAGE b/tests/core/PACKAGE new file mode 100644 index 0000000000000..c9ed59e908ca9 --- /dev/null +++ b/tests/core/PACKAGE @@ -0,0 +1,9 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is dual-licensed under either the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree or the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. You may select, at your option, one of the +# above-listed licenses. + +write_package_value("buck2_e2e_test.flavor", "isolated") From c6f90449e82ff7df78a04ec8102b636fc21801c7 Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Fri, 1 May 2026 11:23:44 -0700 Subject: [PATCH 7/9] OSS: translate Python module paths We have to translate Python module paths because our repo root is *inside* fbcode//buck2, rather than being at fbcode//. This means that `//foo` needs to get the module path `buck2.foo`, thus this buckconfig hackery. --- .buckconfig | 1 + shim/build_defs/lib/oss.bzl | 13 +++++++++++++ shim/build_defs/python_binary.bzl | 9 +++++++-- shim/build_defs/python_library.bzl | 12 +++++++++--- shim/build_defs/python_unittest.bzl | 9 +++++++-- 5 files changed, 37 insertions(+), 7 deletions(-) diff --git a/.buckconfig b/.buckconfig index de91633a32013..fbe94c90ddc62 100644 --- a/.buckconfig +++ b/.buckconfig @@ -8,6 +8,7 @@ root = gh_facebook_buck2 [oss] internal_cell = fbcode stripped_root_dirs = buck2 +python_module_prefix = buck2 [project] ignore = \ diff --git a/shim/build_defs/lib/oss.bzl b/shim/build_defs/lib/oss.bzl index 0430d96297391..74c07ef5574c1 100644 --- a/shim/build_defs/lib/oss.bzl +++ b/shim/build_defs/lib/oss.bzl @@ -116,6 +116,18 @@ PREFIX_MAPPINGS = _parse_prefix_mappings( _filter_empty_strings(read_config("oss", "prefix_mappings", "").split(" ")), ) +# Prepended to default `base_module` in python rules, so that source files keep +# the same dotted import path they have inside the internal monorepo (e.g. +# `fbcode/buck2/tests/foo.py` is imported as `buck2.tests.foo`, but in OSS the +# package_name is `tests/foo` and would otherwise be `tests.foo`). +PYTHON_MODULE_PREFIX = read_config("oss", "python_module_prefix", "") + +def default_base_module(): + pkg = native.package_name().replace("/", ".") + if PYTHON_MODULE_PREFIX: + return PYTHON_MODULE_PREFIX + "." + pkg if pkg else PYTHON_MODULE_PREFIX + return pkg + # Hardcoded rewrite rules that apply to many projects and only produce targets # within the shim cell. They are applied after the rules from .buckconfig, and # will not be applied if any other rules match. @@ -235,3 +247,4 @@ def _swap_root_dir_for_path(path: str, root_dir: str, new_root_dir) -> str: suffix = "/" + suffix replace_path = new_root_dir.removesuffix("/") + suffix return replace_path.removeprefix("/") + diff --git a/shim/build_defs/python_binary.bzl b/shim/build_defs/python_binary.bzl index bbf60674c923e..13fa0614af631 100644 --- a/shim/build_defs/python_binary.bzl +++ b/shim/build_defs/python_binary.bzl @@ -6,8 +6,13 @@ # of this source tree. You may select, at your option, one of the # above-listed licenses. -def python_binary(srcs = [], **kwargs): +load("@shim//build_defs/lib:oss.bzl", "default_base_module") + +def python_binary(srcs = [], base_module = None, **kwargs): _unused = srcs # @unused + if base_module == None: + base_module = default_base_module() + # @lint-ignore BUCKLINT: avoid "Direct usage of native rules is not allowed." - native.python_binary(**kwargs) + native.python_binary(base_module = base_module, **kwargs) diff --git a/shim/build_defs/python_library.bzl b/shim/build_defs/python_library.bzl index 438473ffeac6a..ad8bc8c5f0e07 100644 --- a/shim/build_defs/python_library.bzl +++ b/shim/build_defs/python_library.bzl @@ -6,8 +6,14 @@ # of this source tree. You may select, at your option, one of the # above-listed licenses. -def python_library(srcs = [], visibility = ["PUBLIC"], **kwargs): - _unused = srcs # @unused +load("@shim//build_defs/lib:oss.bzl", "default_base_module", "translate_target") + +def python_library(srcs = [], visibility = ["PUBLIC"], deps = None, base_module = None, **kwargs): + if deps != None: + kwargs["deps"] = [translate_target(d) for d in deps] + + if base_module == None: + base_module = default_base_module() # @lint-ignore BUCKLINT: avoid "Direct usage of native rules is not allowed." - native.python_library(visibility = visibility, **kwargs) + native.python_library(srcs = srcs, visibility = visibility, base_module = base_module, **kwargs) diff --git a/shim/build_defs/python_unittest.bzl b/shim/build_defs/python_unittest.bzl index 031c738495706..5caeaa549ef13 100644 --- a/shim/build_defs/python_unittest.bzl +++ b/shim/build_defs/python_unittest.bzl @@ -6,8 +6,13 @@ # of this source tree. You may select, at your option, one of the # above-listed licenses. -def python_unittest(srcs = [], py_version = None, **kwargs): +load("@shim//build_defs/lib:oss.bzl", "default_base_module") + +def python_unittest(srcs = [], py_version = None, base_module = None, **kwargs): _unused = (srcs, py_version) # @unused + if base_module == None: + base_module = default_base_module() + # @lint-ignore BUCKLINT: avoid "Direct usage of native rules is not allowed." - native.python_test(**kwargs) + native.python_test(base_module = base_module, **kwargs) From fb4ae7761792bd70008bf4effb0e84bad79c2495 Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Fri, 1 May 2026 11:17:04 -0700 Subject: [PATCH 8/9] shim: add python_pytest macro and OSS stubs for buck_e2e infra Wraps native.python_test to provide a pytest-based test runner. Adds the minimum stubs needed for tests/buck_e2e.bzl to load in OSS: a wrapper forwarding to the real buck_e2e.bzl in the root cell, no-op modifier and ci_hint stubs, and a remove_labels addition to the existing ci stub. --- .buckconfig | 3 +- shim/buck2/app/modifier.bzl | 17 ++ shim/buck2/tests/buck_e2e.bzl | 22 +++ shim/build_defs/python_pytest.bzl | 149 ++++++++++++++++++ shim/tools/target_determinator/macros/ci.bzl | 4 + .../target_determinator/macros/ci_hint.bzl | 13 ++ 6 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 shim/buck2/app/modifier.bzl create mode 100644 shim/buck2/tests/buck_e2e.bzl create mode 100644 shim/build_defs/python_pytest.bzl create mode 100644 shim/tools/target_determinator/macros/ci_hint.bzl diff --git a/.buckconfig b/.buckconfig index fbe94c90ddc62..6052d88a242d1 100644 --- a/.buckconfig +++ b/.buckconfig @@ -15,8 +15,7 @@ ignore = \ app/buck2_explain, \ app_dep_graph_rules, \ examples, \ - integrations/rust-project/tests, \ - tests + integrations/rust-project/tests [rust] default_edition = 2024 diff --git a/shim/buck2/app/modifier.bzl b/shim/buck2/app/modifier.bzl new file mode 100644 index 0000000000000..f992bb8aeff21 --- /dev/null +++ b/shim/buck2/app/modifier.bzl @@ -0,0 +1,17 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is dual-licensed under either the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree or the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. You may select, at your option, one of the +# above-listed licenses. + +# OSS stubs for the Meta-internal `ovr_config//`-driven modifier set used by +# `tests/buck_e2e.bzl`. The OSS build doesn't have those config targets, so +# return empty lists. + +def buck2_modifiers(): + return [] + +def disable_buck2_modifiers(): + return [] diff --git a/shim/buck2/tests/buck_e2e.bzl b/shim/buck2/tests/buck_e2e.bzl new file mode 100644 index 0000000000000..d0dd9823d91e1 --- /dev/null +++ b/shim/buck2/tests/buck_e2e.bzl @@ -0,0 +1,22 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is dual-licensed under either the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree or the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. You may select, at your option, one of the +# above-listed licenses. + +# Loads `@fbcode//buck2/tests:buck_e2e.bzl` resolve to the shim cell because +# `fbcode` is aliased to the shim cell in OSS. Forward to the real file in the +# buck2 root cell so the e2e tests can actually be parsed. + +load( + "@gh_facebook_buck2//tests:buck_e2e.bzl", + _buck2_core_tests = "buck2_core_tests", + _buck2_e2e_test = "buck2_e2e_test", + _buck_e2e_test = "buck_e2e_test", +) + +buck2_core_tests = _buck2_core_tests +buck2_e2e_test = _buck2_e2e_test +buck_e2e_test = _buck_e2e_test diff --git a/shim/build_defs/python_pytest.bzl b/shim/build_defs/python_pytest.bzl new file mode 100644 index 0000000000000..88df83f76e4c1 --- /dev/null +++ b/shim/build_defs/python_pytest.bzl @@ -0,0 +1,149 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is dual-licensed under either the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree or the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. You may select, at your option, one of the +# above-listed licenses. + +load("@prelude//utils:type_defs.bzl", "is_select") +load("@shim//build_defs/lib:oss.bzl", "default_base_module", "translate_target") + +def _build_addopts(pytest_config, pytest_marks, pytest_expr, pytest_confcutdir): + parts = [] + if pytest_config: + parts += ["-c", pytest_config] + if pytest_marks: + parts += ["-m", pytest_marks] + if pytest_expr: + parts += ["-k", pytest_expr] + if pytest_confcutdir: + parts += ["--confcutdir", pytest_confcutdir] + return parts + +def _src_to_module(src, base_module): + if not src.endswith(".py"): + fail("python_pytest sources must be .py files: {}".format(src)) + relative = src[:-3].replace("/", ".") + return base_module + "." + relative if base_module else relative + +def _shell_single_quote(s): + return "'" + s.replace("'", "'\\''") + "'" + +def _translate_env_value_str(v): + # Rewrite any cell-qualified targets that appear inside `$(macro ...)` + # substrings (e.g. `$(location fbcode//buck2:buck2)`) so that env values + # produced by the Meta-internal `tests/buck_e2e.bzl` resolve correctly + # through the OSS shim cell mapping. + if "//" not in v: + return v + chunks = v.split("$(") + out = [chunks[0]] + for chunk in chunks[1:]: + if ")" not in chunk: + out.append("$(" + chunk) + continue + end = chunk.index(")") + macro_body = chunk[:end] + rest = chunk[end:] + tokens = macro_body.split(" ") + for i in range(len(tokens)): + if "//" in tokens[i]: + tokens[i] = translate_target(tokens[i]) + out.append("$(" + " ".join(tokens) + rest) + return "".join(out) + +def _translate_env_value(v): + if is_select(v): + return select_map(v, _translate_env_value_str) + return _translate_env_value_str(v) + +def python_pytest( + name, + srcs = [], + env = None, + emails = None, + base_module = None, + deps = None, + resources = None, + pytest_config = None, + pytest_marks = None, + pytest_expr = None, + pytest_confcutdir = None, + skip_on_mode_mac = False, + skip_on_mode_win = False, + modifiers = None, + **kwargs): + # The shim has no native handling for these meta-internal attrs; tests + # silently run on all platforms in OSS. + _unused = (skip_on_mode_mac, skip_on_mode_win, modifiers) # @unused + + if deps != None: + kwargs["deps"] = [translate_target(d) for d in deps] + + if resources != None: + # Meta's `python_pytest` takes resources as `{target: dest_path}`; + # the OSS `python_test` rule takes `{dest_path: target}`. Flip and + # rewrite the target paths to their shim equivalents. + kwargs["resources"] = { + dest: translate_target(target) + for target, dest in resources.items() + } + + env = {k: _translate_env_value(v) for k, v in env.items()} if env else {} + extra = _build_addopts(pytest_config, pytest_marks, pytest_expr, pytest_confcutdir) + + # The buck2 e2e tests use an async `buck` pytest fixture; pytest-asyncio's + # default "strict" mode requires explicit fixture marking, which the + # upstream tests don't have. Meta's internal harness sets this elsewhere. + extra = ["--asyncio-mode=auto"] + extra + if extra: + existing = env.get("PYTEST_ADDOPTS", "").strip() + joined = " ".join(extra) + env["PYTEST_ADDOPTS"] = (existing + " " + joined) if existing else joined + + module_base = base_module if base_module != None else default_base_module() + test_modules = [_src_to_module(s, module_base) for s in srcs] + + main_lines = [ + "import sys", + "import pytest", + "", + "TEST_MODULES = [", + ] + [' "{}",'.format(m) for m in test_modules] + [ + "]", + "", + "if __name__ == \"__main__\":", + " sys.exit(pytest.main([\"--pyargs\"] + TEST_MODULES + sys.argv[1:]))", + ] + quoted_lines = " ".join([_shell_single_quote(l) for l in main_lines]) + + main_genrule = "__{}__pytest_main".format(name) + + # @lint-ignore BUCKLINT: avoid "Direct usage of native rules is not allowed." + native.genrule( + name = main_genrule, + out = "__pytest_main__.py", + cmd = "printf '%s\\n' " + quoted_lines + " > $OUT", + ) + + if emails != None: + kwargs["contacts"] = emails + + # The buck2 e2e tests look up `__manifest__.fbmake["build_rule"]` (a Meta + # convention) to derive a stable isolation prefix per test. Provide a + # minimally compatible shape so tests that read this don't crash in OSS. + kwargs.setdefault( + "manifest_module_entries", + {"fbmake": {"build_rule": "{}:{}".format(native.package_name(), name)}}, + ) + + # @lint-ignore BUCKLINT: avoid "Direct usage of native rules is not allowed." + native.python_test( + name = name, + srcs = list(srcs) + [":" + main_genrule], + env = env, + base_module = module_base, + main_module = (module_base + "." if module_base else "") + "__pytest_main__", + **kwargs + ) diff --git a/shim/tools/target_determinator/macros/ci.bzl b/shim/tools/target_determinator/macros/ci.bzl index 4544cea70c64b..b46bd8d3de016 100644 --- a/shim/tools/target_determinator/macros/ci.bzl +++ b/shim/tools/target_determinator/macros/ci.bzl @@ -20,6 +20,9 @@ def _package( def _labels(*args): return [] +def _remove_labels(*_args): + return [] + ci = struct( package = _package, linux = _lbl, @@ -30,4 +33,5 @@ ci = struct( mode = _lbl, opt = _lbl, labels = _labels, + remove_labels = _remove_labels, ) diff --git a/shim/tools/target_determinator/macros/ci_hint.bzl b/shim/tools/target_determinator/macros/ci_hint.bzl new file mode 100644 index 0000000000000..c017ec63476b4 --- /dev/null +++ b/shim/tools/target_determinator/macros/ci_hint.bzl @@ -0,0 +1,13 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is dual-licensed under either the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree or the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. You may select, at your option, one of the +# above-listed licenses. + +# OSS stub for `@fbsource//tools/target_determinator/macros:ci_hint.bzl`. +# `ci_hint` is a no-op outside the Meta CI environment. + +def ci_hint(**_kwargs): + pass From 17a8b132600f5a36a07ed904d1521bde3d55f599 Mon Sep 17 00:00:00 2001 From: Jade Lovelace Date: Fri, 1 May 2026 11:23:44 -0700 Subject: [PATCH 9/9] OSS: shim out clifoundation This was required for analysis. Claude stubbed it. --- .../cli_target/buck_defs/cli.bzl | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 shim/clifoundation/cli_target/buck_defs/cli.bzl diff --git a/shim/clifoundation/cli_target/buck_defs/cli.bzl b/shim/clifoundation/cli_target/buck_defs/cli.bzl new file mode 100644 index 0000000000000..718ccb5b3b596 --- /dev/null +++ b/shim/clifoundation/cli_target/buck_defs/cli.bzl @@ -0,0 +1,45 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is dual-licensed under either the MIT license found in the +# LICENSE-MIT file in the root directory of this source tree or the Apache +# License, Version 2.0 found in the LICENSE-APACHE file in the root directory +# of this source tree. You may select, at your option, one of the +# above-listed licenses. + +# OSS stub for `@fbcode//clifoundation/cli_target/buck_defs:cli.bzl`. The Meta +# CLI deployer infrastructure isn't ported to OSS — `cli.deployer(...)` is a +# no-op so its argument-builder helpers can return any placeholder value. + +def _noop(**_kwargs): + pass + +def _placeholder(*_args, **_kwargs): + return None + +_TOKEN = struct() + +cli = struct( + deployer = _noop, + distribution = _placeholder, + bump_diffs = _placeholder, + destination = _placeholder, + repository = struct(FBSOURCE = _TOKEN), + metadata = _placeholder, + criticality = _placeholder, + level = struct(NOT_CRITICAL = _TOKEN), + packaging = _placeholder, + dotslash = _placeholder, + platform = struct( + linux = _placeholder, + macos = _placeholder, + windows = _placeholder, + ), + architecture = struct(AARCH64 = _TOKEN, X86_64 = _TOKEN), + release = _placeholder, + artifact_ci_config = _placeholder, + conveyor = _placeholder, + frequency = struct(DAILY = _TOKEN), + holiday_country = struct(UNITED_STATES = _TOKEN), + srconveyor = _placeholder, + rollout = struct(AT_ONCE = _TOKEN), +)