Skip to content

fix(ci): preserve exec bit on fbuild console script in wheel#135

Merged
zackees merged 1 commit intomainfrom
fix/wheel-exec-bit
Apr 19, 2026
Merged

fix(ci): preserve exec bit on fbuild console script in wheel#135
zackees merged 1 commit intomainfrom
fix/wheel-exec-bit

Conversation

@zackees
Copy link
Copy Markdown
Member

@zackees zackees commented Apr 19, 2026

Summary

Fixes the packaging half of #129. After pip install fbuild==2.1.18, the installed console script at <env>/bin/fbuild had the execute bit cleared:

$ fbuild --version
bash: /.../bin/fbuild: Permission denied

The only workaround was chmod +x $(which fbuild).

Root cause

ci/publish.py::add_file builds the wheel via zipfile.ZipInfo and sets info.external_attr = exec_perms << 16 but leaves info.create_system at the default value of 0 (DOS/Windows). In the ZIP format, the upper 16 bits of external_attr are interpreted as Unix permissions only when create_system == 3 (Unix); otherwise they encode DOS file-attribute flags and every unpacker — pip, installer, unzip — ignores the mode and installs the file without +x.

That's why the fbuild binary shipped through PyPI had no execute bit on Linux/macOS.

Fix

Set info.create_system = 3 alongside the existing external_attr assignment in the executable=True branch of add_file. A round-trip sanity check (zipfile → re-open → inspect) confirms the mode survives as 0o755.

Test plan

  • Local round-trip check:
    python -c \"import zipfile,io; ...\"  # create_system=3, mode=0o755 → confirmed
    
  • Next published wheel (2.1.19+) installs with +x on Linux/macOS — verified by end-to-end pip install fbuild && fbuild --version after release.

🤖 Generated with Claude Code

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 19, 2026

Warning

Rate limit exceeded

@zackees has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 20 minutes and 18 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 20 minutes and 18 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7338ece8-9255-4ec4-b554-d5a1fa4e892d

📥 Commits

Reviewing files that changed from the base of the PR and between 5a1a346 and c7fd824.

📒 Files selected for processing (1)
  • ci/publish.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/wheel-exec-bit

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

`pip install fbuild==2.1.18` produced a console script at
`<env>/bin/fbuild` with the execute bit cleared, so every invocation
of `fbuild --version` (or any other subcommand) failed with
`Permission denied`. The only workaround was
`chmod +x $(which fbuild)`.

Root cause: `ci/publish.py::add_file` set
`info.external_attr = exec_perms << 16` but left
`info.create_system` at the `ZipInfo` default of `0` (DOS/Windows).
In ZIP archives, the upper 16 bits of `external_attr` only encode
Unix permissions when `create_system == 3`; otherwise they encode
DOS file-attribute flags and every unpacker (pip, installer, unzip)
ignores the permission bits. Result: the packaged fbuild binary
never got `+x` on the installer's filesystem.

Fix: set `info.create_system = 3` alongside the existing
`external_attr` assignment in the `executable=True` branch. A
round-trip sanity check (`zipfile → re-open → inspect`) confirms
the mode survives as `0o755`.

Refs #129.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@zackees zackees force-pushed the fix/wheel-exec-bit branch from 8ddfcd9 to c7fd824 Compare April 19, 2026 05:43
@zackees zackees merged commit b38c003 into main Apr 19, 2026
7 of 76 checks passed
zackees added a commit that referenced this pull request Apr 19, 2026
Cuts a release containing the two P0 fixes landed since 2.1.19:

- #134 "P0 regression — Operation not permitted (os error 1) on warm build"
- #135 "preserve exec bit on fbuild console script in wheel"

Both are currently blocking every FastLED uno build on GitHub Actions:
the wheel's console script installs without +x, so CI can't even run
`fbuild --version`, and the subsequent compile fails with
`Operation not permitted (os error 1)` on every example.

Also includes:

- #131 rustfmt on lnk pipeline
- #133 DiskCache leases.refcount schema migration
- #128 AVR orchestrator fingerprint fast-path + telemetry (#127)
- #126 FBUILD_WATCH_SET_CACHE_SECS env override
- f8533d3 extend watch-set fingerprint fast-path to AVR orchestrator

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zackees added a commit that referenced this pull request Apr 19, 2026
Cuts a release containing the two P0 fixes landed since 2.1.19:

- #134 "P0 regression — Operation not permitted (os error 1) on warm build"
- #135 "preserve exec bit on fbuild console script in wheel"

Both are currently blocking every FastLED uno build on GitHub Actions:
the wheel's console script installs without +x, so CI can't even run
`fbuild --version`, and the subsequent compile fails with
`Operation not permitted (os error 1)` on every example.

Also includes:

- #131 rustfmt on lnk pipeline
- #133 DiskCache leases.refcount schema migration
- #128 AVR orchestrator fingerprint fast-path + telemetry (#127)
- #126 FBUILD_WATCH_SET_CACHE_SECS env override
- f8533d3 extend watch-set fingerprint fast-path to AVR orchestrator

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zackees added a commit that referenced this pull request Apr 19, 2026
The 2.1.16 pin and the chmod/re-pin workarounds were added in iter1b
to dodge #129 — the broken exec bit on the `fbuild`
console script in the 2.1.18 wheel. That bug was fixed in #135 and
released in 2.1.20 (verified: `pip install fbuild==2.1.20` on
Windows now yields a working `fbuild --version`).

This iteration removes:
- the `fbuild==2.1.16` pin in the root pip install
- the defensive `chmod +x` on the root venv's `fbuild` script
- the `uv pip install --python .venv/bin/python "fbuild==2.1.16"`
  re-pin after `./install` / `uv sync`
- the defensive `chmod +x` on the FastLED venv's `fbuild` script

Also tightens the smoke check: `fbuild --version` is now `set -e`
fatal instead of `|| true`, since the whole point of this iter is
that the console script is back to working.

CACHE_BUST bumped to force a cold run — first cold measurement on
fbuild 2.1.20 and confirmation in CI (Linux) that the 2.1.20 wheel
holds up end-to-end through toolchain materialization + full
examples compile. Prior iter1b numbers were on 2.1.16; new baseline
to follow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zackees added a commit that referenced this pull request Apr 19, 2026
2.1.20 shipped with mode=0o755 on the `fbuild` / `fbuild-daemon`
console scripts but with external_attr=0x01ed0000 — i.e. the mode
bits were set correctly but the file-type bit (S_IFREG) was zero.

pip's wheel installer calls stat.S_ISREG() on the upper 16 bits of
external_attr before deciding whether to apply the script's mode;
without the IFREG bit that test returns False, pip falls back to
umask defaults (0o644), and the binary lands on disk without +x:

    /opt/hostedtoolcache/Python/3.12.13/x64/bin/fbuild:
        Permission denied  (exit code 126)

Windows doesn't care about exec bits on .exe files, which is why
2.1.20 looked fine on `uv tool install fbuild==2.1.20` on Windows
but was broken for every Linux/macOS user. It's also the root
cause of #129#135's "preserve exec bit" fix set
the mode but not the file-type bit.

Reference: uv / ruff / maturin-built wheels all have
external_attr=0x81ed0000 (S_IFREG | 0o755) on their script entries.

Verified locally by rebuilding the Linux x86_64 wheel against the
existing binary artifact: new external_attr=0x81ed0000, IFREG=True.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zackees added a commit that referenced this pull request Apr 19, 2026
2.1.20 shipped with mode=0o755 on the `fbuild` / `fbuild-daemon`
console scripts but with external_attr=0x01ed0000 — i.e. the mode
bits were set correctly but the file-type bit (S_IFREG) was zero.

pip's wheel installer calls stat.S_ISREG() on the upper 16 bits of
external_attr before deciding whether to apply the script's mode;
without the IFREG bit that test returns False, pip falls back to
umask defaults (0o644), and the binary lands on disk without +x:

    /opt/hostedtoolcache/Python/3.12.13/x64/bin/fbuild:
        Permission denied  (exit code 126)

Windows doesn't care about exec bits on .exe files, which is why
2.1.20 looked fine on `uv tool install fbuild==2.1.20` on Windows
but was broken for every Linux/macOS user. It's also the root
cause of #129#135's "preserve exec bit" fix set
the mode but not the file-type bit.

Reference: uv / ruff / maturin-built wheels all have
external_attr=0x81ed0000 (S_IFREG | 0o755) on their script entries.

Verified locally by rebuilding the Linux x86_64 wheel against the
existing binary artifact: new external_attr=0x81ed0000, IFREG=True.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zackees added a commit that referenced this pull request Apr 19, 2026
2.1.20 shipped with mode=0o755 on the `fbuild` / `fbuild-daemon`
console scripts but with external_attr=0x01ed0000 — i.e. the mode
bits were set correctly but the file-type bit (S_IFREG) was zero.

pip's wheel installer calls stat.S_ISREG() on the upper 16 bits of
external_attr before deciding whether to apply the script's mode;
without the IFREG bit that test returns False, pip falls back to
umask defaults (0o644), and the binary lands on disk without +x:

    /opt/hostedtoolcache/Python/3.12.13/x64/bin/fbuild:
        Permission denied  (exit code 126)

Windows doesn't care about exec bits on .exe files, which is why
2.1.20 looked fine on `uv tool install fbuild==2.1.20` on Windows
but was broken for every Linux/macOS user. It's also the root
cause of #129#135's "preserve exec bit" fix set
the mode but not the file-type bit.

Reference: uv / ruff / maturin-built wheels all have
external_attr=0x81ed0000 (S_IFREG | 0o755) on their script entries.

Verified locally by rebuilding the Linux x86_64 wheel against the
existing binary artifact: new external_attr=0x81ed0000, IFREG=True.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant