build(python): vendor rpycdec #17

Merged
alexion merged 3 commits from vendor-rpycdec-python into main 2026-09-06 17:24:42 -04:00
63 changed files with 6709 additions and 464 deletions

View File

@@ -4,9 +4,14 @@ module(
) )
bazel_dep(name = "bazel_skylib", version = "1.8.2") bazel_dep(name = "bazel_skylib", version = "1.8.2")
bazel_dep(name = "rules_python", version = "1.6.3")
bazel_dep(name = "rules_rust", version = "0.74.0") bazel_dep(name = "rules_rust", version = "0.74.0")
python = use_extension("//bazel:python.bzl", "python")
use_repo(python, "python")
rust = use_extension("//bazel:rust.bzl", "rust") rust = use_extension("//bazel:rust.bzl", "rust")
use_repo(rust, "rust") use_repo(rust, "rust")
register_toolchains("@python//:python")
register_toolchains("@rust//:rust") register_toolchains("@rust//:rust")

638
MODULE.bazel.lock generated
View File

@@ -6,35 +6,53 @@
"https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589",
"https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0", "https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb", "https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/source.json": "14892cc698e02ffedf4967546e6bedb7245015906888d3465fcf27c90a26da10", "https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16",
"https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da",
"https://bcr.bazel.build/modules/apple_support/1.24.1/MODULE.bazel": "f46e8ddad60aef170ee92b2f3d00ef66c147ceafea68b6877cb45bd91737f5f8", "https://bcr.bazel.build/modules/apple_support/1.24.1/MODULE.bazel": "f46e8ddad60aef170ee92b2f3d00ef66c147ceafea68b6877cb45bd91737f5f8",
"https://bcr.bazel.build/modules/apple_support/1.24.1/source.json": "cf725267cbacc5f028ef13bb77e7f2c2e0066923a4dab1025e4a0511b1ed258a", "https://bcr.bazel.build/modules/apple_support/1.24.1/source.json": "cf725267cbacc5f028ef13bb77e7f2c2e0066923a4dab1025e4a0511b1ed258a",
"https://bcr.bazel.build/modules/apple_support/1.5.0/MODULE.bazel": "50341a62efbc483e8a2a6aec30994a58749bd7b885e18dd96aa8c33031e558ef", "https://bcr.bazel.build/modules/apple_support/1.5.0/MODULE.bazel": "50341a62efbc483e8a2a6aec30994a58749bd7b885e18dd96aa8c33031e558ef",
"https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
"https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8",
"https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d",
"https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d",
"https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a",
"https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58",
"https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
"https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65", "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
"https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
"https://bcr.bazel.build/modules/bazel_features/1.50.0/MODULE.bazel": "2083ef9c7a469f520890483ccf8e0189d6e71e2117e7752e15e6554433d5ae3e", "https://bcr.bazel.build/modules/bazel_features/1.50.0/MODULE.bazel": "2083ef9c7a469f520890483ccf8e0189d6e71e2117e7752e15e6554433d5ae3e",
"https://bcr.bazel.build/modules/bazel_features/1.50.0/source.json": "e0ee3debde2789ff56e4452e612d126925ba9ab64d4bde79c67f099d2902df9b", "https://bcr.bazel.build/modules/bazel_features/1.50.0/source.json": "e0ee3debde2789ff56e4452e612d126925ba9ab64d4bde79c67f099d2902df9b",
"https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
"https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
"https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
"https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a", "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a",
"https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5", "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5",
"https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d", "https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d",
"https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651",
"https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138",
"https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917", "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917",
"https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d",
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b", "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb", "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb",
"https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84", "https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84",
"https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8", "https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8",
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
"https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/source.json": "41e9e129f80d8c8bf103a7acc337b76e54fad1214ac0a7084bf24f4cd924b8b4",
"https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f", "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f",
"https://bcr.bazel.build/modules/googletest/1.14.0/source.json": "2478949479000fdd7de9a3d0107ba2c85bb5f961c3ecb1aa448f52549ce310b5",
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075",
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d", "https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d",
"https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902",
"https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83", "https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83",
"https://bcr.bazel.build/modules/package_metadata/0.0.3/source.json": "742075a428ad12a3fa18a69014c2f57f01af910c6d9d18646c990200853e641a", "https://bcr.bazel.build/modules/package_metadata/0.0.3/source.json": "742075a428ad12a3fa18a69014c2f57f01af910c6d9d18646c990200853e641a",
"https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
"https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f",
"https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee",
"https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37",
"https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615",
@@ -45,10 +63,22 @@
"https://bcr.bazel.build/modules/platforms/1.1.0/source.json": "fcf351c47596c939140ab0d333dfdd08ed1ea6ce33c2fe70c12493a301cf1344", "https://bcr.bazel.build/modules/platforms/1.1.0/source.json": "fcf351c47596c939140ab0d333dfdd08ed1ea6ce33c2fe70c12493a301cf1344",
"https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7",
"https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c",
"https://bcr.bazel.build/modules/protobuf/27.0/source.json": "1acf3d080c728d42f423fde5422fd0a1a24f44c15908124ce12363a253384193", "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df",
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92",
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/source.json": "c16a6488fb279ef578da7098e605082d72ed85fc8d843eaae81e7d27d0f4625d",
"https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0",
"https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858", "https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858",
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e",
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/source.json": "be4789e951dd5301282729fe3d4938995dc4c1a81c2ff150afc9f1b0504c6022",
"https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206",
"https://bcr.bazel.build/modules/re2/2023-09-01/source.json": "e044ce89c2883cd957a2969a43e79f7752f9656f6b20050b62f90ede21ec6eb4",
"https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8",
"https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e",
"https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
"https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002",
"https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191",
"https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc",
"https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87",
"https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c",
"https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f",
"https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
@@ -56,41 +86,83 @@
"https://bcr.bazel.build/modules/rules_cc/0.2.4/MODULE.bazel": "1ff1223dfd24f3ecf8f028446d4a27608aa43c3f41e346d22838a4223980b8cc", "https://bcr.bazel.build/modules/rules_cc/0.2.4/MODULE.bazel": "1ff1223dfd24f3ecf8f028446d4a27608aa43c3f41e346d22838a4223980b8cc",
"https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642", "https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642",
"https://bcr.bazel.build/modules/rules_cc/0.2.8/source.json": "85087982aca15f31307bd52698316b28faa31bd2c3095a41f456afec0131344c", "https://bcr.bazel.build/modules/rules_cc/0.2.8/source.json": "85087982aca15f31307bd52698316b28faa31bd2c3095a41f456afec0131344c",
"https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8",
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/source.json": "c8b1e2c717646f1702290959a3302a178fb639d987ab61d548105019f11e527e",
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
"https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31",
"https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a",
"https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6",
"https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab",
"https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
"https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1", "https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1",
"https://bcr.bazel.build/modules/rules_java/7.6.5/source.json": "a805b889531d1690e3c72a7a7e47a870d00323186a9904b36af83aa3d053ee8d", "https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017",
"https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939",
"https://bcr.bazel.build/modules/rules_java/8.5.1/source.json": "db1a77d81b059e0f84985db67a22f3f579a529a86b7997605be3d214a0abe38e",
"https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
"https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909", "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909",
"https://bcr.bazel.build/modules/rules_jvm_external/5.1/source.json": "5abb45cc9beb27b77aec6a65a11855ef2b55d95dfdc358e9f312b78ae0ba32d5", "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036",
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0",
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/source.json": "6f5f5a5a4419ae4e37c35a5bb0a6ae657ed40b7abc5a5189111b47fcebe43197",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5",
"https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0",
"https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d",
"https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c", "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c",
"https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb", "https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb",
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/source.json": "c2557066e0c0342223ba592510ad3d812d4963b9024831f7f66fd0584dd8c66c", "https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a",
"https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/source.json": "d57902c052424dfda0e71646cb12668d39c4620ee0544294d9d941e7d12bc3a9", "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73",
"https://bcr.bazel.build/modules/rules_proto/6.0.2/source.json": "17a2e195f56cb28d6bbf763e49973d13890487c6945311ed141e196fb660426d",
"https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f",
"https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7", "https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7",
"https://bcr.bazel.build/modules/rules_python/0.22.1/source.json": "57226905e783bae7c37c2dd662be078728e48fa28ee4324a7eabcafb5a43d014", "https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300",
"https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382",
"https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed",
"https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58",
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
"https://bcr.bazel.build/modules/rules_python/1.6.3/MODULE.bazel": "a7b80c42cb3de5ee2a5fa1abc119684593704fcd2fec83165ebe615dec76574f",
"https://bcr.bazel.build/modules/rules_python/1.6.3/source.json": "f0be74977e5604a6526c8a416cda22985093ff7d5d380d41722d7e44015cc419",
"https://bcr.bazel.build/modules/rules_rust/0.74.0/MODULE.bazel": "6e6fa04d7c8202f9cd63205ec9f96da39382a4ac971ee1d7a900fe8e1f68be17", "https://bcr.bazel.build/modules/rules_rust/0.74.0/MODULE.bazel": "6e6fa04d7c8202f9cd63205ec9f96da39382a4ac971ee1d7a900fe8e1f68be17",
"https://bcr.bazel.build/modules/rules_rust/0.74.0/source.json": "04eac31b4536eb23dd8e8e8ca7e079d3c1c4ac99934d38aa54bbae36917c7507", "https://bcr.bazel.build/modules/rules_rust/0.74.0/source.json": "04eac31b4536eb23dd8e8e8ca7e079d3c1c4ac99934d38aa54bbae36917c7507",
"https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
"https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b", "https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b",
"https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c", "https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c",
"https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
"https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c", "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
"https://bcr.bazel.build/modules/stardoc/0.5.3/source.json": "cd53fe968dc8cd98197c052db3db6d82562960c87b61e7a90ee96f8e4e0dda97", "https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c",
"https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
"https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27", "https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79", "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79",
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d" "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d",
"https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198"
}, },
"selectedYankedVersions": {}, "selectedYankedVersions": {},
"moduleExtensions": { "moduleExtensions": {
"//bazel:python.bzl%python": {
"general": {
"bzlTransitiveDigest": "CbGiMYJXjcea8dmoNxUWe/lWOjqRWfQPTXFHUUMheIY=",
"usagesDigest": "bcY43VsTrhTJ8N4Dj5FtsgMhZxLyKKjHP+wWK4ntJQ4=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"python": {
"bzlFile": "@@//bazel:python.bzl",
"ruleClassName": "python_repository",
"attributes": {}
}
},
"recordedRepoMappingEntries": []
}
},
"//bazel:rust.bzl%rust": { "//bazel:rust.bzl%rust": {
"general": { "general": {
"bzlTransitiveDigest": "4kUzQyjRjzchWIBZJKkNs8LuO4F+g/DwuwtTa0RTPcU=", "bzlTransitiveDigest": "4kUzQyjRjzchWIBZJKkNs8LuO4F+g/DwuwtTa0RTPcU=",
@@ -108,6 +180,29 @@
"recordedRepoMappingEntries": [] "recordedRepoMappingEntries": []
} }
}, },
"@@rules_java~//java:rules_java_deps.bzl%compatibility_proxy": {
"general": {
"bzlTransitiveDigest": "KIX40nDfygEWbU+rq3nYpt3tVgTK/iO8PKh5VMBlN7M=",
"usagesDigest": "pwHZ+26iLgQdwvdZeA5wnAjKnNI3y6XO2VbhOTeo5h8=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"compatibility_proxy": {
"bzlFile": "@@rules_java~//java:rules_java_deps.bzl",
"ruleClassName": "_compatibility_proxy_repo_rule",
"attributes": {}
}
},
"recordedRepoMappingEntries": [
[
"rules_java~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@rules_jvm_external~//:extensions.bzl%maven": { "@@rules_jvm_external~//:extensions.bzl%maven": {
"general": { "general": {
"bzlTransitiveDigest": "twF5CTH+cMNcSDOS8HgBaxaIQ+FsdzZBKM0EbgJoYGU=", "bzlTransitiveDigest": "twF5CTH+cMNcSDOS8HgBaxaIQ+FsdzZBKM0EbgJoYGU=",
@@ -1123,47 +1218,100 @@
] ]
} }
}, },
"@@rules_jvm_external~//:non-module-deps.bzl%non_module_deps": { "@@rules_kotlin~//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
"general": { "general": {
"bzlTransitiveDigest": "ZOivBbbZUakRexeLO/N26oX4Bcph6HHnqNmfxt7yoCc=", "bzlTransitiveDigest": "fus14IFJ/1LGWWGKPH/U18VnJCoMjfDt1ckahqCnM0A=",
"usagesDigest": "SgmaWbjiY+8dgpUSia89kYJqNm3A67fr0hnvRaklrV4=", "usagesDigest": "aJF6fLy82rR95Ff5CZPAqxNoFgOMLMN5ImfBS0nhnkg=",
"recordedFileInputs": {}, "recordedFileInputs": {},
"recordedDirentsInputs": {}, "recordedDirentsInputs": {},
"envVariables": {}, "envVariables": {},
"generatedRepoSpecs": { "generatedRepoSpecs": {
"io_bazel_rules_kotlin": { "com_github_jetbrains_kotlin_git": {
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:compiler.bzl",
"ruleClassName": "kotlin_compiler_git_repository",
"attributes": {
"urls": [
"https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip"
],
"sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88"
}
},
"com_github_jetbrains_kotlin": {
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:compiler.bzl",
"ruleClassName": "kotlin_capabilities_repository",
"attributes": {
"git_repository_name": "com_github_jetbrains_kotlin_git",
"compiler_version": "1.9.23"
}
},
"com_github_google_ksp": {
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:ksp.bzl",
"ruleClassName": "ksp_compiler_plugin_repository",
"attributes": {
"urls": [
"https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip"
],
"sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d",
"strip_version": "1.9.23-1.0.20"
}
},
"com_github_pinterest_ktlint": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_file",
"attributes": {
"sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985",
"urls": [
"https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint"
],
"executable": true
}
},
"rules_android": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl", "bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive", "ruleClassName": "http_archive",
"attributes": { "attributes": {
"sha256": "946747acdbeae799b085d12b240ec346f775ac65236dfcf18aa0cd7300f6de78", "sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806",
"strip_prefix": "rules_android-0.1.1",
"urls": [ "urls": [
"https://github.com/bazelbuild/rules_kotlin/releases/download/v1.7.0-RC-2/rules_kotlin_release.tgz" "https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip"
] ]
} }
} }
}, },
"recordedRepoMappingEntries": [ "recordedRepoMappingEntries": [
[ [
"rules_jvm_external~", "rules_kotlin~",
"bazel_tools", "bazel_tools",
"bazel_tools" "bazel_tools"
] ]
] ]
} }
}, },
"@@rules_python~//python/extensions:python.bzl%python": { "@@rules_python~//python/uv:uv.bzl%uv": {
"general": { "general": {
"bzlTransitiveDigest": "lbXqTyC4ahBb81TIrIp+2d3sWnlurVNqSeAaLJknLUs=", "bzlTransitiveDigest": "mxPY/VBQrSC9LvYeRrlxD+0IkDTQ4+36NGMnGWlN/Vw=",
"usagesDigest": "1Y6kbygksx7wAtDStFoHnR90xr8Yeq00I91YcLMbxMI=", "usagesDigest": "qIeg3YEigU6oZD/nE0vQzliorEcL5dtbidAIP+ZhylM=",
"recordedFileInputs": {}, "recordedFileInputs": {},
"recordedDirentsInputs": {}, "recordedDirentsInputs": {},
"envVariables": {}, "envVariables": {},
"generatedRepoSpecs": { "generatedRepoSpecs": {
"pythons_hub": { "uv": {
"bzlFile": "@@rules_python~//python/extensions/private:interpreter_hub.bzl", "bzlFile": "@@rules_python~//python/uv/private:uv_toolchains_repo.bzl",
"ruleClassName": "hub_repo", "ruleClassName": "uv_toolchains_repo",
"attributes": { "attributes": {
"toolchains": [] "toolchain_type": "'@@rules_python~//python/uv:uv_toolchain_type'",
"toolchain_names": [
"none"
],
"toolchain_implementations": {
"none": "'@@rules_python~//python:none'"
},
"toolchain_compatible_with": {
"none": [
"@platforms//:incompatible"
]
},
"toolchain_target_settings": {}
} }
} }
}, },
@@ -1175,446 +1323,8 @@
], ],
[ [
"rules_python~", "rules_python~",
"rules_python", "platforms",
"rules_python~" "platforms"
]
]
}
},
"@@rules_python~//python/extensions/private:internal_deps.bzl%internal_deps": {
"general": {
"bzlTransitiveDigest": "b6FMQSdoZ1QOssw14AW8bWDn2BvywI4FVkLbO2nTMsE=",
"usagesDigest": "KPNj8wxzOk7dXY9StqZ91MCKEIJSEnAyV0Q/dGFP5sw=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"pypi__build": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/03/97/f58c723ff036a8d8b4d3115377c0a37ed05c1f68dd9a0d66dab5e82c5c1c/build-0.9.0-py3-none-any.whl",
"sha256": "38a7a2b7a0bdc61a42a0a67509d88c71ecfc37b393baba770fae34e20929ff69",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__click": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/76/0a/b6c5f311e32aeb3b406e03c079ade51e905ea630fc19d1262a46249c1c86/click-8.0.1-py3-none-any.whl",
"sha256": "fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__colorama": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl",
"sha256": "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__installer": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl",
"sha256": "05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__packaging": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/8f/7b/42582927d281d7cb035609cd3a543ffac89b74f3f4ee8e1c50914bcb57eb/packaging-22.0-py3-none-any.whl",
"sha256": "957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__pep517": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/ee/2f/ef63e64e9429111e73d3d6cbee80591672d16f2725e648ebc52096f3d323/pep517-0.13.0-py3-none-any.whl",
"sha256": "4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__pip": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/09/bd/2410905c76ee14c62baf69e3f4aa780226c1bbfc9485731ad018e35b0cb5/pip-22.3.1-py3-none-any.whl",
"sha256": "908c78e6bc29b676ede1c4d57981d490cb892eb45cd8c214ab6298125119e077",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__pip_tools": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/5e/e8/f6d7d1847c7351048da870417724ace5c4506e816b38db02f4d7c675c189/pip_tools-6.12.1-py3-none-any.whl",
"sha256": "f0c0c0ec57b58250afce458e2e6058b1f30a4263db895b7d72fd6311bf1dc6f7",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__setuptools": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/7c/5b/3d92b9f0f7ca1645cba48c080b54fe7d8b1033a4e5720091d1631c4266db/setuptools-60.10.0-py3-none-any.whl",
"sha256": "782ef48d58982ddb49920c11a0c5c9c0b02e7d7d1c2ad0aa44e1a1e133051c96",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__tomli": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl",
"sha256": "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__wheel": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/bd/7c/d38a0b30ce22fc26ed7dbc087c6d00851fb3395e9d0dac40bec1f905030c/wheel-0.38.4-py3-none-any.whl",
"sha256": "b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__importlib_metadata": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/d7/31/74dcb59a601b95fce3b0334e8fc9db758f78e43075f22aeb3677dfb19f4c/importlib_metadata-1.4.0-py2.py3-none-any.whl",
"sha256": "bdd9b7c397c273bcc9a11d6629a38487cd07154fa255a467bf704cd2c258e359",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__zipp": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/f4/50/cc72c5bcd48f6e98219fc4a88a5227e9e28b81637a99c49feba1d51f4d50/zipp-1.0.0-py2.py3-none-any.whl",
"sha256": "8dda78f06bd1674bd8720df8a50bb47b6e1233c503a4eed8e7810686bde37656",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__more_itertools": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"url": "https://files.pythonhosted.org/packages/bd/3f/c4b3dbd315e248f84c388bd4a72b131a29f123ecacc37ffb2b3834546e42/more_itertools-8.13.0-py3-none-any.whl",
"sha256": "c5122bffc5f104d37c1626b8615b511f3427aa5389b94d61e5ef8236bfbc3ddb",
"type": "zip",
"build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:defs.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude in /python/pip_install/tools/bazel.py\n # to avoid non-determinism following pip install's behavior.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/* *\",\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n"
}
},
"pypi__coverage_cp38_aarch64-apple-darwin": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/07/82/79fa21ceca9a9b091eb3c67e27eb648dade27b2c9e1eb23af47232a2a365/coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl"
]
}
},
"pypi__coverage_cp38_aarch64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/40/3b/cd68cb278c4966df00158811ec1e357b9a7d132790c240fc65da57e10013/coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
]
}
},
"pypi__coverage_cp38_x86_64-apple-darwin": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/05/63/a789b462075395d34f8152229dccf92b25ca73eac05b3f6cd75fa5017095/coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl"
]
}
},
"pypi__coverage_cp38_x86_64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/bd/a0/e263b115808226fdb2658f1887808c06ac3f1b579ef5dda02309e0d54459/coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
]
}
},
"pypi__coverage_cp39_aarch64-apple-darwin": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/63/e9/f23e8664ec4032d7802a1cf920853196bcbdce7b56408e3efe1b2da08f3c/coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl"
]
}
},
"pypi__coverage_cp39_aarch64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/18/95/27f80dcd8273171b781a19d109aeaed7f13d78ef6d1e2f7134a5826fd1b4/coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
]
}
},
"pypi__coverage_cp39_x86_64-apple-darwin": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/ea/52/c08080405329326a7ff16c0dfdb4feefaa8edd7446413df67386fe1bbfe0/coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl"
]
}
},
"pypi__coverage_cp39_x86_64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/6b/f2/919f0fdc93d3991ca074894402074d847be8ac1e1d78e7e9e1c371b69a6f/coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
]
}
},
"pypi__coverage_cp310_aarch64-apple-darwin": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/89/a2/cbf599e50bb4be416e0408c4cf523c354c51d7da39935461a9687e039481/coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl"
]
}
},
"pypi__coverage_cp310_aarch64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/15/b0/3639d84ee8a900da0cf6450ab46e22517e4688b6cec0ba8ab6f8166103a2/coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
]
}
},
"pypi__coverage_cp310_x86_64-apple-darwin": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/c4/8d/5ec7d08f4601d2d792563fe31db5e9322c306848fec1e65ec8885927f739/coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl"
]
}
},
"pypi__coverage_cp310_x86_64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/3c/7d/d5211ea782b193ab8064b06dc0cc042cf1a4ca9c93a530071459172c550f/coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
]
}
},
"pypi__coverage_cp311_aarch64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/36/f3/5cbd79cf4cd059c80b59104aca33b8d05af4ad5bf5b1547645ecee716378/coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl"
]
}
},
"pypi__coverage_cp311_x86_64-apple-darwin": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/50/cf/455930004231fa87efe8be06d13512f34e070ddfee8b8bf5a050cdc47ab3/coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl"
]
}
},
"pypi__coverage_cp311_x86_64-unknown-linux-gnu": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file_content": "\nfilegroup(\n name = \"coverage\",\n srcs = [\"coverage/__main__.py\"],\n data = glob([\"coverage/*.py\", \"coverage/**/*.py\", \"coverage/*.so\"]),\n visibility = [\"//visibility:public\"],\n)\n ",
"patch_args": [
"-p1"
],
"patches": [
"@@rules_python~//python/private:coverage.patch"
],
"sha256": "a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91",
"type": "zip",
"urls": [
"https://files.pythonhosted.org/packages/6a/63/8e82513b7e4a1b8d887b4e85c1c2b6c9b754a581b187c0b084f3330ac479/coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
]
}
}
},
"recordedRepoMappingEntries": [
[
"rules_python~",
"bazel_skylib",
"bazel_skylib~"
],
[
"rules_python~",
"bazel_tools",
"bazel_tools"
],
[
"rules_python~",
"rules_python",
"rules_python~"
] ]
] ]
} }

View File

@@ -1,4 +1,6 @@
exports_files([ exports_files([
"python.BUILD.template",
"python.bzl",
"rust.BUILD.template", "rust.BUILD.template",
"rust.bzl", "rust.bzl",
"utils.bzl", "utils.bzl",

View File

@@ -0,0 +1,25 @@
load("@rules_python//python:py_runtime.bzl", "py_runtime")
load("@rules_python//python:py_runtime_pair.bzl", "py_runtime_pair")
py_runtime(
name = "runtime",
implementation_name = "cpython",
interpreter_path = "{interpreter_path}",
interpreter_version_info = {
"major": "{major}",
"minor": "{minor}",
"micro": "{micro}",
},
python_version = "PY3",
)
py_runtime_pair(
name = "runtime_pair",
py3_runtime = ":runtime",
)
toolchain(
name = "python",
toolchain = ":runtime_pair",
toolchain_type = "@rules_python//python:toolchain_type",
)

46
bazel/python.bzl Normal file
View File

@@ -0,0 +1,46 @@
load("//bazel:utils.bzl", "execute", "require_env")
def _python_repository_impl(repository_ctx):
root = repository_ctx.path(require_env(repository_ctx, "PYTHON_TOOLCHAIN_ROOT"))
python3 = root.get_child("bin/python3")
version = execute(
repository_ctx,
[
str(python3),
"-c",
"import sys; print('{}.{}.{}'.format(*sys.version_info[:3]))",
],
)
parts = version.split(".")
if len(parts) != 3:
fail("python3 did not report a major.minor.micro version")
repository_ctx.file("WORKSPACE.bazel", "")
repository_ctx.template(
"BUILD.bazel",
Label("//bazel:python.BUILD.template"),
{
"{interpreter_path}": str(python3),
"{major}": parts[0],
"{minor}": parts[1],
"{micro}": parts[2],
},
)
python_repository = repository_rule(
implementation = _python_repository_impl,
local = True,
environ = ["PYTHON_TOOLCHAIN_ROOT"],
)
def _python_impl(module_ctx):
python_repository(name = "python")
python = module_extension(
implementation = _python_impl,
)

View File

@@ -12,6 +12,7 @@ in
config = lib.mkIf cfg.enable { config = lib.mkIf cfg.enable {
modules.shell.bazel.enable = true; modules.shell.bazel.enable = true;
modules.shell.python.enable = true;
modules.shell.rust.enable = true; modules.shell.rust.enable = true;
}; };
} }

View File

@@ -0,0 +1,20 @@
{
config,
lib,
pkgs,
...
}:
# Python tools for the primary user's shell environment.
let
cfg = config.modules.shell.python;
user = config.user.name;
in
{
options.modules.shell.python.enable = lib.mkEnableOption "Python tools for the primary user's shell environment";
config = lib.mkIf cfg.enable {
registry.env-var.${config.networking.hostName}.PYTHON_TOOLCHAIN_ROOT = "${pkgs.python3}";
home-manager.users.${user}.home.packages = [ pkgs.python3 ];
};
}

16
third_party/python/BUILD.bazel vendored Normal file
View File

@@ -0,0 +1,16 @@
load("@rules_python//python:defs.bzl", "py_binary", "py_library")
package(default_visibility = ["//visibility:public"])
py_library(
name = "rpycdec",
srcs = glob(["rpycdec/src/**/*.py"]),
imports = ["rpycdec/src"],
)
py_binary(
name = "rpycdec_bin",
srcs = ["rpycdec_bin.py"],
main = "rpycdec_bin.py",
deps = [":rpycdec"],
)

41
third_party/python/README.md vendored Normal file
View File

@@ -0,0 +1,41 @@
# Vendored Python packages
This package exposes vendored Python dependencies through stable public Bazel targets.
Bazel uses `PYTHON_TOOLCHAIN_ROOT` to find the monorepo `python3` runtime.
Project code depends on targets in `//third_party/python`.
It does not depend on files inside vendored source directories directly.
Use this layout for a vendored package:
```text
third_party/python/
BUILD.bazel
<name>/
LICENSE
pyproject.toml
src/
```
Use the upstream package name for library targets.
For command-line tools, add a `_bin` suffix to the binary target.
Preserve upstream license files and package metadata.
Keep repository Bazel files outside upstream source when possible.
Add optional dependencies as separate public targets.
Do not bundle optional dependencies into the primary package target.
## `rpycdec`
Ariadne uses `rpycdec` for archive extraction and debug decompilation.
Ariadne uses the game-bundled Ren'Py runtime for AST export.
The vendored source comes from `cnfatal/rpycdec` at commit `489e9c6b8a48ed7904d7039d7787c36a03c75fe3`.
The public targets are:
```text
//third_party/python:rpycdec
//third_party/python:rpycdec_bin
```

View File

@@ -0,0 +1,37 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Upload Python Package
on:
release:
types: [published]
# on push tag
push:
tags:
- "v*.*.*"
permissions:
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # IMPORTANT: this permission is mandatory for trusted publishing
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v3
with:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@release/v1

15
third_party/python/rpycdec/.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
*.swp
*.pyc
*.pyo
*.rpyc
__pycache__
/build
/dist
/.vscode
/tmp
/sdk
/workspace
/.cache
/.venv
/sdks
.DS_Store

2
third_party/python/rpycdec/.pylintrc vendored Normal file
View File

@@ -0,0 +1,2 @@
[MASTER]
ignore-patterns=tests/*,docs/*,renpy/*

10
third_party/python/rpycdec/DEVELOP.md vendored Normal file
View File

@@ -0,0 +1,10 @@
# Development Guide
## How decompilation works
All rpyc files are compiled from rpy files, renpy SDK read every file and parse it to AST object(renpy), and then use pickle to serialize the AST to rpyc file. So we use pickle to deserialize the rpyc file to AST, and then restore the rpy file from AST.
We created a fake "renpy" package(unlikely unrpyc) and it's ast objects to make the pickle can be deserialized correctly.
Another reason to create a fake "renpy" package is that we can separate code generate logic (the `get_code(...)` function) to each AST object, it will be easier to future maintain and for multi-version renpy support.
The most difficult part is generating the rpy file from AST. Different renpy version has different AST structure also different grammar.

21
third_party/python/rpycdec/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) [2023] [cnfatal@gmail.com]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

21
third_party/python/rpycdec/Makefile vendored Normal file
View File

@@ -0,0 +1,21 @@
PYTHON ?= python
RUFF ?= ruff
.PHONY: wheels release test lint format
wheels:
$(PYTHON) -m build --wheel
release:
twine upload dist/*
test:
$(PYTHON) -m unittest discover -s tests
lint:
$(RUFF) check .
$(RUFF) format --check .
format:
$(RUFF) check . --fix
$(RUFF) format .

111
third_party/python/rpycdec/README.md vendored Normal file
View File

@@ -0,0 +1,111 @@
# rpycdec
A tool for decompiling Ren'py compiled script files (.rpyc and .rpymc).
## Features
- Decompile `.rpyc` and `.rpymc` files to readable Ren'Py script code
- Extract RPA archives
- Create RPA archives from existing files or directories
- Extract and edit Ren'Py save files (`.save` → JSON → `.save`)
- Extract Ren'Py games from Android APK files
- Extract translations from compiled scripts to `tl/{language}/` directories
- Support for multiple Ren'Py versions (7.x, 8.x)
## Installation
Install with pip:
```sh
pip install rpycdec
```
Or install from source:
```sh
git clone https://github.com/cnfatal/rpycdec.git
cd rpycdec
pip install .
```
## Usage
### Command Line Interface
Decompile a single file:
```sh
rpycdec decompile script.rpyc
```
Decompile all files in a directory:
```sh
rpycdec decompile /path/to/game/
```
Extract RPA archive:
```sh
rpycdec unrpa archive.rpa
```
Extract files whose archive paths match a regular expression:
```sh
rpycdec unrpa archive.rpa -e '\.rpyc?$'
```
Extract files with one of several suffixes (case-insensitive):
```sh
rpycdec unrpa archive.rpa -s .rpy .rpyc
```
`--expression`/`-e` may be repeated, as may `--suffix`/`-s`. When both are
used, a file is extracted if it matches any expression or any suffix.
Create RPA archive:
```sh
rpycdec rpa /path/to/game/files -o archive.rpa
```
Extract Ren'Py game from Android APK:
```sh
rpycdec extract-game game.apk
```
Extract translations:
```sh
rpycdec extract-translate /path/to/game/ -l Chinese
```
## Security Warning
This tool processes `.rpyc`, `.rpymc`, `.rpa`, and `.save` files which use Python's `pickle` format internally. rpycdec uses restricted unpicklers with whitelist-based class loading to mitigate arbitrary code execution risks, but **no pickle safeguard is perfect**. Only process files from sources you trust.
Set `RPYCDEC_NO_WARNING=1` to suppress the CLI security warning.
See also: [Python pickle security warning](https://docs.python.org/3/library/pickle.html#module-pickle)
## Troubleshooting
- **Q: Pickle error `ModuleNotFoundError: No module named '...'`**
A: This means our fake `renpy`/`store` packages don't cover the class your file needs. Please [open an issue](https://github.com/cnfatal/rpycdec/issues) with the Ren'Py version and the file that failed.
## Contributing
Contributions are welcome! Please [open an issue](https://github.com/cnfatal/rpycdec/issues) before submitting major changes so we can discuss the approach.
## Community & Support
- [GitHub Issues](https://github.com/cnfatal/rpycdec/issues) — Bug reports and feature requests
- [Telegram Group](https://t.me/rpycdec) — Community discussion and support
## Alternative
- [unrpyc](https://github.com/CensoredUsername/unrpyc) - The well-established and widely-used Ren'Py script decompiler

View File

@@ -0,0 +1,55 @@
[build-system]
requires = ["hatchling", "hatch-vcs"]
build-backend = "hatchling.build"
[project]
name = "rpycdec"
authors = [{ name = "cnfatal", email = "cnfatal@gmail.com" }]
description = "A tool to decrypt .rpyc files"
readme = "README.md"
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3",
"Topic :: Utilities",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
]
dynamic = ["version"]
dependencies = []
[project.optional-dependencies]
release = ["twine"]
sign = ["ecdsa"]
dev = ["ruff>=0.16.4,<0.17"]
[project.urls]
"Homepage" = "https://github.com/cnfatal/rpycdec"
"Bug Tracker" = "https://github.com/cnfatal/rpycdec/issues"
[project.scripts]
rpycdec = "rpycdec:main"
[tool.hatch.build.targets.sdist]
exclude = ["/.github", "/docs"]
[tool.hatch.build.targets.wheel]
packages = ["src/rpycdec", "src/renpy", "src/store"]
[tool.hatch.version]
source = "vcs"
[tool.ruff]
target-version = "py310"
line-length = 88
extend-exclude = ["src/renpy", "src/store"]
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I", "UP035"]
[tool.ruff.lint.per-file-ignores]
"src/rpycdec/__init__.py" = ["F401"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "auto"

View File

@@ -0,0 +1 @@
# Ren'Py AST and support modules for rpycdec

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
class PyExpr(str):
expr: str
filename: str
linenumber: int
py: int
hashcode: str
column: int
def __new__(
cls,
expr: str,
filename: str = '',
linenumber: int = 0,
py: int = 3,
hashcode: str = '',
column: int = 0,
):
self = super().__new__(cls, expr)
self.expr = expr
self.filename = filename
self.linenumber = linenumber
self.py = py
self.hashcode = hashcode
self.column = column
return self
def get_code(self, **kwargs) -> str:
return self.expr
def __str__(self):
return self.expr

View File

@@ -0,0 +1,319 @@
from . import util, ast, astsupport
from .object import Object
class Context(object):
pass
class ATLTransformBase(Object):
pass
class RawStatement(object):
pass
class Statement(Object):
pass
class RawBlock(RawStatement):
statements = None
"""
https://www.renpy.org/doc/html/transforms.html#block-statement
atl_block_stmt ::= "block" ":"
atl_block
"""
def get_code(self, **kwargs) -> str:
if (
not self.animation
and len(self.statements) == 1
and isinstance(self.statements[0], RawBlock)
):
return util.get_code(self.statements[0], **kwargs)
rv = []
if self.animation:
"""
https://www.renpy.org/doc/html/transforms.html#animation-statement
atl_animation ::= "animation"
image eileen vhappy moving:
animation
"eileen vhappy"
xalign 0.0
linear 5.0 xalign 1.0
repeat
"""
rv.append("animation")
statements = self.statements
if statements:
for statement in statements:
if statement is None:
rv.append("pass")
else:
rv.append(util.get_code(statement, **kwargs))
else:
rv.append("pass")
return "\n".join(rv)
class Block(Statement):
pass
class RawMultipurpose(RawStatement):
"""
https://www.renpy.org/doc/html/transforms.html#interpolation-statement
atl_interp ::= ((warper simple_expression) | ("warp" simple_expression simple_expression)) (atl_interp_target+ | (":"
atl_interp_target+ ))
atl_interp_target ::= (atl_property+ ("knot" simple_expression)* )
| atl_transform_expression
| "clockwise"
| "counterclockwise"
| ("circles" simple_expression)
"""
def get_code(self, **kwargs) -> str:
start = ""
warp_fun, duration, wrap = self.warp_function, self.duration, self.warper
if warp_fun and wrap is None:
# warp {warp_function:expr} {duration:expr}
start = f"warp {util.get_code(warp_fun, **kwargs)} {util.get_code(duration, **kwargs)}"
elif not warp_fun and wrap:
# {wrap} {duration:expr}
start = f"{wrap} {util.get_code(duration, **kwargs)}"
elif not warp_fun and not wrap and duration == "0":
pass
rv = []
revolution = self.revolution
if revolution:
rv.append(revolution)
circles = self.circles
if circles and circles != "0":
# circles {expression}
rv.append(f"circles {util.get_code(circles, **kwargs)}")
for prop, expr in self.properties:
rv.append(f"{prop} {util.get_code(expr, **kwargs)}")
for prop, exprs in self.splines:
last, exprs = exprs[-1], exprs[:-1]
knots = " ".join(f"knot {util.get_code(expr, **kwargs)}" for expr in exprs)
rv.append(f"{prop} {util.get_code(last, **kwargs)} {knots}")
for expr, with_expr in self.expressions:
if with_expr is None:
rv.append(str(expr))
continue
if isinstance(with_expr, ast.PyExpr):
valstr = f"with {with_expr}"
elif isinstance(with_expr, astsupport.PyExpr):
valstr = f"with {with_expr}"
else:
valstr = str(with_expr)
rv.append(f"{expr} {valstr}")
if not start:
return " ".join(rv)
if not rv:
# if no properties, return just the warp
return start
if len(rv) < 3:
# if only few properties, return them in one line
return start + " " + " ".join(rv)
return start + ":\n" + util.indent(" ".join(rv))
class RawContainsExpr(RawStatement):
"""
https://www.renpy.org/doc/html/transforms.html#inline-contains-statement
atl_contains ::= "contains" simple_expression
"""
def get_code(self, **kwargs) -> str:
"""
Returns the code for a contains expression.
"""
expression = util.attr(self, "expression")
if expression:
return f"contains {util.get_code(expression, **kwargs)}"
return "contains"
# This allows us to have multiple ATL transforms as children.
class RawChild(RawStatement):
"""
https://www.renpy.org/doc/html/transforms.html#contains-block-statement
atl_counts ::= "contains" ":"
atl_block
"""
def get_code(self, **kwargs) -> str:
children = util.attr(self, "children")
if not children:
return "contains:\n pass"
rv = ["contains:"]
rv.append(util.indent(util.get_code(children, **kwargs)))
return "\n".join(rv)
# This changes the child of this statement, optionally with a transition.
class Child(Statement):
pass
# This causes interpolation to occur.
class Interpolation(Statement):
pass
class RawRepeat(RawStatement):
"""
https://www.renpy.org/doc/html/transforms.html#repeat-statement
atl_repeat ::= "repeat" (simple_expression)?
"""
def get_code(self, **kwargs) -> str:
if self.repeats:
return f"repeat {util.get_code(self.repeats, **kwargs)}"
return "repeat"
class Repeat(Statement):
pass
class RawParallel(RawStatement):
"""
https://www.renpy.org/doc/html/transforms.html#parallel-statement
atl_parallel ::= ("parallel" ":"
atl_block)+
"""
def get_code(self, **kwargs) -> str:
rv = ["parallel:"]
blocks = util.attr(self, "blocks")
if not blocks:
rv.append(util.indent("pass"))
else:
rv.append(util.indent(util.get_code(blocks, **kwargs)))
return "\n".join(rv)
class Parallel(Statement):
pass
class RawChoice(RawStatement):
"""
https://www.renpy.org/doc/html/transforms.html#choice-statement
atl_choice ::= ("choice" (simple_expression)? ":"
atl_block)+
"""
def get_code(self, **kwargs) -> str:
rv = []
for text, stmt in self.choices:
rv.append(f"choice {text}:")
rv.append(util.indent(util.get_code(stmt, **kwargs)))
return "\n".join(rv)
class Choice(Statement):
pass
class RawTime(RawStatement):
"""
https://www.renpy.org/doc/html/atl.html#time-statement
atl_time ::= "time" (simple_expression)?
"""
def get_code(self, **kwargs) -> str:
time = util.attr(self, "time")
if time:
return f"time {util.get_code(time, **kwargs)}"
return "time"
class Time(Statement):
pass
class RawOn(RawStatement):
"""
https://www.renpy.org/doc/html/transforms.html#on-statement
atl_on ::= "on" name [ "," name ] * ":"
atl_block
on show:
alpha 0.0
linear .5 alpha 1.0
on hide:
linear .5 alpha 0.0
"""
def get_code(self, **kwargs) -> str:
rv = []
for text, stmt in self.handlers.items():
rv.append(f"on {text}:")
rv.append(util.indent(util.get_code(stmt, **kwargs)))
return "\n".join(rv)
class On(Statement):
pass
class RawEvent(RawStatement):
"""
https://www.renpy.org/doc/html/transforms.html#event-statement
atl_event ::= "event" name
"""
def get_code(self, **kwargs) -> str:
name = util.attr(self, "name")
if name:
return f"event {name}"
return "event"
class Event(Statement):
pass
class RawFunction(RawStatement):
"""
https://www.renpy.org/doc/html/transforms.html#function-statement
atl_function ::= "function" simple_expression
"""
def get_code(self, **kwargs) -> str:
if self.expr:
return "function " + str(self.expr)
else:
return "function"
class Function(Statement):
pass

View File

@@ -0,0 +1,15 @@
from typing import Optional
# Do we use old text substitutions?
old_substitutions = True
# An alternate path to use when uneliding. (Mostly used by the launcher to enable
# the style inspector.)
alternate_unelide_path = None
# Various directories.
gamedir = ""
basedir = ""
renpy_base = ""
commondir = None # type: Optional[str]
logdir = None # type: Optional[str] # Where log and error files go.

View File

@@ -0,0 +1,71 @@
from . import layout, core
from ..text import text
from .. import object as renpyobject, python
class Keymap(layout.Null):
pass
class RollForward(layout.Null):
pass
class PauseBehavior(layout.Null):
pass
class DismissBehavior(layout.Null):
pass
class SoundStopBehavior(layout.Null):
pass
class SayBehavior(layout.Null):
pass
class Button(layout.Window):
pass
class ImageButton(Button):
pass
class HoveredProxy(object):
pass
class Input(text.Text): # @UndefinedVariable
pass
class Adjustment(renpyobject.Object):
pass
class Bar(core.Displayable):
pass
class Conditional(layout.Container):
pass
class TimerState(python.RevertableObject):
pass
class Timer(layout.Null):
pass
class MouseArea(core.Displayable):
pass
class OnEvent(core.Displayable):
pass

View File

@@ -0,0 +1,33 @@
from .. import object as renpyobject
class IgnoreEvent(Exception):
pass
class EndInteraction(Exception):
pass
class absolute(float):
pass
class DisplayableArguments(renpyobject.Object):
pass
class Displayable(renpyobject.Object):
pass
class SceneLists(renpyobject.Object):
pass
class MouseMove(object):
pass
class Interface(object):
pass

View File

@@ -0,0 +1,10 @@
from . import core, layout
from .. import python
class Drag(core.Displayable, python.RevertableObject):
pass
class DragGroup(layout.MultiBox):
pass

View File

@@ -0,0 +1,84 @@
from ..display import core, layout
from .. import object as renpyobject
class Null(core.Displayable):
pass
class Container(core.Displayable):
pass
class Position(layout.Container):
pass
class Grid(Container):
pass
class IgnoreLayers(Exception):
pass
class MultiBox(layout.Container):
pass
class SizeGroup(renpyobject.Object):
pass
class Window(Container):
pass
class DynamicDisplayable(core.Displayable):
pass
def ConditionSwitch(*args, **kwargs):
pass
class IgnoresEvents(Container):
pass
def Crop(rect, child, **properties):
pass
LiveCrop = Crop
class Side(Container):
pass
class Alpha(core.Displayable):
pass
class AdjustTimes(Container):
pass
class Tile(Container):
pass
LiveTile = Tile
class Flatten(Container):
pass
class AlphaMask(Container):
pass
class NearRect(Container):
pass

View File

@@ -0,0 +1,2 @@
class Transform:
pass

View File

@@ -0,0 +1,50 @@
import os
from typing import NamedTuple
import renpy
class SubParse(object):
"""
This represents the information about a subparse that can be provided to
a creator-defined statement.
"""
def __init__(self, block):
self.block = block
def __repr__(self):
if not self.block:
return "<SubParse empty>"
else:
return "<SubParse {}:{}>".format(
self.block[0].filename, self.block[0].linenumber
)
def unelide_filename(fn: str) -> str:
fn = os.path.normpath(fn)
if renpy.config.alternate_unelide_path is not None:
fn0 = os.path.join(renpy.config.alternate_unelide_path, fn)
if os.path.exists(fn0):
return fn0
fn1 = os.path.join(renpy.config.basedir, fn)
if os.path.exists(fn1):
return fn1
fn2 = os.path.join(renpy.config.renpy_base, fn)
if os.path.exists(fn2):
return fn2
return fn
class GroupedLine(NamedTuple):
# The filename the line is from.
filename: str
number: int
indent: int
text: str
block: list["GroupedLine"]

View File

@@ -0,0 +1,33 @@
class Object(object):
__version__ = 0
nosave = []
def __getstate__(self):
rv = vars(self).copy()
for f in self.nosave:
if f in rv:
del rv[f]
rv["__version__"] = self.__version__
return rv
# None, to prevent this from being called when unnecessary.
after_setstate = None
def __setstate__(self, new_dict):
version = new_dict.pop("__version__", 0)
self.__dict__.update(new_dict)
if version != self.__version__:
self.after_upgrade(version) # E1101
if self.after_setstate:
self.after_setstate() # E1102
class Sentinel(object):
pass

View File

@@ -0,0 +1,63 @@
from .. import util
class Signature:
def get_code(self, **kwargs) -> str:
rv = []
parameters = util.attr(self, "parameters")
if isinstance(parameters, dict):
for name, value in parameters.items():
rv.append(util.get_code(value, **kwargs))
else:
for value in parameters:
rv.append(str(value))
if not rv:
return ""
return f"({', '.join(rv)})"
class Parameter:
(
POSITIONAL_ONLY,
POSITIONAL_OR_KEYWORD,
VAR_POSITIONAL,
KEYWORD_ONLY,
VAR_KEYWORD,
) = range(5)
def _variable_or_default(self, name, default):
return f"{name}={default}" if default is not None else f"{name}"
def get_code(self, **kwargs) -> str:
name = util.attr(self, "name")
kind = util.attr(self, "kind")
default = util.attr(self, "default")
if kind == self.POSITIONAL_ONLY:
return self._variable_or_default(name, default)
elif kind == self.POSITIONAL_OR_KEYWORD:
return self._variable_or_default(name, default)
elif kind == self.VAR_POSITIONAL:
return f"*{name}"
elif kind == self.KEYWORD_ONLY:
return self._variable_or_default(name, default)
elif kind == self.VAR_KEYWORD:
return f"**{name}"
else:
raise ValueError(f"Unknown parameter kind: {kind}")
class ArgumentInfo:
def get_code(self, **kwargs) -> str:
rv = []
for i, (keyword, expression) in enumerate(self.arguments):
if i in self.starred_indexes:
rv.append("*" + expression)
elif i in self.doublestarred_indexes:
rv.append("**" + expression)
elif keyword is not None:
rv.append("{}={}".format(keyword, expression))
else:
rv.append(str(expression))
return "(" + ", ".join(rv) + ")"

View File

@@ -0,0 +1,25 @@
import ast
# Three levels of constness.
GLOBAL_CONST = 2 # Expressions that are const everywhere.
LOCAL_CONST = 1 # Expressions that are const with regard to a screen + parameters.
NOT_CONST = 0 # Expressions that are not const.
class Control(object):
pass
class Analysis(object):
pass
class PyAnalysis(ast.NodeVisitor):
pass
class CompilerCache(object):
pass
ccache = CompilerCache()

View File

@@ -0,0 +1,82 @@
import ast
import random
from .object import Object
class StoreDeleted(object):
pass
class StoreModule(object):
pass
class StoreDict(dict):
pass
class StoreBackup:
pass
class NoRollback(object):
pass
class WrapNode(ast.NodeTransformer):
pass
class CompressedList(object):
pass
class RevertableList(list):
pass
class RevertableDict(dict):
pass
class RevertableSet(set):
def __setstate__(self, state):
if isinstance(state, tuple):
self.update(state[0].keys())
else:
self.update(state)
__reduce__ = object.__reduce__
__reduce_ex__ = object.__reduce_ex__
class RevertableObject(object):
pass
class RollbackRandom(random.Random):
pass
class DetRandom(random.Random):
pass
class Rollback(Object):
pass
class RollbackLog(Object):
pass
class StoreProxy(object):
pass
def py_compile(source, mode, filename="<none>", lineno=1, ast_node=False, cache=True):
pass
def py_eval_bytecode(bytecode, globals=None, locals=None): # @ReservedAssignment
pass

View File

@@ -0,0 +1,83 @@
import ast
import random
from .object import Object
class StoreDeleted(object):
pass
class StoreModule(object):
pass
class StoreDict(dict):
pass
class StoreBackup:
pass
class NoRollback(object):
pass
class WrapNode(ast.NodeTransformer):
pass
class CompressedList(object):
pass
class RevertableList(list):
pass
class RevertableDict(dict):
pass
class RevertableSet(set):
def __setstate__(self, state):
if isinstance(state, tuple):
self.update(state[0].keys())
else:
self.update(state)
# Required to ensure that getstate and setstate are called.
__reduce__ = object.__reduce__
__reduce_ex__ = object.__reduce_ex__
class RevertableObject(object):
pass
class RollbackRandom(random.Random):
pass
class DetRandom(random.Random):
pass
class Rollback(Object):
pass
class RollbackLog(Object):
pass
class StoreProxy(object):
pass
def py_compile(source, mode, filename="<none>", lineno=1, ast_node=False, cache=True):
pass
def py_eval_bytecode(bytecode, globals=None, locals=None): # @ReservedAssignment
pass

View File

@@ -0,0 +1,12 @@
def collapse_stmts(stmts):
"""
Returns a flat list containing every statement in the tree
stmts.
"""
rv = []
for i in stmts:
i.get_children(rv.append)
return rv

View File

@@ -0,0 +1,323 @@
import logging
from .. import ast, astsupport, ui, util
class SLContext(ui.Addable):
pass
class SLNode(object):
pass
class SLBlock(SLNode):
"""
foo bar
baz 1
expression
"""
def get_code(self, **kwargs) -> str:
rv = []
if self.keyword:
rv.append(util.get_code_properties(self.keyword, newline=True))
if self.children:
rv.append(util.get_code(self.children, **kwargs))
return "\n".join(rv)
class SLCache(object):
pass
class SLDisplayable(SLBlock):
"""
`displayable`
A function that, when called with the positional and keyword
arguments, causes the displayable to be displayed.
`scope`
If true, the scope is supplied as an argument to the displayable.
`child_or_fixed`
If true and the number of children of this displayable is not one,
the children are added to a Fixed, and the Fixed is added to the
displayable.
`style`
The base name of the main style.
`pass_context`
If given, the context is passed in as the first positonal argument
of the displayable.
`imagemap`
True if this is an imagemap, and should be handled as one.
`hotspot`
True if this is a hotspot that depends on the imagemap it was
first displayed with.
`replaces`
True if the object this displayable replaces should be
passed to it.
`default_keywords`
The default keyword arguments to supply to the displayable.
`variable`
A variable that the main displayable is assigned to.
https://www.renpy.org/doc/html/screens.html#add-statement
https://www.renpy.org/doc/html/screens.html#use
https://www.renpy.org/doc/html/screens.html#has
https://www.renpy.org/doc/html/screens.html#imagebutton
"""
def get_name(self) -> str:
# higher version use style instead of name
name = getattr(self, "name", None)
displayable = getattr(self, "displayable", None)
style = getattr(self, "style", None)
start = ""
if name:
start = name
elif displayable:
# displayable function named sl2xxx like sl2vbar , sl2viewport
displable_name = displayable.__name__.lower()
if displable_name.startswith("sl2"):
start = displable_name.replace("sl2", "")
elif displable_name == "onevent":
start = "on"
else:
start = displable_name.replace("_", "")
if style:
match start:
case "multibox":
start = style
case "window":
start = style
if not start:
logging.warning(
"warning: displayable not found, use textbutton instead, you may need fix it manually"
)
start = "textbutton"
return start
def get_code(self, **kwargs) -> str:
# higher version use style instead of name
start = self.get_name()
if not start:
raise Exception("displayable not found")
variable = util.attr(self, "variable")
if variable:
start += f" as {variable}"
tag = util.attr(self, "tag")
if tag:
start += f" tag {tag}"
positional = util.attr(self, "positional")
if positional:
if isinstance(positional, list):
start += " " + " ".join(util.get_code(p, **kwargs) for p in positional)
else:
start += " " + util.get_code(positional, **kwargs)
# keywords
keyword = util.attr(self, "keyword")
children = util.attr(self, "children")
if not children:
if keyword:
start += f" {util.get_code_properties(keyword)}"
return start
# children
rv = [start + ":"]
if keyword:
rv.append(util.indent(util.get_code_properties(keyword, newline=True)))
rv.append(util.indent(util.get_code(children, **kwargs)))
return "\n".join(rv)
class SLIf(SLNode):
def get_code(self, **kwargs) -> str:
rv = []
for index, (cond, body) in enumerate(self.entries):
body_code = util.get_code(body, **kwargs)
if not body_code:
body_code = "pass"
body_code = util.indent(body_code)
if index == 0:
rv.append(f"if {cond}:\n{body_code}")
continue
if cond and cond != "True":
rv.append(f"elif {cond}:\n{body_code}")
else:
rv.append(f"else:\n{body_code}")
break
return "\n".join(rv)
class SLShowIf(SLNode):
"""
https://www.renpy.org/doc/html/screens.html#showif-statement
showif n == 3:
text "Three" size 100 at cd_transform
elif n == 2:
text "Two" size 100 at cd_transform
elif n == 1:
text "One" size 100 at cd_transform
else:
text "Liftoff!" size 100 at cd_transform
"""
def get_code(self, **kwargs) -> str:
rv = []
for index, (cond, body) in enumerate(self.entries):
body_code = util.get_code(body, **kwargs)
if not body_code:
body_code = "pass"
body_code = util.indent(body_code)
if index == 0:
rv.append(f"showif {cond}:\n{body_code}")
continue
if cond and cond != "True":
rv.append(f"elif {cond}:\n{body_code}")
else:
rv.append(f"else:\n{body_code}")
break
return "\n".join(rv)
class SLFor(SLBlock):
pass
"""
screen five_buttons():
vbox:
for i, numeral index numeral in enumerate(numerals):
textbutton numeral action Return(i + 1)
"""
def get_code(self, **kwargs) -> str:
rv = [f"for {self.variable} in {self.expression}:"]
children = util.attr(self, "children")
if not children:
rv.append(util.indent("pass"))
else:
rv.append(util.indent(util.get_code(children, **kwargs)))
return "\n".join(rv)
class SLPython(SLNode):
def get_code(self, **kwargs) -> str:
inner_code = util.get_code(self.code, **kwargs)
if not inner_code:
return ""
if len(inner_code.splitlines()) == 1:
return f"$ {inner_code}"
return f"python:\n{util.indent(inner_code)}"
class SLPass(SLNode):
pass
class SLDefault(SLNode):
"""
https://www.renpy.org/doc/html/python.html#default-statement
"""
def get_code(self, **kwargs) -> str:
return f"default {self.variable} = {self.expression}"
class SLUse(SLNode):
"""
https://www.renpy.org/doc/html/screens.html#use
use [expression EXPR pass|WORD] ARGUMENTS [id EXPR][:]
BLOCK
"""
def get_code(self, **kwargs) -> str:
start = "use"
target = util.attr(self, "target")
if target:
if isinstance(target, ast.PyExpr):
start += f" expression {util.get_code(target, **kwargs)} pass "
elif isinstance(target, astsupport.PyExpr):
start += f" expression {util.get_code(target, **kwargs)} pass "
else:
start += f" {target}"
if self.args:
start += f"{util.get_code(self.args, **kwargs)}"
if self.block:
return util.label_code(start, self.block, **kwargs)
if self.ast:
return util.label_code(start, self.ast, **kwargs)
return start
# https://www.renpy.org/doc/html/screens.html#use-and-transclude
"""
screen movable_frame(pos):
drag:
pos pos
frame:
background Frame("movable_frame.png", 10, 10)
top_padding 20
transclude
"""
class SLTransclude(SLNode):
def get_code(self, **kwargs) -> str:
return "transclude"
class SLScreen(SLBlock):
"""
https://www.renpy.org/doc/html/screens.html#screen-statement
"""
parameters: ast.ParameterInfo
keyword: list
def get_code(self, **kwargs) -> str:
start = "screen"
if self.name:
start += f" {self.name}"
if self.parameters:
start += f"{util.get_code(self.parameters, **kwargs)}"
properties = self.keyword.copy()
tag = util.attr(self, "tag")
if tag:
properties.append(("tag", tag))
rv = [start + ":"]
# keyword
if properties:
rv.append(util.indent(util.get_code_properties(properties, newline=True)))
rv.append("") # add a newline after keyword
# children
children = util.attr(self, "children")
if children:
rv.append(util.indent(util.get_code(children, **kwargs)))
if len(rv) == 1:
rv.append("pass")
return "\n".join(rv)
class ScreenCache(object):
pass
class SLContinue(SLNode):
def get_code(self, **kwargs) -> str:
return "continue"

View File

@@ -0,0 +1,25 @@
from ..display import layout
class ShowIf(layout.Container):
pass
def sl2vbar(context=None, **properties):
pass
def sl2viewport(context=None, **kwargs):
pass
def sl2vpgrid(context=None, **kwargs):
pass
def sl2add(d, replaces=None, scope=None, **kwargs):
pass
def sl2bar(context=None, **properties):
pass

View File

View File

@@ -0,0 +1,2 @@
class ParameterizedText(object):
pass

View File

@@ -0,0 +1,26 @@
class ImageFont(object):
pass
class FontGroup(object):
pass
class ScaledImageFont(ImageFont):
pass
class BMFont(ImageFont):
pass
class MudgeFont(ImageFont):
pass
class SFont(ImageFont):
pass
class ImageFont(object):
pass

View File

@@ -0,0 +1,33 @@
from ..display import core
class Blit(object):
pass
class DrawInfo(object):
pass
class TextSegment(object):
pass
class SpaceSegment(object):
pass
class DisplayableSegment(object):
pass
class FlagSegment(object):
pass
class Layout(object):
pass
class Text(core.Displayable):
pass

View File

@@ -0,0 +1,28 @@
import re
def encode_say_string(s):
"""
Encodes a string in the format used by Ren'Py say statements.
"""
s = s.replace("\\", "\\\\")
s = s.replace("\n", "\\n")
s = s.replace('"', '\\"')
s = re.sub(r"(?<= ) ", "\\ ", s)
return '"' + s + '"'
def quote_unicode(s):
s = s.replace("\\", "\\\\")
s = s.replace('"', '\\"')
s = s.replace("\a", "\\a")
s = s.replace("\b", "\\b")
s = s.replace("\f", "\\f")
s = s.replace("\n", "\\n")
s = s.replace("\r", "\\r")
s = s.replace("\t", "\\t")
s = s.replace("\v", "\\v")
return s

View File

@@ -0,0 +1,27 @@
import os
import renpy
def shorten_filename(filename):
"""
Shortens a file name. Returns the shortened filename, and a flag that says
if the filename is in the common directory.
"""
commondir = os.path.normpath(renpy.config.commondir) # type: ignore
gamedir = os.path.normpath(renpy.config.gamedir)
if filename.startswith(commondir):
fn = os.path.relpath(filename, commondir)
common = True
elif filename.startswith(gamedir):
fn = os.path.relpath(filename, gamedir)
common = False
else:
fn = os.path.basename(filename)
common = False
return fn, common

View File

@@ -0,0 +1,9 @@
STRING_RE = r"""(?x)
\b(?:_|__|___|_p)
\s*(\((?:[\s\\\n]*[uU]?(?:
\"\"\"(?:\\.|\\\n|\"{1,2}|[^\\"])*?\"\"\"
|'''(?:\\.|\\\n|\'{1,2}|[^\\'])*?'''
|"(?:\\.|\\\n|[^\\"])*"
|'(?:\\.|\\\n|[^\\'])*'
))+\s*\))
"""

View File

@@ -0,0 +1,96 @@
from .object import Object
class Action(Object):
pass
class BarValue(Object):
pass
class Addable(object):
pass
class Layer(Addable):
pass
class Many(Addable):
pass
class One(Addable):
pass
class Detached(Addable):
pass
class ChildOrFixed(Addable):
pass
def get_code(self, **kwargs) -> str:
pass
def _hotspot(spot, style="hotspot", **properties):
pass
def _imagebutton(
idle_image=None,
hover_image=None,
insensitive_image=None,
activate_image=None,
selected_idle_image=None,
selected_hover_image=None,
selected_insensitive_image=None,
selected_activate_image=None,
idle=None,
hover=None,
insensitive=None,
selected_idle=None,
selected_hover=None,
selected_insensitive=None,
image_style=None,
auto=None,
**properties,
):
pass
def _imagemap(
ground,
selected,
hotspots,
unselected=None,
overlays=False,
style="imagemap",
mouse="imagemap",
with_none=None,
**properties,
):
pass
def _textbutton(
label,
clicked=None,
style=None,
text_style=None,
substitute=True,
scope=None,
**kwargs,
):
pass
def _key(key, action=None, activate_sound=None):
pass
def _label(label, style=None, text_style=None, substitute=True, scope=None, **kwargs):
pass

View File

@@ -0,0 +1,250 @@
from . import ast
from collections import deque
IDENT_CHAR = " "
def indent(code: str, level: int = 1) -> str:
"""
Indent each line of code by a specified level.
Args:
code: The string to indent
level: Number of indentation levels to apply (default: 1)
Returns:
The indented string
"""
if not code:
return ""
indent_str = IDENT_CHAR * level
# add indentation to each non-empty line
# keep empty lines as they are
rv = [indent_str + line if line.strip() else line for line in code.split("\n")]
return "\n".join(rv)
def get_code_properties(props: tuple | dict, newline: bool = False, **kwargs) -> str:
"""
:param keyword: tuple | dict
:param newline: bool
:return: str
>>> get_code_properties((("a", 1), ("b", 2)))
a 1 b 2
>>> get_code_properties((("a", 1), (None, b)), newline=True)
a 1
b
>>> get_code_properties({"a": 1, "b": None})
a 1
b None
"""
list = []
if isinstance(props, dict):
props = props.items()
for prop in props:
if isinstance(prop, tuple) and len(prop) == 2:
key, value = prop[0], prop[1]
valstr = get_code(value, **kwargs)
if not valstr:
list.append(f"{key}")
else:
list.append(f"{key} {valstr}")
continue
else:
prop_str = " ".join([str(x) for x in prop])
if not prop_str:
continue
list.append(prop_str)
return ("\n" if newline else " ").join(list)
def get_code(node, **kwargs) -> str:
"""
Parameters
----------
node : ast.Node
kwargs : dict
indent : int
space indent level
modifier : Callable[[ast.Node], ast.Node]
modify node before get code
Returns
-------
str
generated code
Raises
------
NotImplementedError
if node type is not implemented or some attributes unable to handle.
"""
if isinstance(node, str):
return node
if isinstance(node, list):
rv = []
items = deque(node)
while items:
item = items.popleft()
# [Say|UserStatement?, Menu]
if isinstance(item, ast.Say) or isinstance(item, ast.UserStatement):
next = items[0] if items else None
if isinstance(next, ast.Menu) and attr(next, "statement_start") == item:
menu = items.popleft()
call_kwargs = kwargs.copy()
call_kwargs["menu_say"] = item
rv.append(get_code(menu, **call_kwargs))
continue
# [Label, Say|UserStatement?, Menu]
if isinstance(item, ast.Label):
next = items[0] if items else None
# [Label, Say|UserStatement, Menu]
if isinstance(next, ast.Say) or isinstance(next, ast.UserStatement):
next_next = items[1] if len(items) > 1 else None
if (
isinstance(next_next, ast.Menu)
and attr(next_next, "statement_start") == item
):
say = items.popleft()
menu = items.popleft()
call_kwargs = kwargs.copy()
call_kwargs["menu_label"] = item
call_kwargs["menu_say"] = say
rv.append(get_code(menu, **call_kwargs))
continue
# [Label, Menu]
if isinstance(next, ast.Menu) and attr(next, "statement_start") == item:
menu = items.popleft()
call_kwargs = kwargs.copy()
call_kwargs["menu_label"] = item
rv.append(get_code(menu, **call_kwargs))
continue
if isinstance(item, ast.With):
# some node quoted by two ast.With if has with suffix expr, the sdk snippet:
#
# renpy.parser.parse_with(node):
# if not "with":
# return node
# expr = simple_expression
# return [ast.With(loc, "None", paired=expr), node, ast.With(loc, expr)]
#
# - ast.Scene
# - ast.Show
# - ast.Hide
expr, paired = attr(item, "expr"), attr(item, "paired")
if (not expr or expr == "None") and paired:
next_next = items[1] if len(items) > 1 else None
if (
isinstance(next_next, ast.With)
and attr(next_next, "expr") == paired
):
node = items.popleft()
close_with = items.popleft()
call_kwargs = kwargs.copy()
call_kwargs.update({"with_expr": attr(close_with, "expr")})
rv.append(get_code(node, **call_kwargs))
continue
if isinstance(item, ast.Call):
# call = [Call, Label?, Pass]
from_label = None
# try to pop next Label|Pass
next = items.popleft() if items else None
if isinstance(next, ast.Label):
from_label = next
# pop Pass
items.popleft() if items else None
if from_label is not None:
call_kwargs = kwargs.copy()
call_kwargs["from_label"] = from_label
rv.append(get_code(item, **call_kwargs))
continue
if isinstance(item, ast.Return):
if (
# it cant tell out an anto appended Return or an empty one
# currently we must not ignore
False
and len(node) > 1
and not items
and not attr(item, "expression")
# omit the return after While may cause unexpected behavior
and not any(isinstance(n, ast.While) for n in node)
):
# ignore the return
continue
# if all left items are returns
if all(
isinstance(n, ast.Return) and not attr(n, "expression")
for n in items
):
# ignore all returns
rv.append(get_code(item, **kwargs))
break
# if isinstance(item, ast.Init):
# if item.priority == 500:
# if len(item.block) == 1 and isinstance(item.block[0], ast.Image):
# rv.append(get_code(item.block[0]))
# continue
rv.append(get_code(item, **kwargs))
return "\n".join(rv)
# modify node before get code
modifier = kwargs.get("modifier")
if modifier:
modifier(node, **kwargs)
return node.get_code(**kwargs)
def get_block_code(node, **kwargs) -> str:
"""
https://www.renpy.org/doc/html/layeredimage.html#layeredimage
"""
if isinstance(node, list):
return "\n".join(map(lambda x: get_block_code(x, **kwargs), node))
lines = []
if isinstance(node, tuple):
if len(node) == 5:
_, _, _, code, block = node
elif len(node) == 4:
_, _, code, block = node
else:
raise NotImplementedError
lines.append(code)
lines.append(indent(get_block_code(block, **kwargs)))
else:
raise NotImplementedError
return "\n".join(lines)
def attr(item, key: str) -> str:
if isinstance(item, dict):
return item.get(key, None)
if hasattr(item, key):
return getattr(item, key)
return None
def label_code(label: str, child, **kwargs) -> str:
"""
return a code block with a label
example:
>>> label_getcode("label foo", None)
"label foo"
>>> label_getcode("label foo", Expression("bar"))
"label foo:
bar"
"""
if not child:
return label
return f"{label}:\n{indent(get_code(child, **kwargs))}"

View File

@@ -0,0 +1,5 @@
from .cli import main
from .decompile import decompile, decompile_file
from .rpa import create_rpa, extract_rpa
from .save import dump_save_info, extract_save, generate_new_key, restore_save
from .translate import extract_translations

View File

@@ -0,0 +1,3 @@
from .cli import main
main()

View File

@@ -0,0 +1,193 @@
"""
APK extraction module for Ren'Py games.
This module provides functionality to extract Ren'Py game resources from Android APK files.
The extraction is done in a streaming fashion to minimize disk I/O and temporary file usage.
"""
import io
import logging
import tarfile
import zipfile
from pathlib import Path
from typing import Optional
from rpycdec.utils import safe_path
logger = logging.getLogger(__name__)
class PrependedStream(io.RawIOBase):
"""Wrapper to prepend already-read bytes back to stream."""
def __init__(self, prepend_data, stream):
self.prepend_data = prepend_data
self.stream = stream
self.prepend_pos = 0
def readable(self):
return True
def read(self, size=-1):
if size == -1:
result = self.prepend_data[self.prepend_pos :] + self.stream.read()
self.prepend_pos = len(self.prepend_data)
return result
result = b""
# First read from prepended data
if self.prepend_pos < len(self.prepend_data):
take = min(size, len(self.prepend_data) - self.prepend_pos)
result = self.prepend_data[self.prepend_pos : self.prepend_pos + take]
self.prepend_pos += take
size -= take
# Then read from underlying stream
if size > 0:
result += self.stream.read(size)
return result
def readinto(self, b):
data = self.read(len(b))
b[: len(data)] = data
return len(data)
def extract_apk(
apk_path: str,
output_dir: Optional[str] = None,
) -> bool:
"""
Extract Ren'Py game resources from an Android APK file.
This function directly extracts files from the APK (ZIP) archive,
renaming them on-the-fly to remove 'x-' prefixes and organizing
them into a standard Ren'Py structure. The private.mp3 archive
is extracted in-memory without writing to disk first.
Args:
apk_path: Path to the APK file
output_dir: Output directory (default: APK filename without extension)
Returns:
True if extraction was successful, False otherwise
"""
apk_file = Path(apk_path)
if not apk_file.exists():
logger.error(f"APK file not found: {apk_path}")
return False
if not zipfile.is_zipfile(apk_path):
logger.error(f"File is not a valid APK/ZIP file: {apk_path}")
return False
# Determine output directory
if output_dir is None:
output_path = apk_file.parent / apk_file.stem
else:
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
logger.info(f"Extracting APK: {apk_path}")
with zipfile.ZipFile(apk_path, "r") as apk:
# Get all relevant files (assets and lib)
target_files = [f for f in apk.namelist() if f.startswith(("assets/", "lib/"))]
if not target_files:
logger.warning("No assets or lib directory found in APK")
return False
private_mp3_path = None
extracted_count = 0
for file_path in target_files:
# Skip directories
if file_path.endswith("/"):
continue
if file_path.startswith("assets/"):
# Remove 'assets/' prefix
relative_path = file_path[7:] # len('assets/') = 7
# Handle private.mp3 specially - remember path for later streaming extraction
if relative_path == "private.mp3":
logger.info("Found private.mp3, will extract via streaming...")
private_mp3_path = file_path
continue
# Process path: remove x- prefix and apply mappings
path_parts = relative_path.split("/")
# Process all path parts to remove x- prefix
for i in range(len(path_parts)):
# Remove x- prefix from all parts
if path_parts[i].startswith("x-"):
path_parts[i] = path_parts[i][2:] # Remove 'x-' prefix
# Reconstruct path
dest_path = output_path / "/".join(path_parts)
elif file_path.startswith("lib/"):
# User request: extract lib to output/lib/android/
rel_lib_path = file_path[4:] # len('lib/') = 4
dest_path = output_path / "lib" / "android" / rel_lib_path
else:
continue
# Create parent directories
dest_path.parent.mkdir(parents=True, exist_ok=True)
# Extract file directly
with apk.open(file_path) as src, open(dest_path, "wb") as dst:
dst.write(src.read())
extracted_count += 1
if extracted_count % 100 == 0:
logger.info(f"Extracted {extracted_count} files...")
logger.info(f"Extracted {extracted_count} files")
# Extract private.mp3 via streaming if found
if private_mp3_path:
logger.info("Extracting files from private.mp3 archive...")
# Extract directly to output root (same level as game/)
private_output = output_path
private_output.mkdir(parents=True, exist_ok=True)
with apk.open(private_mp3_path) as private_stream:
# Read magic bytes to detect gzip format
magic = private_stream.read(2)
if not magic.startswith(b"\x1f\x8b"):
logger.warning(
"private.mp3 is not gzip format, skipping extraction"
)
else:
# Create buffered stream with magic bytes prepended
wrapped_stream = io.BufferedReader(
PrependedStream(magic, private_stream)
)
with tarfile.open(fileobj=wrapped_stream, mode="r:gz") as tar:
# Path traversal protection
for member in tar.getmembers():
try:
safe_path(str(private_output), member.name)
except ValueError:
logger.warning(
"Skipping path traversal attempt in tar: %s",
member.name,
)
continue
tar.extract(member, path=private_output)
logger.info(
f"Extracted {len(tar.getmembers())} files from private.mp3"
)
logger.info(f"Successfully extracted game to: {output_path}")
return True

View File

@@ -0,0 +1,317 @@
import argparse
import logging
import os
import re
import sys
from rpycdec.apk import extract_apk
from rpycdec.decompile import decompile
from rpycdec.rpa import create_rpa, extract_rpa
from rpycdec.save import dump_save_info, extract_save, generate_new_key, restore_save
from rpycdec.translate import extract_translations
logger = logging.getLogger(__name__)
def regular_expression(value: str) -> str:
"""Validate a command-line regular expression."""
try:
re.compile(value)
except re.error as exc:
raise argparse.ArgumentTypeError(f"invalid regular expression: {exc}") from exc
return value
def non_empty_suffix(value: str) -> str:
"""Validate a command-line file suffix."""
if not value:
raise argparse.ArgumentTypeError("suffix must not be empty")
return value
def decompile_files(srcs: list[str], **kwargs):
"""
decompile rpyc file or directory.
"""
for src in srcs:
decompile(
src,
output_path=kwargs.get("output"),
dis=kwargs.get("disassemble", False),
)
def extract_rpa_files(srcs: list[str], **kwargs):
"""
extract rpa archive.
"""
for src in srcs:
with open(src, "rb") as f:
output_path = kwargs.get("output") or os.path.dirname(src)
extract_rpa(
f,
output_dir=output_path,
expressions=kwargs.get("expressions"),
suffixes=kwargs.get("suffixes"),
)
def create_rpa_archive(srcs: list[str], output: str, **kwargs):
"""
create rpa archive.
"""
create_rpa(
output,
srcs,
base_dir=kwargs.get("base"),
)
def run_extract_translations(
srcs: list[str], language: str, output: str | None = None, **kwargs
):
"""
extract translations from rpy files.
"""
for src in srcs:
# Default output to current directory, extract_translations will create tl/{language}/
dest = output or "."
extract_translations(
game_dir=src,
output_dir=dest,
language=language,
include_dialogues=kwargs.get("include_dialogues", True),
include_strings=kwargs.get("include_strings", True),
empty_translation=kwargs.get("empty_translation", False),
)
def extract_game_from_apk(apk_path: str, **kwargs):
"""
Extract Ren'Py game from Android APK file.
"""
extract_apk(
apk_path=apk_path,
output_dir=kwargs.get("output"),
)
SECURITY_WARNING = (
"\033[33mSecurity Warning: This tool uses a restricted pickle unpickler with "
"whitelist-based class loading to mitigate arbitrary code execution risks. "
"However, no safeguard is perfect. Only process files from trusted sources.\033[0m"
)
def main():
"""
command line tool entry.
"""
logging.basicConfig(level=logging.INFO)
argparser = argparse.ArgumentParser()
argparser.add_argument(
"--verbose", "-v", action="store_true", help="verbose output"
)
subparsers = argparser.add_subparsers(
title="subcommands", dest="command", help="subcommand help"
)
decompile_parser = subparsers.add_parser("decompile", help="decompile rpyc file")
decompile_parser.add_argument("src", nargs="+", help="rpyc file or directory")
decompile_parser.add_argument(
"--output",
"-o",
help="output path",
)
decompile_parser.add_argument(
"--disassemble", "-d", action="store_true", help="disassemble rpyc file"
)
decompile_parser.set_defaults(
func=lambda args: decompile_files(args.src, **vars(args))
)
unrpa_parser = subparsers.add_parser("unrpa", help="extract rpa archive")
unrpa_parser.add_argument("file", nargs=1, help="rpa archive")
unrpa_parser.add_argument(
"--output",
"-o",
help="output path",
)
unrpa_parser.add_argument(
"--expression",
"-e",
dest="expressions",
action="append",
type=regular_expression,
metavar="REGEX",
help=(
"extract archive paths matching this regular expression; may be repeated"
),
)
unrpa_parser.add_argument(
"--suffix",
"-s",
dest="suffixes",
action="extend",
nargs="+",
type=non_empty_suffix,
metavar="SUFFIX",
help=(
"extract archive paths ending with any of these suffixes "
"(case-insensitive); may be repeated"
),
)
unrpa_parser.set_defaults(
func=lambda args: extract_rpa_files(args.file, **vars(args))
)
rpa_parser = subparsers.add_parser("rpa", help="create rpa archive")
rpa_parser.add_argument("src", nargs="+", help="file or directory to add")
rpa_parser.add_argument(
"--output",
"-o",
required=True,
help="output .rpa archive path",
)
rpa_parser.add_argument(
"--base",
help=(
"base directory for archive names "
"(default: source directory for a single directory, otherwise common parent)"
),
)
rpa_parser.set_defaults(
func=lambda args: create_rpa_archive(
args.src,
args.output,
base=args.base,
)
)
extract_game_parser = subparsers.add_parser(
"extract-game", help="extract Ren'Py game from Android APK file"
)
extract_game_parser.add_argument("apk", help="Android APK file path")
extract_game_parser.add_argument(
"--output",
"-o",
help="output directory (default: APK filename without extension)",
)
extract_game_parser.set_defaults(
func=lambda args: extract_game_from_apk(args.apk, **vars(args))
)
extract_translate_parser = subparsers.add_parser(
"extract-translate",
help="""extract translations from rpy files and save to tl/<language> directory.""",
)
extract_translate_parser.add_argument(
"src", nargs="+", help="source game directory"
)
extract_translate_parser.add_argument(
"--output",
"-o",
help="output directory (default: current directory, creates tl/<language>/)",
)
extract_translate_parser.add_argument(
"--language",
"-l",
required=True,
help="target language code (e.g. chinese, japanese)",
)
extract_translate_parser.add_argument(
"--no-dialogues",
action="store_true",
help="skip dialogue translations",
)
extract_translate_parser.add_argument(
"--no-strings",
action="store_true",
help="skip string translations",
)
extract_translate_parser.add_argument(
"--empty",
action="store_true",
help="generate empty translations (default: copy original)",
)
extract_translate_parser.set_defaults(
func=lambda args: run_extract_translations(
args.src,
args.language,
output=args.output,
include_dialogues=not args.no_dialogues,
include_strings=not args.no_strings,
empty_translation=args.empty,
)
)
save_parser = subparsers.add_parser("save", help="save operations")
save_subparsers = save_parser.add_subparsers(
title="save subcommands", dest="save_command", help="save subcommand help"
)
save_extract_parser = save_subparsers.add_parser(
"extract", help="extract save data to JSON files for editing"
)
save_extract_parser.add_argument("path", nargs=1, help="save-file path")
save_extract_parser.add_argument("dest", nargs="?", help="extract to directory")
save_extract_parser.add_argument(
"--disassemble", "-d", action="store_true", help="print pickle disassembly"
)
save_extract_parser.add_argument(
"--verbose", "-V", action="store_true", help="verbose class loading output"
)
save_extract_parser.set_defaults(
func=lambda args: extract_save(
args.path[0],
args.dest or "",
dissemble=args.disassemble,
verbose=getattr(args, "verbose", False),
)
)
save_restore_parser = save_subparsers.add_parser(
"restore", help="restore save data from extracted JSON directory"
)
save_restore_parser.add_argument("path", nargs=1, help="extracted directory")
save_restore_parser.add_argument("dest", nargs="?", help="restore to save-file")
save_restore_parser.add_argument(
"--key", "-k", help="path to security_keys.txt for re-signing"
)
save_restore_parser.set_defaults(
func=lambda args: restore_save(
args.path[0], args.dest or "", key_file=args.key or ""
)
)
save_info_parser = save_subparsers.add_parser(
"info", help="show save file information"
)
save_info_parser.add_argument("path", nargs=1, help="save-file path")
save_info_parser.set_defaults(func=lambda args: dump_save_info(args.path[0]))
save_genkey_parser = save_subparsers.add_parser(
"genkey", help="generate a new signing key (requires ecdsa library)"
)
save_genkey_parser.add_argument(
"output",
nargs="?",
default="security_keys.txt",
help="output key file path (default: security_keys.txt)",
)
save_genkey_parser.set_defaults(func=lambda args: generate_new_key(args.output))
args = argparser.parse_args()
if args.verbose:
logging.getLogger().setLevel(logging.DEBUG)
if not args.command:
argparser.print_help()
return
# Show security warning for commands that process pickle data
if args.command in ("decompile", "save", "unrpa") and not os.environ.get(
"RPYCDEC_NO_WARNING"
):
print(SECURITY_WARNING, file=sys.stderr)
args.func(args)

View File

@@ -0,0 +1,58 @@
"""Module for decompiling Ren'Py compiled files (.rpyc) back to source code (.rpy)."""
import logging
import os
from renpy import util
from rpycdec import stmts, utils
logger = logging.getLogger(__name__)
def decompile_file(input_file, output_path=None, **kwargs):
"""
decompile rpyc file into rpy file and write to output.
"""
if not output_path:
name, _ = os.path.splitext(input_file)
output_path = f"{name}.rpy"
output_dir = os.path.dirname(output_path)
if output_dir:
os.makedirs(output_dir, exist_ok=True)
try:
stmt = stmts.load_file(input_file, **kwargs)
code = util.get_code(stmt)
except Exception as e:
logger.error("decode file %s failed: %s", input_file, e)
raise
utils.write_file(output_path, code)
logger.info("decompile %s -> %s", input_file, output_path)
def decompile(input_path, output_path=None, **kwargs):
"""
decompile rpyc file or directory into rpy
Parameters
----------
input_path : str
path to rpyc file or directory contains rpyc files
output_path : str, optional
output path, by default it's same path of input_path.
"""
if not os.path.isdir(input_path):
if output_path:
name, _ = os.path.splitext(os.path.basename(input_path))
output_path = os.path.join(output_path, f"{name}.rpy")
decompile_file(input_path, output_path, **kwargs)
return
if not output_path:
output_path = input_path
for filename in utils.match_files(input_path, r".*\.rpym?c$"):
decompile_file(
os.path.join(input_path, filename),
os.path.join(output_path, filename.removesuffix("c")),
**kwargs,
)

View File

@@ -0,0 +1,249 @@
import logging
import os
import pickle
import re
import zlib
from collections.abc import Iterable
from io import BufferedIOBase
from pathlib import Path
from rpycdec.safe_pickle import rpa_loads
from rpycdec.utils import safe_path
logger = logging.getLogger(__name__)
RPA3_KEY = 0x42424242
RPA3_PADDING = b"Made with Ren'Py."
RPA3_HEADER_PLACEHOLDER = b"RPA-3.0 XXXXXXXXXXXXXXXX XXXXXXXX\n"
def read_until(data: BufferedIOBase, delimiter: int = 0x00) -> bytes:
"""Read bytes from stream until delimiter is found.
Raises ValueError on unexpected EOF.
"""
content = bytearray()
while True:
c = data.read(1)
if not c:
raise ValueError("Unexpected EOF while reading stream")
if c[0] == delimiter:
break
content += c
return bytes(content)
def start_to_bytes(left: list | None) -> bytes:
if not left:
return b""
if isinstance(left[0], bytes):
return left[0]
return left[0].encode("latin-1")
def normalize_archive_name(name: str) -> str:
return name.replace(os.sep, "/")
def collect_rpa_files(
sources: list[str],
base_dir: str | None = None,
output_path: str | None = None,
) -> list[tuple[str, str]]:
"""Collect files to add to an archive.
Returns ``(archive_name, filesystem_path)`` tuples. Directories contribute
all nested files, with deterministic sorting.
"""
if not sources:
raise ValueError("No source files were provided.")
paths = [Path(src) for src in sources]
for path in paths:
if not path.exists():
raise FileNotFoundError(path)
if not path.is_file() and not path.is_dir():
raise ValueError(f"Unsupported source path: {path}")
if base_dir is not None:
base = Path(base_dir).resolve()
elif len(paths) == 1 and paths[0].is_dir():
base = paths[0].resolve()
else:
parents = [
path.resolve().parent if path.is_file() else path.resolve()
for path in paths
]
base = Path(os.path.commonpath([str(parent) for parent in parents]))
output = Path(output_path).resolve() if output_path else None
entries: list[tuple[str, str]] = []
seen: set[str] = set()
for path in sorted(paths, key=lambda p: str(p)):
resolved = path.resolve()
candidates = [resolved]
if resolved.is_dir():
candidates = sorted(
(p for p in resolved.rglob("*") if p.is_file()),
key=lambda p: str(p),
)
for file_path in candidates:
if output is not None and file_path.resolve() == output:
logger.info("skipping output archive: %s", file_path)
continue
try:
archive_name = normalize_archive_name(
str(file_path.resolve().relative_to(base))
)
except ValueError as exc:
raise ValueError(
f"{file_path} is not under base directory {base}"
) from exc
if archive_name in ("", "."):
archive_name = file_path.name
if archive_name in seen:
raise ValueError(f"Duplicate archive path: {archive_name}")
seen.add(archive_name)
entries.append((archive_name, str(file_path)))
return entries
def create_rpa(
output_path: str,
sources: list[str],
base_dir: str | None = None,
key: int = RPA3_KEY,
):
"""Create a Ren'Py RPA-3.0 archive from files or directories."""
entries = collect_rpa_files(sources, base_dir=base_dir, output_path=output_path)
output_dir = os.path.dirname(output_path)
if output_dir:
os.makedirs(output_dir, exist_ok=True)
index: dict[str, list[tuple[int, int, bytes]]] = {}
with open(output_path, "wb") as archive:
archive.write(RPA3_HEADER_PLACEHOLDER)
for archive_name, file_path in entries:
archive.write(RPA3_PADDING)
offset = archive.tell()
size = os.path.getsize(file_path)
with open(file_path, "rb") as source:
while True:
chunk = source.read(1024 * 1024)
if not chunk:
break
archive.write(chunk)
index[archive_name] = [(offset ^ key, size ^ key, b"")]
logger.info("adding: %s", archive_name)
index_offset = archive.tell()
archive.write(zlib.compress(pickle.dumps(index, pickle.HIGHEST_PROTOCOL)))
archive.seek(0)
archive.write(b"RPA-3.0 %016x %08x\n" % (index_offset, key))
def _compile_file_filters(
expressions: str | Iterable[str] | None,
suffixes: str | Iterable[str] | None,
) -> tuple[list[re.Pattern[str]], tuple[str, ...]]:
expression_values = (expressions,) if isinstance(expressions, str) else expressions
suffix_values = (suffixes,) if isinstance(suffixes, str) else suffixes
patterns = [re.compile(expression) for expression in expression_values or ()]
normalized_suffixes = tuple(suffix.casefold() for suffix in suffix_values or ())
if any(not suffix for suffix in normalized_suffixes):
raise ValueError("RPA file suffixes must not be empty.")
return patterns, normalized_suffixes
def _matches_file_filters(
filename: str,
patterns: list[re.Pattern[str]],
suffixes: tuple[str, ...],
) -> bool:
if not patterns and not suffixes:
return True
# RPA paths conventionally use forward slashes. Normalize legacy archives
# so expressions behave consistently on every host platform.
archive_path = filename.replace("\\", "/")
return any(pattern.search(archive_path) for pattern in patterns) or (
bool(suffixes) and archive_path.casefold().endswith(suffixes)
)
def extract_rpa(
r: BufferedIOBase,
output_dir: str | None = None,
expressions: str | Iterable[str] | None = None,
suffixes: str | Iterable[str] | None = None,
):
"""Extract files from an RPA archive.
When *expressions* or *suffixes* are provided, a file is extracted when
its normalized archive path matches any regular expression or suffix.
Regular expressions use :func:`re.search`; suffix matching ignores case.
"""
output_dir = output_dir or "."
patterns, normalized_suffixes = _compile_file_filters(expressions, suffixes)
magic = read_until(r, 0x20)
if magic != b"RPA-3.0":
raise ValueError("Not a Ren'Py RPA-3.0 archive.")
index_offset = int(read_until(r, 0x20), 16)
key = int(read_until(r, 0x0A).decode(), 16)
# read index
r.seek(index_offset)
index = rpa_loads(zlib.decompress(r.read()))
for k, v in index.items():
index[k] = [
(offset ^ key, dlen ^ key, start_to_bytes(left))
for offset, dlen, *left in v
]
for filename, entries in index.items():
# Handle bytes filenames from Python 2 era archives
if isinstance(filename, bytes):
filename = filename.decode("utf-8", errors="surrogateescape")
if not _matches_file_filters(filename, patterns, normalized_suffixes):
logger.debug("skipping filtered file: %s", filename)
continue
# Path traversal protection. Resolve the destination before reading
# the payload so rejected entries do not consume unnecessary memory.
try:
dest = safe_path(output_dir, filename)
except ValueError:
logger.warning("Skipping path traversal attempt: %s", filename)
continue
data = bytearray()
for offset, dlen, start in entries:
r.seek(offset)
block = r.read(dlen)
if start:
if block.startswith(start):
block = block[len(start) :]
else:
logger.warning(
"%s does not start with expected prefix %s", filename, start
)
data += block
os.makedirs(os.path.dirname(dest), exist_ok=True)
with open(dest, "wb") as f:
logger.info("extracting: %s", dest)
f.write(data)

View File

@@ -0,0 +1,257 @@
"""
Safe pickle unpickling utilities.
WARNING: Python's pickle format can execute arbitrary code during
deserialization. This module provides restricted unpicklers that only
allow known-safe classes to be loaded, preventing malicious pickle
files from executing harmful code.
See: https://docs.python.org/3/library/pickle.html#module-pickle
"""
import io
import logging
import pickle
from typing import Any
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Whitelist definitions
# ---------------------------------------------------------------------------
# Standard library modules that are safe to import during unpickling.
SAFE_MODULES: set[str] = {
"collections",
"collections.abc",
}
# Builtin names that are safe to unpickle (data types only).
# Dangerous names like eval, exec, __import__, getattr, etc. are excluded.
SAFE_BUILTINS: set[str] = {
"set",
"frozenset",
"list",
"dict",
"tuple",
"bytes",
"bytearray",
"complex",
"True",
"False",
"None",
"object",
"int",
"float",
"str",
"bool",
"slice",
"range",
"type",
}
# ---------------------------------------------------------------------------
# DummyClass — safe placeholder for unknown classes
# ---------------------------------------------------------------------------
class DummyClass(object):
"""
Safe placeholder class for unpickling unknown classes.
Handles all pickle reconstruction patterns:
- NEWOBJ/REDUCE with arguments (stored as _new_args/_new_kwargs)
- BUILD with dict state, (state, slotstate) tuples, or opaque state
- List-like append operations (APPENDS opcode)
"""
_state: Any
_new_args: tuple[Any, ...] | None
_new_kwargs: dict[str, Any] | None
def __new__(cls, *args: Any, **kwargs: Any) -> "DummyClass":
self = object.__new__(cls)
self._state = None
self._new_args = args if args else None
self._new_kwargs = kwargs if kwargs else None
return self
def __init__(self, *args: Any, **kwargs: Any):
# __new__ already handled storage; avoid overwriting
pass
def append(self, value: Any) -> None:
if self._state is None:
self._state = []
self._state.append(value)
def __getitem__(self, key: str) -> Any:
return self.__dict__[key]
def __eq__(self, other: object) -> bool:
if not isinstance(other, DummyClass):
return NotImplemented
return self.__dict__ == other.__dict__
def __setitem__(self, key: str, value: Any) -> None:
self.__dict__[key] = value
def __getstate__(self) -> Any:
if self._state is not None:
return self._state
# Exclude internal fields from state
return {k: v for k, v in self.__dict__.items() if not k.startswith("_new_")}
def __setstate__(self, state: Any) -> None:
# Handle (state, slotstate) tuple pattern from __slots__ classes
slotstate = None
if (
isinstance(state, tuple)
and len(state) == 2
and (state[0] is None or isinstance(state[0], dict))
and (state[1] is None or isinstance(state[1], dict))
):
state, slotstate = state
if isinstance(state, dict):
self.__dict__.update(state)
elif state is not None:
self._state = state
if slotstate:
self.__dict__.update(slotstate)
def __repr__(self) -> str:
cls = type(self)
module = getattr(cls, "__module__", "?")
name = getattr(cls, "__name__", "?")
if self._state is not None:
return f"<{module}.{name}: state={self._state!r}>"
attrs = {
k: v
for k, v in self.__dict__.items()
if not k.startswith("_new_") and k != "_state"
}
return f"<{module}.{name}: {attrs}>"
def get_code(self, **kwargs) -> str:
name = getattr(type(self), "__name__", "<?>")
logger.warning("Unrecognized node %s in decompile output", name)
return f"# <unrecognized: {name}>"
def make_dummy_class(module: str, name: str) -> type:
"""Create a named DummyClass subclass for a given module.name."""
return type(name, (DummyClass,), {"__module__": module})
# ---------------------------------------------------------------------------
# SafeUnpickler — base restricted unpickler
# ---------------------------------------------------------------------------
class SafeUnpickler(pickle.Unpickler):
"""Base restricted unpickler with whitelist-based class loading.
Security model:
- ``renpy.*`` / ``store.*``: allowed (our fake packages)
- ``builtins`` / ``__builtin__``: only safe data types
- ``SAFE_MODULES``: standard library modules known to be safe
- Everything else: handled by ``_on_unknown_class()``
Subclasses can override ``_on_unknown_class()`` to customise behaviour
for classes outside the whitelist (raise, create placeholder, etc.).
"""
def find_class(self, module: str, name: str) -> Any:
# store.* modules -> DummyClass placeholder
if module.startswith("store"):
return make_dummy_class(module, name)
# Our fake renpy package is safe to import
if module.startswith("renpy"):
try:
return super().find_class(module, name)
except (ImportError, AttributeError):
return self._on_unknown_class(module, name)
# Whitelisted standard library modules
if module in SAFE_MODULES:
return super().find_class(module, name)
# Safe builtins only (excludes eval, exec, __import__, etc.)
if module in ("builtins", "__builtin__") and name in SAFE_BUILTINS:
return super().find_class(module, name)
return self._on_unknown_class(module, name)
def _on_unknown_class(self, module: str, name: str) -> Any:
"""Handle a class reference outside the whitelist.
Default: log a warning and return a DummyClass placeholder.
Subclasses may override to raise or return different placeholders.
"""
logger.warning(
f"Unknown class encountered: {module}.{name}. "
f"Substituting with DummyClass placeholder. "
f"If this is a legitimate Ren'Py class, please report an issue."
)
return make_dummy_class(module, name)
# ---------------------------------------------------------------------------
# RestrictedUnpickler — only allows primitive/builtin data types
# ---------------------------------------------------------------------------
# Primitive builtin types that are always safe.
_PRIMITIVE_BUILTINS = {
"dict",
"list",
"tuple",
"set",
"frozenset",
"bytes",
"bytearray",
"str",
"int",
"float",
"bool",
"complex",
"True",
"False",
"None",
}
class RestrictedUnpickler(pickle.Unpickler):
"""Strictly restricted unpickler that only allows primitive data types.
Suitable for data that should only contain basic Python types
(dict, list, tuple, int, str, bytes, etc.) with no custom classes.
Used by RPA index parsing and any other context where only
primitive data structures are expected.
"""
def find_class(self, module: str, name: str) -> Any:
if module in ("builtins", "__builtin__") and name in _PRIMITIVE_BUILTINS:
return super().find_class(module, name)
raise pickle.UnpicklingError(
f"Refusing to unpickle non-primitive class: {module}.{name}"
)
def safe_loads(data: bytes, **kwargs: Any) -> Any:
"""Safely unpickle data using SafeUnpickler.
Unknown classes are substituted with DummyClass placeholders.
"""
return SafeUnpickler(io.BytesIO(data), **kwargs).load()
def rpa_loads(data: bytes) -> Any:
"""Safely unpickle RPA index data (very restricted, primitives only)."""
return RestrictedUnpickler(io.BytesIO(data), encoding="bytes").load()

View File

@@ -0,0 +1,697 @@
"""
Ren'Py Save File Parser and Writer
Ren'Py save files are ZIP archives containing:
- screenshot.png: Save screenshot
- extra_info: UTF-8 encoded save name/description
- json: JSON metadata (version, timestamp, etc.)
- renpy_version: Ren'Py version string
- log: Pickle serialized game state (roots, log)
- signatures: Digital signature for integrity verification
This module provides functionality to:
1. Extract save files to JSON for manual editing
2. Restore edited JSON back to valid save files
"""
import base64
import io
import json
import os
import pickle
import pickletools
import zipfile
from typing import Any
from rpycdec.safe_pickle import (
SAFE_MODULES,
SafeUnpickler,
)
class DynamicObject:
"""
A dynamic object that can represent any Ren'Py class during unpickling.
Stores the original class name and module for JSON round-trip serialization.
Unlike ``DummyClass`` (safe_pickle.py) which spreads state into ``__dict__``
for attribute access during decompilation, ``DynamicObject`` keeps state
as-is in ``_state`` so it can be faithfully serialized to / from JSON.
Handles all pickle reconstruction patterns:
- NEWOBJ/REDUCE with arguments
- BUILD with state (dict or opaque)
- APPENDS (list-like append)
- SETITEMS (dict-like __setitem__)
"""
# Class-level defaults — overridden by type() in _on_unknown_class.
# Using class attrs ensures they survive even when pickle skips __init__
# (e.g. the NEWOBJ opcode calls __new__ only).
_class_name: str = ""
_module_name: str = ""
_state: Any
_items: list[Any] | None
_new_args: tuple[Any, ...] | None
def __new__(cls, *args: Any, **kwargs: Any) -> "DynamicObject":
self = object.__new__(cls)
self._state = None
self._items = None
self._new_args = args if args else None
return self
def __init__(self, *args: Any, **kwargs: Any):
# __new__ already handled initialization; nothing to do.
pass
# -- pickle BUILD --
def __setstate__(self, state: Any) -> None:
self._state = state
def __getstate__(self) -> Any:
return self._state
# -- pickle APPENDS --
def append(self, item: Any) -> None:
if self._items is None:
self._items = []
self._items.append(item)
# -- pickle SETITEMS --
def __setitem__(self, key: Any, value: Any) -> None:
if self._state is None:
self._state = {}
if isinstance(self._state, dict):
self._state[key] = value
# -- re-pickling / repr --
def __reduce__(self):
"""Support re-pickling by returning the original class info."""
return (
_reconstruct_object,
(self._module_name, self._class_name, self._state),
)
def __repr__(self) -> str:
return f"<{self._module_name}.{self._class_name}: {self._state}>"
# -- JSON round-trip helpers --
def to_json_dict(self) -> dict:
"""Convert to a JSON-serializable dictionary."""
result: dict[str, Any] = {
"__class__": self._class_name,
"__module__": self._module_name,
"__state__": _to_json_serializable(self._state),
}
if self._items:
result["__items__"] = _to_json_serializable(self._items)
return result
@classmethod
def from_json_dict(cls, data: dict) -> "DynamicObject":
"""Create a DynamicObject from a JSON dictionary."""
obj = cls()
obj._class_name = data.get("__class__", "")
obj._module_name = data.get("__module__", "")
obj._state = _from_json_serializable(data.get("__state__"))
items = data.get("__items__")
if items:
obj._items = _from_json_serializable(items)
return obj
# Modules allowed for reconstruction (renpy/store fake packages + safe stdlib)
_RECONSTRUCT_SAFE_PREFIXES = ("renpy.", "store.")
def _reconstruct_object(module_name: str, class_name: str, state: Any) -> Any:
"""Reconstruct an object from its module, class name, and state.
Only allows importing from renpy.*/store.* (our fake packages) and
whitelisted standard library modules. All other modules fall back
to DynamicObject.
"""
# Only allow our fake packages and whitelisted modules
is_safe = (
any(module_name.startswith(p) for p in _RECONSTRUCT_SAFE_PREFIXES)
or module_name in ("renpy", "store")
or module_name in SAFE_MODULES
)
if is_safe:
try:
module = __import__(module_name, fromlist=[class_name])
cls = getattr(module, class_name)
obj = object.__new__(cls)
if state is not None:
if hasattr(obj, "__setstate__"):
obj.__setstate__(state)
elif isinstance(state, dict):
obj.__dict__.update(state)
return obj
except (ImportError, AttributeError):
pass
# Fall back to DynamicObject
obj = DynamicObject()
obj._class_name = class_name
obj._module_name = module_name
obj._state = state
return obj
class SaveUnpickler(SafeUnpickler):
"""Safe unpickler for Ren'Py save files.
Extends SafeUnpickler to replace unknown classes with DynamicObject
instances (instead of raising) so save data can be serialized to JSON.
"""
def __init__(
self,
file,
*,
fix_imports: bool = False,
encoding: str = "ASCII",
errors: str = "strict",
verbose: bool = False,
):
super().__init__(
file, fix_imports=fix_imports, encoding=encoding, errors=errors
)
self.verbose = verbose
def _on_unknown_class(self, module: str, name: str) -> type:
"""Return a DynamicObject subclass instead of raising."""
if self.verbose:
print(f"Creating dynamic class: {module}.{name}")
return type(
name,
(DynamicObject,),
{
"__module__": module,
"_class_name": name,
"_module_name": module,
},
)
class SavePickler(pickle.Pickler):
"""
Custom pickler that handles DynamicObject instances by restoring
their original class information.
"""
def reducer_override(self, obj):
if isinstance(obj, DynamicObject):
# Return a reduction that will reconstruct the original class
return (
_reconstruct_object,
(obj._module_name, obj._class_name, obj._state),
)
# Fall back to default behavior
return NotImplemented
def _to_json_serializable(obj: Any) -> Any:
"""Convert a Python object to a JSON-serializable structure."""
if obj is None or isinstance(obj, (bool, int, float, str)):
return obj
if isinstance(obj, bytes):
return {"__bytes__": base64.b64encode(obj).decode("ascii")}
if isinstance(obj, tuple):
return {"__tuple__": [_to_json_serializable(item) for item in obj]}
if isinstance(obj, list):
return [_to_json_serializable(item) for item in obj]
if isinstance(obj, dict):
result = {}
for key, value in obj.items():
# JSON keys must be strings
if isinstance(key, str):
json_key = key
elif isinstance(key, (int, float, bool)):
json_key = f"__key_{type(key).__name__}__:{key}"
elif isinstance(key, tuple):
json_key = f"__key_tuple__:{json.dumps(_to_json_serializable(key))}"
else:
json_key = f"__key_repr__:{repr(key)}"
result[json_key] = _to_json_serializable(value)
return result
if isinstance(obj, set):
return {"__set__": [_to_json_serializable(item) for item in obj]}
if isinstance(obj, frozenset):
return {"__frozenset__": [_to_json_serializable(item) for item in obj]}
if isinstance(obj, DynamicObject):
return obj.to_json_dict()
# For other objects, try to extract state
if hasattr(obj, "__dict__"):
return {
"__class__": obj.__class__.__name__,
"__module__": obj.__class__.__module__,
"__state__": _to_json_serializable(obj.__dict__),
}
if hasattr(obj, "__getstate__"):
return {
"__class__": obj.__class__.__name__,
"__module__": obj.__class__.__module__,
"__state__": _to_json_serializable(obj.__getstate__()),
}
# Fallback: convert to string representation
return {"__repr__": repr(obj), "__type__": type(obj).__name__}
def _from_json_serializable(obj: Any) -> Any:
"""Convert a JSON structure back to Python objects."""
if obj is None or isinstance(obj, (bool, int, float, str)):
return obj
if isinstance(obj, list):
return [_from_json_serializable(item) for item in obj]
if isinstance(obj, dict):
# Check for special types
if "__bytes__" in obj:
return base64.b64decode(obj["__bytes__"])
if "__tuple__" in obj:
return tuple(_from_json_serializable(item) for item in obj["__tuple__"])
if "__set__" in obj:
return set(_from_json_serializable(item) for item in obj["__set__"])
if "__frozenset__" in obj:
return frozenset(
_from_json_serializable(item) for item in obj["__frozenset__"]
)
if "__class__" in obj and "__module__" in obj:
return DynamicObject.from_json_dict(obj)
if "__repr__" in obj:
# Cannot reconstruct, return as-is with warning marker
return {"__unrecoverable__": obj["__repr__"]}
# Regular dict with possibly encoded keys
result = {}
for key, value in obj.items():
if key.startswith("__key_int__:"):
actual_key = int(key.split(":", 1)[1])
elif key.startswith("__key_float__:"):
actual_key = float(key.split(":", 1)[1])
elif key.startswith("__key_bool__:"):
actual_key = key.split(":", 1)[1] == "True"
elif key.startswith("__key_tuple__:"):
actual_key = tuple(
_from_json_serializable(json.loads(key.split(":", 1)[1]))
)
elif key.startswith("__key_repr__:"):
# Cannot fully reconstruct, use string
actual_key = key.split(":", 1)[1]
else:
actual_key = key
result[actual_key] = _from_json_serializable(value)
return result
return obj
def load_save_zip(file_path: str) -> dict:
"""
Load all components from a Ren'Py save file.
Returns a dict with keys:
- screenshot: bytes or None
- extra_info: str
- json: dict (parsed JSON metadata)
- renpy_version: str
- log: bytes (raw pickle data)
- signatures: str
"""
with zipfile.ZipFile(file_path, "r") as zf:
result = {
"screenshot": None,
"extra_info": "",
"json": {},
"renpy_version": "",
"log": b"",
"signatures": "",
}
namelist = zf.namelist()
if "screenshot.png" in namelist:
result["screenshot"] = zf.read("screenshot.png")
if "extra_info" in namelist:
result["extra_info"] = zf.read("extra_info").decode("utf-8")
if "json" in namelist:
result["json"] = json.loads(zf.read("json").decode("utf-8"))
if "renpy_version" in namelist:
result["renpy_version"] = zf.read("renpy_version").decode("utf-8")
if "log" in namelist:
result["log"] = zf.read("log")
if "signatures" in namelist:
result["signatures"] = zf.read("signatures").decode("utf-8")
return result
def parse_save_log(log_data: bytes, verbose: bool = False) -> tuple[Any, Any]:
"""
Parse the pickle log data from a save file.
Returns (roots, log) tuple.
"""
unpickler = SaveUnpickler(
io.BytesIO(log_data),
verbose=verbose,
encoding="utf-8",
errors="surrogateescape",
)
return unpickler.load()
def serialize_save_log(roots: Any, log: Any) -> bytes:
"""
Serialize roots and log back to pickle format.
"""
buffer = io.BytesIO()
pickler = SavePickler(buffer, protocol=2)
pickler.dump((roots, log))
return buffer.getvalue()
def extract_save(
file_path: str,
extracted_dir: str = "",
dissemble: bool = False,
verbose: bool = False,
**kwargs,
):
"""
Extract a Ren'Py save file to a directory with JSON files for editing.
Creates:
- metadata.json: Contains extra_info, json metadata, renpy_version, signatures
- roots.json: The game state roots (variables, etc.)
- log.json: The game log/history
- screenshot.png: The save screenshot (if present)
Args:
file_path: Path to the .save file
extracted_dir: Output directory (default: {file_path}.extracted)
dissemble: If True, print pickle disassembly to stdout
verbose: If True, print verbose class loading info
"""
if not extracted_dir:
extracted_dir = file_path + ".extracted"
print(f"Extracting save file: {file_path}")
print(f"Output directory: {extracted_dir}")
# Load the save file
save_data = load_save_zip(file_path)
if dissemble:
print("\n=== Pickle Disassembly ===")
pickletools.dis(save_data["log"])
print("=== End Disassembly ===\n")
# Parse the log data
roots, log = parse_save_log(save_data["log"], verbose=verbose)
# Create output directory
os.makedirs(extracted_dir, exist_ok=True)
# Save metadata
metadata = {
"extra_info": save_data["extra_info"],
"json": save_data["json"],
"renpy_version": save_data["renpy_version"],
"signatures": save_data["signatures"],
}
metadata_path = os.path.join(extracted_dir, "metadata.json")
with open(metadata_path, "w", encoding="utf-8") as f:
json.dump(metadata, f, indent=2, ensure_ascii=False)
print(" Saved: metadata.json")
# Save roots
roots_path = os.path.join(extracted_dir, "roots.json")
with open(roots_path, "w", encoding="utf-8") as f:
json.dump(_to_json_serializable(roots), f, indent=2, ensure_ascii=False)
print(" Saved: roots.json")
# Save log
log_path = os.path.join(extracted_dir, "log.json")
with open(log_path, "w", encoding="utf-8") as f:
json.dump(_to_json_serializable(log), f, indent=2, ensure_ascii=False)
print(" Saved: log.json")
# Save screenshot if present
if save_data["screenshot"]:
screenshot_path = os.path.join(extracted_dir, "screenshot.png")
with open(screenshot_path, "wb") as f:
f.write(save_data["screenshot"])
print(" Saved: screenshot.png")
print("\nExtraction complete!")
print("Edit roots.json or log.json, then use 'restore' to create a new save file.")
def restore_save(
extracted_dir: str,
output_file: str = "",
key_file: str = "",
**kwargs,
):
"""
Restore a Ren'Py save file from extracted JSON files.
Reads:
- metadata.json: Contains extra_info, json metadata, renpy_version, signatures
- roots.json: The game state roots
- log.json: The game log/history
- screenshot.png: The save screenshot (optional)
Args:
extracted_dir: Directory containing extracted JSON files
output_file: Output save file path (default: {extracted_dir}.save)
key_file: Path to security_keys.txt for re-signing (optional)
"""
if not output_file:
output_file = (
extracted_dir.rstrip("/").removesuffix(".extracted") + ".restored.save"
)
print(f"Restoring save file from: {extracted_dir}")
print(f"Output file: {output_file}")
# Load metadata
metadata_path = os.path.join(extracted_dir, "metadata.json")
with open(metadata_path, "r", encoding="utf-8") as f:
metadata = json.load(f)
print(" Loaded: metadata.json")
# Load roots
roots_path = os.path.join(extracted_dir, "roots.json")
with open(roots_path, "r", encoding="utf-8") as f:
roots_json = json.load(f)
roots = _from_json_serializable(roots_json)
print(" Loaded: roots.json")
# Load log
log_path = os.path.join(extracted_dir, "log.json")
with open(log_path, "r", encoding="utf-8") as f:
log_json = json.load(f)
log = _from_json_serializable(log_json)
print(" Loaded: log.json")
# Load screenshot if present
screenshot = None
screenshot_path = os.path.join(extracted_dir, "screenshot.png")
if os.path.exists(screenshot_path):
with open(screenshot_path, "rb") as f:
screenshot = f.read()
print(" Loaded: screenshot.png")
# Serialize the game state
log_data = serialize_save_log(roots, log)
# Handle signatures
if key_file:
print(f" Re-signing with key: {key_file}")
signatures = sign_data(log_data, key_file)
print(" Generated new signature")
else:
# Keep original signatures (will be invalid for modified saves)
signatures = metadata.get("signatures", "")
# Create the save file
output_file_tmp = output_file + ".tmp"
with zipfile.ZipFile(output_file_tmp, "w", zipfile.ZIP_DEFLATED) as zf:
# Screenshot
if screenshot:
zf.writestr("screenshot.png", screenshot)
# Extra info
zf.writestr("extra_info", metadata.get("extra_info", "").encode("utf-8"))
# JSON metadata
zf.writestr("json", json.dumps(metadata.get("json", {})).encode("utf-8"))
# Ren'Py version
zf.writestr("renpy_version", metadata.get("renpy_version", "").encode("utf-8"))
# Log data
zf.writestr("log", log_data)
# Signatures
zf.writestr("signatures", signatures.encode("utf-8"))
# Atomic rename
if os.path.exists(output_file):
os.unlink(output_file)
os.rename(output_file_tmp, output_file)
print("\nRestore complete!")
print(f"Save file created: {output_file}")
if not key_file:
print(
"\nNote: No signing key provided. Original signatures preserved but invalid."
)
print("Use --key to provide security_keys.txt for valid signatures.")
print("Some games with strict signature checking may reject modified saves.")
def sign_data(log_data: bytes, key_file: str) -> str:
"""
Sign log data using ECDSA keys from a Ren'Py security_keys.txt file.
The key file is typically located at:
<game_save_dir>/tokens/security_keys.txt
Args:
log_data: The pickle-serialized game state (log) data
key_file: Path to security_keys.txt containing the signing key
Returns:
Signature string in Ren'Py format
"""
try:
import ecdsa
except ImportError:
raise ImportError(
"ecdsa library required for signing. Install with: pip install ecdsa"
)
def encode_line(kind: str, key: bytes, sig: bytes = b"") -> str:
"""Encode a signature line in Ren'Py format."""
key_b64 = base64.b64encode(key).decode("ascii")
if sig:
sig_b64 = base64.b64encode(sig).decode("ascii")
return f"{kind} {key_b64} {sig_b64}\n"
return f"{kind} {key_b64}\n"
def decode_line(line: str) -> tuple[str, bytes, bytes]:
"""Decode a signature line from Ren'Py format."""
parts = line.strip().split()
if len(parts) < 2:
return "", b"", b""
kind = parts[0]
key = base64.b64decode(parts[1])
sig = base64.b64decode(parts[2]) if len(parts) > 2 else b""
return kind, key, sig
signatures = ""
with open(key_file, "r") as f:
for line in f:
kind, key_der, _ = decode_line(line)
if kind == "signing-key":
try:
sk = ecdsa.SigningKey.from_der(key_der)
if sk is not None and sk.verifying_key is not None:
sig = sk.sign(log_data)
signatures += encode_line(
"signature", sk.verifying_key.to_der(), sig
)
except Exception as e:
print(f"Warning: Failed to sign with key: {e}")
return signatures
def generate_new_key(key_file: str) -> str:
"""
Generate a new ECDSA signing key and save to file.
This creates a new security_keys.txt that can be used to sign saves.
Note: Saves signed with this key will trigger a trust dialog in the game.
Args:
key_file: Path to save the new security_keys.txt
Returns:
Path to the created key file
"""
try:
import ecdsa
except ImportError:
raise ImportError("ecdsa library required. Install with: pip install ecdsa")
sk = ecdsa.SigningKey.generate(curve=ecdsa.NIST256p)
vk = sk.verifying_key
if vk is not None:
sk_b64 = base64.b64encode(sk.to_der()).decode("ascii")
vk_b64 = base64.b64encode(vk.to_der()).decode("ascii")
line = f"signing-key {sk_b64} {vk_b64}\n"
os.makedirs(os.path.dirname(key_file) or ".", exist_ok=True)
with open(key_file, "w") as f:
f.write(line)
print(f"Generated new signing key: {key_file}")
return key_file
raise RuntimeError("Failed to generate signing key")
def dump_save_info(file_path: str):
"""
Print summary information about a save file.
"""
save_data = load_save_zip(file_path)
print(f"Save file: {file_path}")
print("\n=== Metadata ===")
print(f"Extra info: {save_data['extra_info']}")
print(f"Ren'Py version: {save_data['renpy_version']}")
print("\n=== JSON Metadata ===")
for key, value in save_data["json"].items():
print(f" {key}: {value}")
print("\n=== Contents ===")
print(f"Screenshot: {'Yes' if save_data['screenshot'] else 'No'}")
print(f"Log data size: {len(save_data['log'])} bytes")
print(f"Has signatures: {'Yes' if save_data['signatures'] else 'No'}")

View File

@@ -0,0 +1,87 @@
import io
import logging
import pickletools
import struct
import zlib
from os import path
from renpy.ast import Node
from rpycdec.safe_pickle import SafeUnpickler
logger = logging.getLogger(__name__)
# A string at the start of each rpycv2 file.
RPYC2_HEADER = b"RENPY RPC2"
def read_rpyc_data(file: io.BufferedReader, slot):
"""
Reads the binary data from `slot` in a .rpyc (v1 or v2) file. Returns
the data if the slot exists, or None if the slot does not exist.
"""
header_data = file.read(1024)
# Legacy path.
if header_data[: len(RPYC2_HEADER)] != RPYC2_HEADER:
if slot != 1:
return None
file.seek(0)
data = file.read()
return zlib.decompress(data)
# RPYC2 path.
pos = len(RPYC2_HEADER)
while True:
header_slot, start, length = struct.unpack("III", header_data[pos : pos + 12])
if slot == header_slot:
break
if header_slot == 0:
return None
pos += 12
file.seek(start)
data = file.read(length)
return zlib.decompress(data)
def load(
data: io.BufferedReader, slots: list[int] | None = None, **kwargs
) -> list[Node] | None:
"""Load Ren'Py AST from a .rpyc file.
Tries each slot in order. Slot 1 is the original script,
slot 2 is the pre-translated version.
"""
if slots is None:
slots = [1, 2]
# 1 is statements before translation, 2 is after translation.
for slot in slots:
try:
bindata = read_rpyc_data(data, slot)
if not bindata:
continue
if kwargs.get("dis", False):
logger.info("Disassembling rpyc file...")
pickletools.dis(bindata)
unpickler = SafeUnpickler(
io.BytesIO(bindata), encoding="utf-8", errors="surrogateescape"
)
metadata, stmts = unpickler.load()
return stmts
except Exception as e:
logger.warning(f"Failed to read slot {slot}: {e}")
data.seek(0)
continue
raise ValueError("Unsupported file format or invalid file")
def load_file(filename, **kwargs) -> list[Node] | None:
"""Load Ren'Py AST from a .rpyc/.rpymc file."""
ext = path.splitext(filename)[1]
if ext in [".rpy", ".rpym"]:
raise NotImplementedError(
"Parsing .rpy files is not supported. Use renpy.parser.parse() from Ren'Py's SDK."
)
# slot 2 is for pre-translated scripts, slot 1 is for normal scripts
slots = [2, 1] if kwargs.get("pre_translated", False) else [1, 2]
with open(filename, "rb") as file:
return load(file, slots=slots, **kwargs)

View File

@@ -0,0 +1,473 @@
import collections
import datetime
import hashlib
import logging
import os
from collections.abc import Callable, Iterator
from dataclasses import dataclass, field
from typing import Optional
import renpy.ast
from rpycdec import stmts, utils
logger = logging.getLogger(__name__)
# ============================================================================
# Translation Extraction Data Structures
# ============================================================================
@dataclass
class DialogueTranslation:
"""Dialogue translation item - generates translate {identifier}: format"""
identifier: str # Translation identifier e.g. "start_a1b2c3d4"
filename: str # Source file
linenumber: int # Line number
who: Optional[str] # Speaker
what: str # Dialogue content
code: str # Original code (for comments)
@dataclass
class StringTranslation:
"""String translation item - generates translate strings: format"""
filename: str
linenumber: int
text: str # Text to translate
source: str = "string" # Source: "menu", "string", "screen", etc.
@dataclass
class TranslationExtractor:
"""Extract translation content from AST"""
label: Optional[str] = None
identifiers: set[str] = field(default_factory=set)
dialogues: list[DialogueTranslation] = field(default_factory=list)
strings: list[StringTranslation] = field(default_factory=list)
_seen_strings: set[str] = field(default_factory=set)
def extract_file(self, filename: str, nodes: list[renpy.ast.Node]) -> None:
"""Extract translations from a single file"""
for node in self._walk_nodes(nodes):
self._process_node(node, filename)
def _walk_nodes(self, nodes: list[renpy.ast.Node]) -> Iterator[renpy.ast.Node]:
"""Recursively walk all nodes"""
if not nodes:
return
for node in nodes:
yield node
# Handle different node types - each node type should only be processed once
if isinstance(node, renpy.ast.If):
# If node has entries with blocks
for _, block in node.entries:
if block:
yield from self._walk_nodes(block)
elif isinstance(node, renpy.ast.Menu):
# Menu node has items with blocks
for _, _, block in node.items:
if block:
yield from self._walk_nodes(block)
elif isinstance(node, renpy.ast.UserStatement):
# UserStatement may have code_block and subparses
if hasattr(node, "code_block") and node.code_block:
yield from self._walk_nodes(node.code_block)
if hasattr(node, "subparses") and node.subparses:
for subparse in node.subparses:
if hasattr(subparse, "block") and subparse.block:
yield from self._walk_nodes(subparse.block)
elif hasattr(node, "block") and node.block:
# Generic block handling for Label, While, Init, Translate, etc.
yield from self._walk_nodes(node.block)
def _process_node(self, node: renpy.ast.Node, filename: str) -> None:
"""Process a single node"""
# Track label
if isinstance(node, renpy.ast.Label):
if not getattr(node, "hide", False):
name = (
node.get_name()
if hasattr(node, "get_name")
else getattr(node, "name", None)
)
if name and isinstance(name, str) and not name.startswith("_"):
self.label = name
# Say node -> dialogue translation
if isinstance(node, renpy.ast.Say):
self._extract_say(node, filename)
# TranslateSay node - already translated, skip
elif isinstance(node, renpy.ast.TranslateSay):
pass # Already a translation node, no need to extract
# Menu node -> string translation
elif isinstance(node, renpy.ast.Menu):
self._extract_menu(node, filename)
# UserStatement node -> string translation
elif isinstance(node, renpy.ast.UserStatement):
self._extract_user_statement(node, filename)
def _extract_say(self, node: renpy.ast.Say, filename: str) -> None:
"""Extract Say node"""
what = getattr(node, "what", None)
if not what:
return
identifier = self._generate_identifier(node)
who = getattr(node, "who", None)
self.dialogues.append(
DialogueTranslation(
identifier=identifier,
filename=filename,
linenumber=node.linenumber,
who=who,
what=what,
code=node.get_code(),
)
)
def _extract_menu(self, node: renpy.ast.Menu, filename: str) -> None:
"""Extract Menu node options"""
items = getattr(node, "items", [])
for caption, condition, block in items:
if caption and caption not in self._seen_strings:
self._seen_strings.add(caption)
# Calculate correct line number
if block:
line = block[0].linenumber - 1
else:
line = node.linenumber
self.strings.append(
StringTranslation(
filename=filename,
linenumber=line,
text=caption,
source="menu",
)
)
def _extract_user_statement(
self, node: renpy.ast.UserStatement, filename: str
) -> None:
"""Extract strings from UserStatement node"""
try:
translation_strings = node.get_translation_strings()
for item in translation_strings:
if isinstance(item, tuple):
line, text = item
else:
line, text = node.linenumber, item
if text and text not in self._seen_strings:
self._seen_strings.add(text)
self.strings.append(
StringTranslation(
filename=filename,
linenumber=line,
text=text,
source="user_statement",
)
)
except Exception:
pass # Some UserStatements may not implement get_translation_strings
def _generate_identifier(self, node: renpy.ast.Node) -> str:
"""Generate translation identifier (Ren'Py compatible)"""
# Check for explicit identifier
explicit_id = getattr(node, "identifier", None)
if explicit_id and getattr(node, "explicit_identifier", False):
if explicit_id not in self.identifiers:
self.identifiers.add(explicit_id)
return explicit_id
# Calculate MD5
code = node.get_code()
md5 = hashlib.md5()
md5.update((code + "\r\n").encode("utf-8"))
digest = md5.hexdigest()[:8]
identifier = unique_identifier(self.label, digest, self.identifiers)
self.identifiers.add(identifier)
return identifier
# ============================================================================
# Helper Functions
# ============================================================================
def unique_identifier(
label: str | None, digest: str, existing_identifiers: set | None = None
) -> str:
"""Generate a unique translation identifier"""
if existing_identifiers is None:
existing_identifiers = set()
if label is None:
base = digest
else:
base = label.replace(".", "_") + "_" + digest
i = 0
suffix = ""
while True:
identifier = base + suffix
if identifier not in existing_identifiers:
break
i += 1
suffix = "_{0}".format(i)
return identifier
# ============================================================================
# Translation File Generation
# ============================================================================
def quote_unicode(s: str) -> str:
"""Escape special characters in string"""
s = s.replace("\\", "\\\\")
s = s.replace('"', '\\"')
s = s.replace("\a", "\\a")
s = s.replace("\b", "\\b")
s = s.replace("\f", "\\f")
s = s.replace("\n", "\\n")
s = s.replace("\r", "\\r")
s = s.replace("\t", "\\t")
s = s.replace("\v", "\\v")
return s
def elide_filename(filename: str, game_dir: str = "") -> str:
"""Simplify filename for comments"""
if game_dir and filename.startswith(game_dir):
filename = filename[len(game_dir) :].lstrip(os.sep)
# Convert .rpyc to .rpy for display
if filename.endswith(".rpyc"):
filename = filename[:-1]
elif filename.endswith(".rpymc"):
filename = filename[:-1]
return filename
def get_translation_filename(source_filename: str) -> str:
"""Generate translation filename from source filename, preserving directory structure"""
# Remove .rpyc/.rpymc compiled suffix to get the source .rpy/.rpym extension
if source_filename.endswith(".rpyc"):
return source_filename[:-1]
elif source_filename.endswith(".rpymc"):
return source_filename[:-1]
return source_filename
def write_dialogue_translations(
dialogues: list[DialogueTranslation],
output_dir: str,
language: str,
game_dir: str = "",
empty_translation: bool = True,
filter_func: Optional[Callable[[str], str]] = None,
) -> dict[str, int]:
"""
Generate dialogue translation files.
Returns:
dict: {filename: count} number of translations written per file
"""
# Group by source file
by_file: dict[str, list[DialogueTranslation]] = collections.defaultdict(list)
for d in dialogues:
tl_filename = get_translation_filename(d.filename)
by_file[tl_filename].append(d)
result = {}
for tl_filename, items in by_file.items():
tl_path = os.path.join(output_dir, tl_filename)
os.makedirs(
os.path.dirname(tl_path) if os.path.dirname(tl_path) else output_dir,
exist_ok=True,
)
with open(tl_path, "w", encoding="utf-8") as f:
f.write(
f"# TODO: Translation updated at {datetime.datetime.now().isoformat()}\n\n"
)
for item in items:
# Write source file location comment
elided = elide_filename(item.filename, game_dir)
f.write(f"# {elided}:{item.linenumber}\n")
# Write translate block
f.write(f"translate {language} {item.identifier}:\n\n")
# Write original code as comment
f.write(f" # {item.code}\n")
# Write translation content
if empty_translation:
new_what = ""
elif filter_func:
new_what = filter_func(item.what)
else:
new_what = item.what
# Generate translated code
if item.who:
f.write(f' {item.who} "{quote_unicode(new_what)}"\n')
else:
f.write(f' "{quote_unicode(new_what)}"\n')
f.write("\n")
result[tl_filename] = len(items)
logger.info(f"Wrote {len(items)} dialogue translations to {tl_path}")
return result
def write_string_translations(
strings: list[StringTranslation],
output_dir: str,
language: str,
game_dir: str = "",
empty_translation: bool = True,
filter_func: Optional[Callable[[str], str]] = None,
) -> int:
"""
Generate string translation file.
Returns:
int: number of translations written
"""
if not strings:
return 0
tl_path = os.path.join(output_dir, "strings.rpy")
with open(tl_path, "w", encoding="utf-8") as f:
f.write(
f"# TODO: Translation updated at {datetime.datetime.now().isoformat()}\n\n"
)
f.write(f"translate {language} strings:\n\n")
for item in strings:
# Write source file location comment
elided = elide_filename(item.filename, game_dir)
f.write(f" # {elided}:{item.linenumber}\n")
# Write old/new
old_text = quote_unicode(item.text)
if empty_translation:
new_text = ""
elif filter_func:
new_text = quote_unicode(filter_func(item.text))
else:
new_text = old_text
f.write(f' old "{old_text}"\n')
f.write(f' new "{new_text}"\n\n')
logger.info(f"Wrote {len(strings)} string translations to {tl_path}")
return len(strings)
def extract_translations(
game_dir: str,
output_dir: str,
language: str,
*,
include_dialogues: bool = True,
include_strings: bool = True,
empty_translation: bool = True,
filter_func: Optional[Callable[[str], str]] = None,
) -> dict:
"""
Extract translation templates from .rpyc files.
Args:
game_dir: Game directory (containing .rpyc files)
output_dir: Output directory (will create tl/{language}/ structure)
language: Target language code e.g. "chinese", "japanese"
include_dialogues: Whether to extract dialogue translations
include_strings: Whether to extract string translations
empty_translation: True=new is empty, False=new copies old
filter_func: Optional text filter/transform function
Returns:
dict: Extraction statistics
"""
# Create output directory tl/{language}/
tl_output_dir = os.path.join(output_dir, "tl", language)
os.makedirs(tl_output_dir, exist_ok=True)
# Find all .rpyc/.rpymc files
matches = utils.match_files(game_dir, r".*\.rpym?c$")
logger.info(f"Found {len(matches)} rpyc files in {game_dir}")
# Create extractor
extractor = TranslationExtractor()
# Process all files
for filename in matches:
filepath = os.path.join(game_dir, filename)
logger.info(f"Processing {filename}")
try:
loaded_stmts = stmts.load_file(filepath)
if loaded_stmts:
extractor.extract_file(filename, loaded_stmts)
except Exception as e:
logger.error(f"Failed to process {filename}: {e}")
continue
# Statistics
stats = {
"files_processed": len(matches),
"dialogues_found": len(extractor.dialogues),
"strings_found": len(extractor.strings),
"dialogues_written": 0,
"strings_written": 0,
}
# Generate translation files
if include_dialogues and extractor.dialogues:
dialogue_stats = write_dialogue_translations(
extractor.dialogues,
tl_output_dir,
language,
game_dir=game_dir,
empty_translation=empty_translation,
filter_func=filter_func,
)
stats["dialogues_written"] = sum(dialogue_stats.values())
stats["dialogue_files"] = dialogue_stats
if include_strings and extractor.strings:
stats["strings_written"] = write_string_translations(
extractor.strings,
tl_output_dir,
language,
game_dir=game_dir,
empty_translation=empty_translation,
filter_func=filter_func,
)
logger.info(
f"Extraction complete: {stats['dialogues_written']} dialogues, "
f"{stats['strings_written']} strings written to {tl_output_dir}"
)
return stats

View File

@@ -0,0 +1,56 @@
import os
import re
def safe_path(base_dir: str, filename: str) -> str:
"""Resolve *filename* under *base_dir* and guard against path traversal.
Returns the resolved absolute path. Raises ``ValueError`` if the
resulting path escapes *base_dir*.
"""
root = os.path.realpath(base_dir)
dest = os.path.realpath(os.path.join(base_dir, filename))
# dest must be inside root (or equal to root itself)
if dest != root and not dest.startswith(root + os.sep):
raise ValueError(f"Path traversal detected: {filename!r}")
return dest
def write_file(filename: str, data: str):
"""
write data to file
"""
dir = os.path.dirname(filename)
if dir and not os.path.exists(dir):
os.makedirs(dir)
with open(filename, "w", encoding="utf-8") as file:
file.write(data)
def match_files(base_dir: str, pattern: str) -> list[str]:
"""
match files in dir with regex pattern
Parameters
----------
base_dir : str
directory to find in
pattern : str
regex pattern
Returns
-------
list[str]
matched filenames relative to base_dir
"""
if pattern == "":
pattern = ".*"
results = []
matched = re.compile(pattern)
for root, _, files in os.walk(base_dir):
for filename in files:
filename = os.path.relpath(os.path.join(root, filename), base_dir)
if matched.match(filename):
results.append(filename)
return results

View File

@@ -0,0 +1 @@
Hover = ''

View File

@@ -0,0 +1,22 @@
class RawLayeredImage:
pass
class RawAlways:
pass
class RawAttributeGroup:
pass
class RawAttribute:
pass
class RawConditionGroup:
pass
class RawCondition:
pass

View File

@@ -0,0 +1,8 @@
*.swp
*.pyc
*.rpyc
__pycache__
saves
cache
*_decompiled
log.txt

View File

@@ -0,0 +1,109 @@
"""Tests for renpy.ast node code generation"""
import os
import sys
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
import renpy.ast as ast
def make_pycode(code_str):
"""Create a PyCode object with the given source string."""
pycode = ast.PyCode()
pycode.state = (None, code_str, None, None, None)
return pycode
class TestPythonGetCode(unittest.TestCase):
"""Tests for Python.get_code() keyword order and correctness."""
def _make_node(self, code_str, store=None, hide=None):
node = ast.Python(loc=("script.rpyc", 1))
node.code = make_pycode(code_str)
if store is not None:
node.store = store
if hide is not None:
node.hide = hide
return node
def test_single_line_shorthand(self):
"""Single-line Python without store/hide uses $ shorthand."""
node = self._make_node("flag = True")
self.assertEqual(node.get_code(), "$ flag = True")
def test_hide_before_store(self):
"""'hide' must appear before 'in <store>' per Ren'Py grammar."""
node = self._make_node("flag = True", hide=True, store="store.mystore")
result = node.get_code()
self.assertIn("python hide in mystore:", result)
def test_hide_only(self):
"""python hide: block without store."""
node = self._make_node("flag = True", hide=True)
result = node.get_code()
self.assertTrue(result.startswith("python hide:"))
def test_store_only(self):
"""python in <store>: block without hide."""
node = self._make_node("flag = True", store="store.mystore")
result = node.get_code()
self.assertTrue(result.startswith("python in mystore:"))
def test_multiline(self):
"""Multiline code produces a python: block."""
node = self._make_node("flag = True\nother = False")
result = node.get_code()
self.assertIn("python:", result)
class TestEarlyPythonGetCode(unittest.TestCase):
"""Tests for EarlyPython.get_code() — keyword order and missing-attribute safety."""
def _make_node(self, code_str, store=None, hide=None):
node = ast.EarlyPython(loc=("script.rpyc", 1))
node.code = make_pycode(code_str)
if store is not None:
node.store = store
if hide is not None:
node.hide = hide
return node
def test_single_line_no_hide_attr(self):
"""EarlyPython without 'hide' attribute should not raise AttributeError."""
node = self._make_node("flag = True")
self.assertEqual(node.get_code(), "$ flag = True")
def test_single_line_hide_false(self):
"""EarlyPython with hide=False should use $ shorthand."""
node = self._make_node("flag = True", hide=False)
self.assertEqual(node.get_code(), "$ flag = True")
def test_hide_before_store(self):
"""'hide' must appear before 'in <store>' per Ren'Py grammar."""
node = self._make_node("flag = True", hide=True, store="store.mystore")
result = node.get_code()
self.assertIn("python early hide in mystore:", result)
def test_hide_only(self):
"""python early hide: block without store."""
node = self._make_node("flag = True", hide=True)
result = node.get_code()
self.assertTrue(result.startswith("python early hide:"))
def test_store_only(self):
"""python early in <store>: block without hide."""
node = self._make_node("flag = True", store="store.mystore")
result = node.get_code()
self.assertTrue(result.startswith("python early in mystore:"))
def test_multiline(self):
"""Multiline code should emit 'python early:' block."""
node = self._make_node("flag = True\nother = False")
result = node.get_code()
self.assertIn("python early:", result)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,187 @@
import os
import sys
import tempfile
import unittest
from pathlib import Path
from subprocess import run
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
from rpycdec.rpa import create_rpa, extract_rpa
SRC_DIR = str(Path(__file__).resolve().parents[1] / "src")
class RPATests(unittest.TestCase):
def create_filter_fixture(self, root: Path) -> Path:
source = root / "game"
(source / "images").mkdir(parents=True)
(source / "scripts").mkdir()
(source / "script.rpy").write_text('label start:\n "Hello"\n')
(source / "scripts" / "chapter.RPYC").write_bytes(b"compiled")
(source / "images" / "bg.png").write_bytes(b"image")
(source / "notes.txt").write_text("notes")
archive = root / "archive.rpa"
create_rpa(str(archive), [str(source)])
return archive
def test_create_rpa_round_trips_directory(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
source = root / "game"
(source / "images").mkdir(parents=True)
(source / "script.rpy").write_text('label start:\n "Hello"\n')
(source / "images" / "bg.png").write_bytes(b"\x89PNG\r\n\x1a\n")
archive = root / "archive.rpa"
create_rpa(str(archive), [str(source)])
output = root / "out"
with archive.open("rb") as f:
extract_rpa(f, str(output))
self.assertEqual(
(output / "script.rpy").read_text(), 'label start:\n "Hello"\n'
)
self.assertEqual(
(output / "images" / "bg.png").read_bytes(), b"\x89PNG\r\n\x1a\n"
)
def test_rpa_cli_creates_archive_from_file(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
source = root / "00unlock.rpy"
source.write_text("define unlock = True\n")
archive = root / "unlock.rpa"
env = os.environ.copy()
env["PYTHONPATH"] = SRC_DIR + os.pathsep + env.get("PYTHONPATH", "")
result = run(
[
sys.executable,
"-m",
"rpycdec",
"rpa",
"-o",
str(archive),
str(source),
],
text=True,
capture_output=True,
env=env,
)
self.assertEqual(result.returncode, 0, result.stderr)
output = root / "out"
with archive.open("rb") as f:
extract_rpa(f, str(output))
self.assertEqual(
(output / "00unlock.rpy").read_text(), "define unlock = True\n"
)
def test_extract_rpa_filters_by_multiple_suffixes_case_insensitively(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
archive = self.create_filter_fixture(root)
output = root / "out"
with archive.open("rb") as f:
extract_rpa(f, str(output), suffixes=[".rpy", ".rpyc"])
self.assertTrue((output / "script.rpy").is_file())
self.assertTrue((output / "scripts" / "chapter.RPYC").is_file())
self.assertFalse((output / "images" / "bg.png").exists())
self.assertFalse((output / "notes.txt").exists())
def test_extract_rpa_accepts_a_single_filter_string(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
archive = self.create_filter_fixture(root)
output = root / "out"
with archive.open("rb") as f:
extract_rpa(f, str(output), suffixes=".rpy")
self.assertTrue((output / "script.rpy").is_file())
self.assertFalse((output / "scripts" / "chapter.RPYC").exists())
def test_extract_rpa_combines_expressions_and_suffixes_with_or(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
archive = self.create_filter_fixture(root)
output = root / "out"
with archive.open("rb") as f:
extract_rpa(
f,
str(output),
expressions=[r"^images/.*\.png$"],
suffixes=[".rpy"],
)
self.assertTrue((output / "script.rpy").is_file())
self.assertTrue((output / "images" / "bg.png").is_file())
self.assertFalse((output / "scripts" / "chapter.RPYC").exists())
self.assertFalse((output / "notes.txt").exists())
def test_unrpa_cli_supports_expression_and_multiple_suffixes(self):
with tempfile.TemporaryDirectory() as td:
root = Path(td)
archive = self.create_filter_fixture(root)
output = root / "out"
env = os.environ.copy()
env["PYTHONPATH"] = SRC_DIR + os.pathsep + env.get("PYTHONPATH", "")
result = run(
[
sys.executable,
"-m",
"rpycdec",
"unrpa",
str(archive),
"-o",
str(output),
"-e",
r"^images/",
"-s",
".rpy",
".rpyc",
],
text=True,
capture_output=True,
env=env,
)
self.assertEqual(result.returncode, 0, result.stderr)
self.assertTrue((output / "script.rpy").is_file())
self.assertTrue((output / "scripts" / "chapter.RPYC").is_file())
self.assertTrue((output / "images" / "bg.png").is_file())
self.assertFalse((output / "notes.txt").exists())
def test_unrpa_cli_rejects_invalid_expression(self):
env = os.environ.copy()
env["PYTHONPATH"] = SRC_DIR + os.pathsep + env.get("PYTHONPATH", "")
result = run(
[
sys.executable,
"-m",
"rpycdec",
"unrpa",
"archive.rpa",
"-e",
"[",
],
text=True,
capture_output=True,
env=env,
)
self.assertEqual(result.returncode, 2)
self.assertIn("invalid regular expression", result.stderr)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,296 @@
"""Tests for rpycdec.translate module"""
import os
import sys
import tempfile
import unittest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
import renpy.ast
from rpycdec.translate import (
DialogueTranslation,
StringTranslation,
TranslationExtractor,
elide_filename,
get_translation_filename,
quote_unicode,
unique_identifier,
write_dialogue_translations,
write_string_translations,
)
class TestGetTranslationFilename(unittest.TestCase):
"""Tests for get_translation_filename()"""
def test_rpyc_to_rpy(self):
self.assertEqual(get_translation_filename("script.rpyc"), "script.rpy")
def test_rpymc_to_rpym(self):
self.assertEqual(get_translation_filename("script.rpymc"), "script.rpym")
def test_subdirectory_preserved(self):
"""Directory structure should be preserved (was a bug: basename stripped dirs)"""
self.assertEqual(get_translation_filename("sub/intro.rpyc"), "sub/intro.rpy")
def test_nested_subdirectory_preserved(self):
self.assertEqual(
get_translation_filename("a/b/c/script.rpyc"), "a/b/c/script.rpy"
)
def test_rpym_unchanged(self):
self.assertEqual(get_translation_filename("script.rpym"), "script.rpym")
def test_rpy_unchanged(self):
self.assertEqual(get_translation_filename("script.rpy"), "script.rpy")
class TestElideFilename(unittest.TestCase):
def test_strips_game_dir(self):
result = elide_filename("/game/script.rpyc", "/game")
self.assertEqual(result, "script.rpy")
def test_rpymc_to_rpym(self):
result = elide_filename("/game/script.rpymc", "/game")
self.assertEqual(result, "script.rpym")
def test_no_game_dir(self):
result = elide_filename("script.rpyc")
self.assertEqual(result, "script.rpy")
class TestQuoteUnicode(unittest.TestCase):
def test_backslash(self):
self.assertEqual(quote_unicode("a\\b"), "a\\\\b")
def test_double_quote(self):
self.assertEqual(quote_unicode('say "hi"'), 'say \\"hi\\"')
def test_newline(self):
self.assertEqual(quote_unicode("line1\nline2"), "line1\\nline2")
def test_plain_string(self):
self.assertEqual(quote_unicode("hello"), "hello")
class TestUniqueIdentifier(unittest.TestCase):
def test_no_label(self):
result = unique_identifier(None, "abcd1234")
self.assertEqual(result, "abcd1234")
def test_with_label(self):
result = unique_identifier("start", "abcd1234")
self.assertEqual(result, "start_abcd1234")
def test_label_dot_replaced(self):
result = unique_identifier("chapter.one", "abcd1234")
self.assertEqual(result, "chapter_one_abcd1234")
def test_collision_appends_index(self):
existing = {"start_abcd1234"}
result = unique_identifier("start", "abcd1234", existing)
self.assertEqual(result, "start_abcd1234_1")
def test_multiple_collisions(self):
existing = {"start_abcd1234", "start_abcd1234_1"}
result = unique_identifier("start", "abcd1234", existing)
self.assertEqual(result, "start_abcd1234_2")
class TestTranslationExtractor(unittest.TestCase):
def _make_say_node(self, who, what, filename="script.rpyc", linenumber=1):
node = renpy.ast.Say(loc=(filename, linenumber))
node.who = who
node.what = what
node.interact = True
node.identifier = None
node.explicit_identifier = False
node.attributes = None
node.temporary_attributes = None
node.arguments = None
node.with_ = None
return node
def test_extract_say_dialogue(self):
extractor = TranslationExtractor()
node = self._make_say_node("e", "Hello world.")
extractor.extract_file("script.rpyc", [node])
self.assertEqual(len(extractor.dialogues), 1)
d = extractor.dialogues[0]
self.assertEqual(d.who, "e")
self.assertEqual(d.what, "Hello world.")
self.assertEqual(d.filename, "script.rpyc")
def test_extract_say_no_speaker(self):
extractor = TranslationExtractor()
node = self._make_say_node(None, "A narrator line.")
extractor.extract_file("script.rpyc", [node])
self.assertEqual(len(extractor.dialogues), 1)
self.assertIsNone(extractor.dialogues[0].who)
def test_no_say_no_dialogue(self):
extractor = TranslationExtractor()
extractor.extract_file("script.rpyc", [])
self.assertEqual(len(extractor.dialogues), 0)
self.assertEqual(len(extractor.strings), 0)
def test_identifier_generated(self):
extractor = TranslationExtractor()
node = self._make_say_node("e", "Hello.")
extractor.extract_file("script.rpyc", [node])
self.assertIsNotNone(extractor.dialogues[0].identifier)
self.assertIsInstance(extractor.dialogues[0].identifier, str)
class TestWriteDialogueTranslations(unittest.TestCase):
def test_writes_dialogue_file(self):
dialogues = [
DialogueTranslation(
identifier="start_abcd1234",
filename="script.rpyc",
linenumber=5,
who="e",
what="Hello.",
code='e "Hello."',
)
]
with tempfile.TemporaryDirectory() as tmpdir:
write_dialogue_translations(
dialogues, tmpdir, "chinese", empty_translation=False
)
tl_file = os.path.join(tmpdir, "script.rpy")
self.assertTrue(os.path.exists(tl_file))
with open(tl_file) as f:
content = f.read()
self.assertIn("translate chinese start_abcd1234:", content)
self.assertIn('e "Hello."', content)
def test_preserves_subdirectory_structure(self):
"""Translation files should mirror the source directory structure"""
dialogues = [
DialogueTranslation(
identifier="intro_abcd1234",
filename="sub/intro.rpyc",
linenumber=3,
who=None,
what="Welcome.",
code='"Welcome."',
)
]
with tempfile.TemporaryDirectory() as tmpdir:
write_dialogue_translations(
dialogues, tmpdir, "chinese", empty_translation=False
)
tl_file = os.path.join(tmpdir, "sub", "intro.rpy")
self.assertTrue(
os.path.exists(tl_file),
f"Expected {tl_file} to exist (subdirectory structure not preserved)",
)
def test_empty_translation(self):
dialogues = [
DialogueTranslation(
identifier="start_abcd1234",
filename="script.rpyc",
linenumber=1,
who="e",
what="Hello.",
code='e "Hello."',
)
]
with tempfile.TemporaryDirectory() as tmpdir:
write_dialogue_translations(
dialogues, tmpdir, "chinese", empty_translation=True
)
with open(os.path.join(tmpdir, "script.rpy")) as f:
content = f.read()
self.assertIn('e ""', content)
class TestWriteStringTranslations(unittest.TestCase):
def test_writes_strings_file(self):
strings = [StringTranslation(filename="script.rpyc", linenumber=2, text="Play")]
with tempfile.TemporaryDirectory() as tmpdir:
count = write_string_translations(
strings, tmpdir, "chinese", empty_translation=False
)
self.assertEqual(count, 1)
tl_file = os.path.join(tmpdir, "strings.rpy")
self.assertTrue(os.path.exists(tl_file))
with open(tl_file) as f:
content = f.read()
self.assertIn("translate chinese strings:", content)
self.assertIn('old "Play"', content)
self.assertIn('new "Play"', content)
def test_empty_strings_not_written(self):
with tempfile.TemporaryDirectory() as tmpdir:
count = write_string_translations([], tmpdir, "chinese")
self.assertEqual(count, 0)
self.assertFalse(os.path.exists(os.path.join(tmpdir, "strings.rpy")))
class TestCLIExtractTranslate(unittest.TestCase):
"""Integration tests for the extract-translate CLI command"""
def test_language_argument_recognized(self):
"""The --language argument must be recognized by the CLI parser"""
from rpycdec.cli import main
with tempfile.TemporaryDirectory() as tmpdir:
sys.argv = [
"rpycdec",
"extract-translate",
tmpdir,
"--language",
"chinese",
]
# Should not raise SystemExit with non-zero code (unrecognized arguments)
try:
main()
except SystemExit as e:
if e.code != 0:
self.fail(
f"CLI raised SystemExit({e.code}) - --language may not be recognized"
)
def test_language_short_flag(self):
"""The -l short flag must also work"""
from rpycdec.cli import main
with tempfile.TemporaryDirectory() as tmpdir:
sys.argv = ["rpycdec", "extract-translate", tmpdir, "-l", "japanese"]
try:
main()
except SystemExit as e:
if e.code != 0:
self.fail(
f"CLI raised SystemExit({e.code}) - -l flag may not be recognized"
)
def test_creates_tl_directory(self):
"""extract-translate must create tl/<language>/ directory in output"""
from rpycdec.cli import main
with tempfile.TemporaryDirectory() as game_dir:
with tempfile.TemporaryDirectory() as out_dir:
sys.argv = [
"rpycdec",
"extract-translate",
game_dir,
"--language",
"chinese",
"--output",
out_dir,
]
main()
tl_dir = os.path.join(out_dir, "tl", "chinese")
self.assertTrue(
os.path.isdir(tl_dir),
f"Expected tl/chinese/ directory at {tl_dir}",
)
if __name__ == "__main__":
unittest.main()

5
third_party/python/rpycdec_bin.py vendored Normal file
View File

@@ -0,0 +1,5 @@
from rpycdec.cli import main
if __name__ == "__main__":
main()