Reinforcements for PHP maintenance needed

We recently discovered a major problem in buildComposerProject2, the currently recommended builder for composer-backed PHP packages: buildComposerProject2: all FODs broken · Issue #451395 · NixOS/nixpkgs · GitHub

The problem is essentially that composer places a bit of extra code into the vendor directory and that may change over multiple composer versions. The reason is that PHP’s core doesn’t have a proper autoloading infrastructure and that’s basically what composer injects here.

Now, the problem is that this snippet may change at any time, meaning that unsuspicious patch updates may actually break FOD hashes. The composer devs confirmed that it was luck that it worked for so long: Fixes binary proxy to support spaces in source path by mediabounds · Pull Request #12524 · composer/composer · GitHub

For now, I did a band-aid fix by updating all affected hashes and ensuring with diffoscope that no further changes sneaked in.

The big question however is, how do we proceed?

Personally, I don’t have a strong opinion on this.

And to be very blunt: I absolutely don’t see myself doing this. PHP became less and less relevant for my work over the past few years and while I’m happy to keep the interpreters up-to-date, I don’t have intrinsic motivation this kind of work.

So, we’ll essentially need somebody who’d be interested in helping out with this. Feel free to reach out here, in the issue I linked above or on Matrix.

6 Likes

Composer 2.9.0 will indeed require us to update all the FODs (meaning: update the vendorHash of all PHP packages), see the comment that was made by its author at Trim trailing spaces in autoload files by vrana · Pull Request #12459 · composer/composer · GitHub

1 Like

Could we remove the autoload from the FOD and get Composer to regenerate it during the build of a package?

2 Likes

Yes, it is possible to run composer install --no-autoload and then later do composer dump-autoload. Going to try that! Thanks for the suggestion.

Edit: I got it working ! PR @ build-support/php: improve PHP builder stability by drupol · Pull Request #459254 · NixOS/nixpkgs · GitHub

3 Likes

I don’t want to be a killjoy here but…

This one will most likely also cause issues in 2.9 because of changes to the binary proxy files in vendor/bin/… Fix suggestions from PhpStorm Inspector by weshooper · Pull Request #12499 · composer/composer · GitHub

And generally speaking, changes to vendor/composer/installed.json or InstalledVersions.php are not to be excluded, or ClassLoader.php also will likely see changes eventually if PHP introduces function autoloading or built-in classmaps.

My point is… all vendor/composer/*.* files are unstable across Composer versions, and there is nothing we can do about it. The software has to evolve and with it these files have to change.

I am not sure what you can do about it except pin Composer to a certain version and once in a while upgrade all hashes when bumping the pinned version.

3 Likes

Hello Seldaek,

Thank you for chiming in!

I wanted to explain a use case we have in Nix/Nixpkgs and ask whether Composer could support an option to better handle reproducible builds: specifically around the generation of files under vendor/bin.

In Nix, we use something called a Fixed Output Derivation (FOD) (What are Fixed Output Derivations and why use them?) to ensure deterministic, reproducible builds.
In simple terms, a FOD is a build step whose output must be bit-identical for a given set of inputs. Nix verifies this by hashing the resulting artefacts (in this case, the vendor directory). If anything changes (even a timestamp) the build is considered invalid.

Here’s how our PHP builder currently works:

Phase 1 — FOD step

  1. Fetch the project source.
  2. Run composer install --no-autoload.
    The --no-autoload flag prevents generation of vendor/composer/* files, which can differ between Composer versions.
  3. Compute a fixed-output hash on the resulting vendor/ directory: ensuring the dependency tree is frozen and reproducible.

Phase 2 — Final installation step

  1. Fetch the project source.
  2. Copy the precomputed vendor/ directory from Phase 1 into the source tree.
  3. Run composer dump-autoload to regenerate the vendor/composer/* files.

This model yields excellent reproducibility because the hashed content in Phase 1 is static.
However, one potential remaining source of variation is the vendor/bin/* directory, which contains proxy scripts generated for package binaries.

Note: In an earlier version of the builder (version 1), we used a custom Composer plugin to create a local composer-type repository from an existing package. That local repository acted as the FOD itself. While this approach worked, it had some limitations and was eventually replaced in version 2 by the simpler composer install --no-autoload process described above.

It would be very helpful if Composer offered a flag or option to skip generation of vendor/bin/* during the initial install (Phase 1).
Then, in Phase 2 (only once the FOD hash is fixed) we could safely generate those binaries (which might vary slightly) without affecting reproducibility.

Specifically:

  • We already use --no-autoload to skip autoload generation.
  • Is there a similar flag or configuration option to tell Composer not to create the vendor/bin proxies?
  • If not, would you consider adding one — perhaps --no-bin-install, or a configuration key to disable binary proxy generation?
  • Likewise, similar to composer dump-autoload, could a command such as composer dump-bin (or equivalent) be feasible to regenerate them later?

Would you be open to:

  1. Confirming whether such a flag or config already exists (to suppress vendor/bin/* creation during install) ?
  2. If not, considering adding one, or accepting a proposal to support this use case ? it would help tools like Nix cleanly separate the hash-sensitive dependency phase from the more variable final packaging phase.

Thanks a lot for your time and for Composer itself!

I’d be happy to provide more technical details about our setup if that helps clarify the use case.

Cheers

2 Likes

Isn’t that what --download-only is for?

2 Likes

Hey,

I have just tested it, but it doesn’t create a vendor/ directory. I guess it download stuff in the cache folder, but the flag --no-cache is set… so basically, it does nothing.

Are you suggesting to use the cache folder for the FOD instead ?

In the meantime, I updated the PR and took into account the remarks from @Seldaek.

The phase 1 of the builder (the FOD) doesn’t generate the autoload files thanks to --no-autoload AND now also delete the bin-dir (rm -rf $(composer config bin-dir)) directory.

In the phase 2, two steps are now being done:

  1. composer dump-autoload
  2. composer install (to reinstall the bin-dir)

It seems to work fine so far.

Looking for comments and tests please!

1 Like

That would make sense. As far as I know it’s only used for storing the downloaded files that would make up the binaries. Seems like exactly what you want for the FOD.

I will try that and keep you posted.

Edit: It works at first glance, but too tired to edit all the commits from the PR, I will continue tomorrow.

Now implemented in build-support/php: improve PHP builder stability by drupol · Pull Request #459254 · NixOS/nixpkgs · GitHub using --download-only !

Everything works, modifying again all the commits was boring, but everything works from the outset.

Thanks for the idea, I have the feeling that it will resolve all the issues we mentioned in this thread.

It would be nice if you could test this and eventually review the PR.

2 Likes

As far as I remember, your initial Nix-Composer integration was using the cache, and I advocated against that because it relies on internal behavior and may also break in some funky ways… But up to you :wink:

There is no --no-bin-install BUT you can set composer config bin-dir /dev/null (or a random dir outside of vendor dir in case /dev/null breaks, not sure) and then undo that after the first install. That should work. Or you can simply ignore vendor/bin/ from the FOD or remove that dir before computing it?

Looks like I completely forgot, thanks for reminding me !

Yes, that is exactly what I initially did in Reinforcements for PHP maintenance needed - #9 by drupol, before switching to the --download-only flag method.

I will wait a bit before making any further changes (changes are tedious and time consuming as I have to recalculate the hash of all the packages using the builder in nixpkgs) to let other contributors share their perspectives.

As far as I can tell, we currently have 2 possible approaches:

  1. Option 1: Use the --download-only flag, while keeping in mind that its structure may change without notice. This doesn’t fully solve the problem. It might also be worth investigating whether Composer downloads archives (ZIPs) directly from GitHub, as that could introduce another potential source of variability.
  2. Option 2: Follow the approach I outlined in https://discourse.nixos.org/t/reinforcements-for-php-maintenance-needed/71794/6: use the --no-autoload flag and remove the vendor/bin directory.
1 Like