From 4503b7eee4c4f54bbac978c1af985538fd68690f Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 20 Aug 2026 09:43:23 -0400 Subject: [PATCH] Make requirements.lock universal, so Windows can install it at all MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Windows entry of the CI build job has never been able to install from this lockfile, and no arrangement of pip-compile can fix that. The lockfile has to serve Linux, macOS and Windows across Python 3.10-3.12. pip-compile resolves for the one environment it runs on and emits no markers, and two dependencies are environment-conditional: aiohttp needs async-timeout only below 3.11, and portalocker needs pywin32 only on Windows. So every possible pip-compile output is wrong somewhere. A Linux/3.11 lockfile killed Windows with "must have their versions pinned" for pywin32; a Windows/3.10 one could never match the diff on the other platforms. Both failure modes turned up in this session, one after the other, as each fix exposed the next. `uv pip compile --universal` resolves across all target environments and emits the markers — `pywin32 ; sys_platform == 'win32'`, `async-timeout ; python_full_version < '3.11'` — pinned and hashed, so one file covers everything. This changes the lock GENERATOR from pip-tools to uv, which is a real tooling decision and worth saying plainly. What it does not change is the consumer: the output is an ordinary pip requirements file, and every build job still installs it with plain `pip install --require-hashes -r requirements.lock`. Nothing depends on uv at install time, so reverting means regenerating the file, not unpicking anything. uv is pinned to 0.10.4 so the regenerate-and- diff stays deterministic. Verified locally: regenerating produces a byte-identical file, and it installs under --require-hashes on both 3.10 and 3.11. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN --- .github/workflows/python-ci.yml | 42 ++++++++++--------- requirements.lock | 71 ++++++++++++++++++++++++++------- 2 files changed, 81 insertions(+), 32 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 23473d00..1ad15a9b 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -10,36 +10,42 @@ on: jobs: # Regenerating the lockfile and diffing it proves the committed file really - # came from src/requirements.txt. It has to happen on ONE interpreter, and - # specifically on LOCK_PYTHON below, so it cannot ride the build matrix. + # came from src/requirements.txt. That is one question about one artifact, + # so it is asked once here rather than once per matrix entry. # - # pip-compile resolves version-conditional dependencies for the interpreter - # it runs on, and the result carries no markers. Python 3.10 needs - # async-timeout (aiohttp requires it below 3.11) and 3.11 does not, so the - # two resolutions are genuinely different files -- 135 lines apart, - # argon2-cffi-bindings among them. One committed lockfile cannot equal both. + # WHY uv AND NOT pip-compile # - # Compiling on the LOWEST supported version is what makes a single lockfile - # work everywhere: 3.10's resolution is a superset, and it installs cleanly - # under --require-hashes on 3.11. The reverse does not -- a 3.11 lockfile - # is missing async-timeout, so on 3.10 pip tries to fetch it unpinned and - # --require-hashes refuses. Both halves of that were failing here: the diff - # could never match on one platform, and the install could never succeed on - # the other. + # This lockfile has to serve Linux, macOS and Windows across Python + # 3.10-3.12, and pip-compile resolves for the single environment it runs on + # and emits no markers. Two dependencies make that impossible: aiohttp + # needs async-timeout only below 3.11, and portalocker needs pywin32 only + # on Windows. So every pip-compile output was wrong somewhere -- a + # Linux/3.11 lockfile made the Windows job fail with "must have their + # versions pinned" for pywin32, and a Windows/3.10 one could never match + # the diff elsewhere. The Windows entry of this job had never been able to + # install from this file. + # + # `uv pip compile --universal` resolves across all of them and emits the + # markers, so ONE lockfile covers every target. The output is still an + # ordinary pip requirements file: nothing downstream changes, the build + # jobs still install it with plain `pip install --require-hashes`. Only the + # generator is different, and it is pinned so this diff stays deterministic. lockfile: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: - # Must equal the minimum in pyproject's `python = ">=3.10,<3.13"`. - # Raising that floor means regenerating requirements.lock here. python-version: "3.10" - name: Verify the lockfile matches src/requirements.txt run: | python -m pip install --upgrade pip - pip install pip-tools - pip-compile --generate-hashes --output-file=requirements.lock src/requirements.txt + pip install uv==0.10.4 + # --python-version must equal the minimum in pyproject's + # `python = ">=3.10,<3.13"`. Raising that floor means regenerating + # requirements.lock with the new value. + uv pip compile --universal --generate-hashes --python-version 3.10 \ + -o requirements.lock src/requirements.txt git diff --exit-code requirements.lock secret-scan: diff --git a/requirements.lock b/requirements.lock index 6a370055..48613e99 100644 --- a/requirements.lock +++ b/requirements.lock @@ -1,9 +1,5 @@ -# -# This file is autogenerated by pip-compile with Python 3.10 -# by the following command: -# -# pip-compile --generate-hashes --output-file=requirements.lock src/requirements.txt -# +# This file was autogenerated by uv via the following command: +# uv pip compile --universal --generate-hashes --python-version 3.10 -o requirements.lock src/requirements.txt aiohappyeyeballs==2.7.1 \ --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 @@ -181,7 +177,7 @@ argon2-cffi-bindings==25.1.0 \ --hash=sha256:da0c79c23a63723aa5d782250fbf51b768abca630285262fb5144ba5ae01e520 \ --hash=sha256:e2fd3bfbff3c5d74fef31a722f729bf93500910db650c925c2d6ef879a7e51cb # via argon2-cffi -async-timeout==5.0.1 \ +async-timeout==5.0.1 ; python_full_version < '3.11' \ --hash=sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c \ --hash=sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3 # via aiohttp @@ -754,8 +750,13 @@ coincurve==21.0.0 \ colorama==0.4.6 \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via -r src/requirements.txt -coverage[toml]==7.15.4 \ + # via + # -r src/requirements.txt + # click + # pytest + # qrcode + # typer +coverage==7.15.4 \ --hash=sha256:002a438859f7b430bc99afeaf01a6d187dad1d0dc907b64cdeffc632a5db8fd8 \ --hash=sha256:050467a7983b8e2fe7dd41a78bb30c3e7f8c0b8cafda14b1c46f8b5e3cf2dd3c \ --hash=sha256:050f66a08805acb5b8a23c6d4a517b1ecf82c08e81ed0e4bd727df065e5c6624 \ @@ -1014,7 +1015,7 @@ ed25519-blake2b-fork==1.4.2 \ --hash=sha256:f3cc0523703bd1e5ccb4ca332d75b1011e74ad5b4c89c0cc7098a2d8a9588979 \ --hash=sha256:fc13ddb5968e3b16fd6ed4fd6433b005a6e8c435bdc7f9214f1032f51a62ca17 # via bip-utils -exceptiongroup==1.3.1 \ +exceptiongroup==1.3.1 ; python_full_version < '3.11' \ --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 # via @@ -1350,7 +1351,7 @@ linkify-it-py==2.1.0 \ --hash=sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e \ --hash=sha256:43360231720999c10e9328dc3691160e27a718e280673d444c38d7d3aaa3b98b # via markdown-it-py -markdown-it-py[linkify]==4.2.0 \ +markdown-it-py==4.2.0 \ --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via @@ -1948,7 +1949,7 @@ pyasn1==0.6.4 \ # via # -r src/requirements.txt # pgpy -pycparser==3.0 \ +pycparser==3.0 ; implementation_name != 'PyPy' \ --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi @@ -2243,7 +2244,30 @@ pytoniq-core-fork==0.1.48 \ --hash=sha256:248b0d0b2deffd6935599208ae587dc703fccf2756d735420e95804b5738177f \ --hash=sha256:eeb22d5f3d9fb15daccc48df5b2faf53e45426503c51bf665b56138a6955ae4a # via bip-utils -pyyaml==6.0.3 \ +pywin32==312 ; sys_platform == 'win32' \ + --hash=sha256:02ebca0f0242b75292e218065004310d6a477407c09fa449bfe4f6022bc0c0fc \ + --hash=sha256:17948aeadbdb091f0ced6ef0841620794e68327b94ee415571c1203594b7215c \ + --hash=sha256:3020656e34f1cf7faeb7bccd2b84653a607c6ff0c55ada85e6487d61716deabd \ + --hash=sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831 \ + --hash=sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed \ + --hash=sha256:5dbc35d2b5320dc07f25fa31269cfb767471002b17de5eb067d03da68c7cb2db \ + --hash=sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950 \ + --hash=sha256:772235332b5d1024c696f11cea1ae4be7930f0a8b894bb43db14e3f435f1ff7e \ + --hash=sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c \ + --hash=sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa \ + --hash=sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e \ + --hash=sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b \ + --hash=sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9 \ + --hash=sha256:b2200a054ca6d6625c4842fc56a4976a4b47f96b73dbe5538c3f813a80359f47 \ + --hash=sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc \ + --hash=sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5 \ + --hash=sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9 \ + --hash=sha256:d11417d84412f859b722fad0841b3614459ed0047f7542d8362e77884f6b6e8a \ + --hash=sha256:d620900033cc7531e50727c3c8333091df5dd3ffe6d68cdca38c03f5821408d5 \ + --hash=sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b \ + --hash=sha256:dc90147579a905b8635e1b0ec6514967dcb07e6e0d9c42f1477feef14cac23bb + # via portalocker +pyyaml==6.0.3 ; python_full_version != '3.13.*' \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \ --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ @@ -2318,6 +2342,25 @@ pyyaml==6.0.3 \ --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via libcst +pyyaml-ft==8.0.0 ; python_full_version == '3.13.*' \ + --hash=sha256:052561b89d5b2a8e1289f326d060e794c21fa068aa11255fe71d65baf18a632e \ + --hash=sha256:06237267dbcab70d4c0e9436d8f719f04a51123f0ca2694c00dd4b68c338e40b \ + --hash=sha256:0c947dce03954c7b5d38869ed4878b2e6ff1d44b08a0d84dc83fdad205ae39ab \ + --hash=sha256:30c5f1751625786c19de751e3130fc345ebcba6a86f6bddd6e1285342f4bbb69 \ + --hash=sha256:3bb4b927929b0cb162fb1605392a321e3333e48ce616cdcfa04a839271373255 \ + --hash=sha256:3fa992481155ddda2e303fcc74c79c05eddcdbc907b888d3d9ce3ff3e2adcfb0 \ + --hash=sha256:58e1015098cf8d8aec82f360789c16283b88ca670fe4275ef6c48c5e30b22a96 \ + --hash=sha256:7d10175a746be65f6feb86224df5d6bc5c049ebf52b89a88cf1cd78af5a367a8 \ + --hash=sha256:8a7f332bc565817644cdb38ffe4739e44c3e18c55793f75dddb87630f03fc254 \ + --hash=sha256:8c1306282bc958bfda31237f900eb52c9bedf9b93a11f82e1aab004c9a5657a6 \ + --hash=sha256:8c56bb46b4fda34cbb92a9446a841da3982cdde6ea13de3fbd80db7eeeab8b49 \ + --hash=sha256:8d445bf6ea16bb93c37b42fdacfb2f94c8e92a79ba9e12768c96ecde867046d1 \ + --hash=sha256:bd48d639cab5ca50ad957b6dd632c7dd3ac02a1abe0e8196a3c24a52f5db3f7a \ + --hash=sha256:cec6c92b4207004b62dfad1f0be321c9f04725e0f271c16247d8b39c3bf3ea42 \ + --hash=sha256:dab0abb46eb1780da486f022dce034b952c8ae40753627b27a626d803926483b \ + --hash=sha256:de04cfe9439565e32f178106c51dd6ca61afaa2907d143835d501d84703d3793 \ + --hash=sha256:e64fa5f3e2ceb790d50602b2fd4ec37abbd760a8c778e46354df647e7c5a4ebb + # via libcst qrcode==8.2 \ --hash=sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f \ --hash=sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c @@ -2475,7 +2518,7 @@ toga-dummy==0.5.6 \ --hash=sha256:5f34d8620bfcc6569aa9f3abbfebe9b5fc3d5d6b410e63fd91290ef88b5bac76 \ --hash=sha256:7d6f43c5cca7300767850ba57d9e808ead58763012ffeb753ba5d05beefe2ef9 # via -r src/requirements.txt -toml==0.10.2 \ +toml==0.10.2 ; python_full_version < '3.11' \ --hash=sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b \ --hash=sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f # via mutmut