Skip to content

Fix Windows 0-byte Tailwind binary downloads & integrity checks#121

Open
Taminoful wants to merge 4 commits intoSymfonyCasts:mainfrom
Taminoful:main
Open

Fix Windows 0-byte Tailwind binary downloads & integrity checks#121
Taminoful wants to merge 4 commits intoSymfonyCasts:mainfrom
Taminoful:main

Conversation

@Taminoful
Copy link
Copy Markdown

@Taminoful Taminoful commented Apr 7, 2026

This PR fixes an issue on Windows where TailwindCSS binaries could be downloaded as empty (0-byte) executables due to antivirus interference, leading to SmartScreen errors and failed command execution. A detailed description of the issues occuring can be found in #115.

In some environments (notably with Windows Defender or third-party antivirus), the downloaded executable may be blocked or quarantined during download. This results in a 0-byte file being saved, which the bundle incorrectly treats as a valid binary. Subsequent execution attempts then fail with Windows SmartScreen / access denied errors.

What’s Fixed

  • Detect and remove 0-byte binaries before use, ensuring corrupted downloads don’t persist.
  • Validate downloaded binaries using SHA256 hashes (when available).
  • If a file fails validation, it is deleted and a clear error is thrown so the next run can retry a clean download.
  • Improved error handling and messaging when downloads fail or assets are unavailable.
  • Added test coverage for corrupted (0-byte) downloads and hash mismatches.

Improvements

  • The download process now uses the GitHub API to fetch release metadata before downloading the binary.
  • This enables retrieving file hashes and asset URLs reliably.
  • While it introduces an additional request, it significantly improves robustness and future extensibility.
  • Path handling has been hardened using canonicalization to avoid filesystem inconsistencies.
  • The architecture now allows future removal of hardcoded platform binary mappings by relying on GitHub-provided release data.

Impact

  • Prevents silent failures caused by antivirus software on Windows.
  • Ensures corrupted binaries are automatically detected and recovered from.
  • Provides clearer feedback to users when something goes wrong during installation or build steps.
  • Improves long-term maintainability of the binary download mechanism.

Removes the possibility that the .idea folder or leftover test files (binaries) get commited to the project.
Adds some model classes that represent the important parts of the GitHub tag endpoint data that makes it easier to rework the download process. Also allows for easy extension if more fields should become relevant in the future.

`TailwindBinaries` represents a release of a version which holds the downloadable assets. The class also contains a helper function as part of it's model too which allows to search for assets of a release by their tag name.

`TailwindBinary` represents the details of each executable that is pushed to GitHub as part of a release. Important to note is, that the digest field only gets filled after Tailwind v4.1.9 but gets filled after, since, realistically people will use v4 from now on more than v3, I decided to not make the field nullable or go the extra route of comparing against the contents of the `sha256sums.txt`. This approach should keep the code more clean going forward as each binary has their digest attached directly as a field.
This commit changes the download method to use the GitHub API endpoint instead. It's requesting the information of the API about the tag and temporarily saves it in the `Model` classes for further use. From there the actual file gets downloaded over the API provided link. The commit also starts using the `Path::canonicalize()` method to eliminate any potential pathing issues.

This does have the downside of sending two requests instead of one but allows for a more robust download process. E.g., it's now possible to check the file integrity with the provided SHA256 hash to determine if the file got corrupted as described in SymfonyCasts#115. In the future this change also allows for removing any hardcoded lists within the code that contain the platform executable names, as it's possible to just get the list off GitHub, which helps in maintaining if TailwindLabs decides to build for other platforms or removes platforms from their builds in the future. The main functionality for this lives in `requestBinariesByVersion()` which I might move to the `TailwindBinary` Model during cleanup, depending on where it feels right.
…asts#115)

- Detect and delete 0-byte files before re-downloading, fixing the core
  Windows antivirus interference bug where a corrupt file blocked recovery
- Validate SHA256 digest after download; delete file and throw a clear
  RuntimeException on mismatch so the next run triggers a clean retry
- Skip integrity check for versions <=4.1.9 where no digest is available
- Replace dd() debug call with a RuntimeException listing available assets
- Fix double "Expected file hash" label (second was the actual hash)
- Fix awkward TailwindBinaries construction (build assets array first,
  pass to constructor directly, removing the setAssets() workaround)
- Simplify model classes: make properties readonly, remove unused setters,
  rename getFileSize() to getSize(), add return type to getAssetByBinaryName()
- Remove dead downloadExecutableOld() method
- Add tailwindcss-linux-armv7 to mock fixture so armv7 test case resolves
- Add tests for 0-byte re-download and integrity failure scenarios
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