Skip to content

feat(software): read software definitions from Composer packages - #169

Merged
roxblnfk merged 5 commits into
1.xfrom
feat/package-registry
Oct 12, 2026
Merged

roxblnfk merged 5 commits into
1.xfrom
feat/package-registry

Conversation

@roxblnfk

@roxblnfk roxblnfk commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

🔍 What was changed

  • Composer packages can extend the software registry: extra.dload.registry in a package's composer.json points to an XML file inside the package (the dload.xml format, only <registry> is read). DLoad reads the root package and its direct dependencies from vendor/composer/installed.json, so no Composer plugin is involved.
  • <package>#<id> picks a package's entry whatever shadows it (dload get acme/fw#gh:^2.0, <download software="acme/fw#gh">). dload software shows each entry's origin and a Shadows: line; dload show shows Source:. <registry from-packages="false"> switches package entries off.
  • The core runs binaries without a shell and keeps names inside their directories, for every config source: see "For users" below.

How it works

  • Priority: dload.xml > official registry > packages; among packages the root one first, then the order of require, then require-dev. Transitive dependencies are never read.
  • A package entry that clashes with another source warns only when that id is actually downloaded, and only when the repositories differ; the clash warning lists every <package>#<id> to choose from.
  • A broken declaration (missing or outside file, invalid XML or JSON, missing attribute, path instead of a file name, invalid regex, a version-command other than one option or version) is a warning and is skipped; DLoad keeps working.

Why?

Frameworks and CMSs need binaries of their own; copying entries into every project's dload.xml or growing the official registry for one package does not scale (#140).

For users

  • version-command is split on whitespace and passed to the binary as arguments, without a shell: pipes, redirects and quotes no longer work there. Official entries only use --version and version.
  • A binary name or a file rename must be a file name: a path, .. or a Windows device name fails the install instead of writing outside the destination.
  • A binary named like a.b is installed as a.b, no longer as a.b.b.
  • An asset whose name is not a valid file name (free text on a self-hosted GitLab) is saved under a sanitized temp name instead of failing.
  • dload show lists a configured software with a numeric id, like alias="7"; before, it never matched.

Review notes

  • Package entries are third-party input that dload show acts on: it checks the version of every registry binary present on disk. Review rounds found ways to turn that into command execution — a shell in version-command, ../ in a binary name, arguments chosen for a binary the project already has (bun vendor/evil.js, rr reset). Hence the strict shape of package entries, and show never runs a package entry's binary unless it is requested or configured.
  • escapeshellarg() on Windows replaces ", % and ! with spaces, so a path like C:\proj\100%\velox.toml reached vx corrupted. BinaryExecutor now uses proc_open() with an argument array and a closed stdin.
  • Text from the registry, packages, release APIs and binaries goes through Logger::text() (console tags escaped, control characters stripped) wherever it is printed: log values and exception traces, software, show, init, install messages, the output of a failed build. Reasons: XML 1.0 rejects ESC but allows C1 controls such as CSI (&#x9b;), and an invalid style in printed text makes the Symfony formatter throw.

Checklist

  • Closes Registry extension from direct Composer dependencies #140
  • How was this tested:
    • Unit and integration tests added; the full suite and Psalm pass locally on Windows
    • Tested manually: fixture projects with package registries, including the injection attempts from the reviews
    • Symlink and POSIX-only cases, skipped on Windows, pass in CI on Linux

Documentation

  • README and its ru/es/zh translations: a "Software from Composer Packages" section, and the rules of version-command, the binary name and rename.
  • dload.xsd (from-packages, <package>#<id>, and the same rules) and resources/software.schema.json; the dload-fetch-tool skill with a new references/package-registry.md.

Squash commit message

Several later commits only fix what an earlier commit of this PR introduced; the changelog should carry only:

feat(software): read software definitions from Composer packages (#169)

feat(software): address the entry of a package as `<package>#<id>` and report shadowed entries
fix(binary): run a binary without a shell, passing every argument as is
fix(installer): keep binary names and file renames inside the destination directory
fix(installer): install a binary named like `a.b` as `a.b`, not `a.b.b`
fix(downloader): save an asset under a file name whatever the release API calls it
fix(cli): print console tags in registry text and logs literally and strip terminal control characters
fix(cli): list a software with a numeric id as configured in `dload show`

feat(software): address the entry of a package as `<package>#<id>` and report shadowed entries
fix(binary): run a binary without a shell, passing every argument as is
fix(installer): keep binary names and file renames inside the destination directory
fix(installer): install a binary named like `a.b` as `a.b`, not `a.b.b`
fix(downloader): save an asset under a file name whatever the release API calls it
fix(cli): strip console tags and terminal control characters from registry text

A framework or CMS can ship the binaries it needs without every project copying them into its dload.xml, and without a DLoad release for each tool. Only the root package and its direct dependencies are read, and their entries never override dload.xml or the official registry (#140).

Package entries are third-party input that `dload show` acts on, so the core stopped trusting any config: a version-command used to reach the shell, a binary name could leave the target directory, and on Windows escapeshellarg() corrupted `"`, `%` and `!` in arguments. Package entries are held to a stricter shape on top: one version option, file names, valid regexes.

Assisted-By: Claude Opus 5.5
@github-actions github-actions Bot added enhancement New feature or request tests labels Oct 12, 2026
@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.47899% with 9 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/Module/Binary/Internal/BinaryExecutor.php 78.94% 4 Missing ⚠️
src/Module/Software/ComposerPackages.php 98.72% 2 Missing ⚠️
src/Module/Binary/Internal/BinaryProviderImpl.php 83.33% 1 Missing ⚠️
src/Module/Software/ShadowReport.php 98.07% 1 Missing ⚠️
src/Service/Logger.php 85.71% 1 Missing ⚠️
Files with missing lines Coverage Δ
src/Bootstrap.php 100.00% <100.00%> (ø)
src/Command/Get.php 93.25% <100.00%> (+50.56%) ⬆️
src/Command/Init.php 83.33% <100.00%> (ø)
src/Command/ListSoftware.php 100.00% <100.00%> (ø)
src/Command/Show.php 90.24% <100.00%> (ø)
src/DLoad.php 97.05% <100.00%> (+0.08%) ⬆️
src/Module/Binary/Internal/AbstractBinary.php 41.37% <100.00%> (+24.13%) ⬆️
src/Module/Common/FileSystem/FS.php 100.00% <100.00%> (ø)
src/Module/Downloader/Downloader.php 100.00% <100.00%> (ø)
src/Module/Installer/Installer.php 100.00% <100.00%> (ø)
... and 9 more

... and 4 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

fix(software): find a `dload.xml` alias containing `#` by its exact id
docs: say the official registry warns only when it points to other repositories, and document `__root__#<id>`

On Windows proc_open() with an argument array warns when CreateProcess fails, for a truncated binary or one built for another OS; `bin/dload` has no error handler, so the warning reached the console.

Assisted-By: Claude Opus 5.5
…nit command

fix(cli): list a software with a numeric id in `dload show` and `dload init`
docs: document the file name rule and the extension of `rename`, and the id rules of package entries

Symfony's formatter reads `<fg=…>` and `<href=…>` in any printed text, and an invalid style in it throws; package strings reach the log through warnings and through call arguments kept in a stack trace.

Assisted-By: Claude Opus 5.5
test(cli): keep call arguments in the trace whatever `zend.exception_ignore_args` is

A binary name may contain `<` and `>` on POSIX, so the path of a package's binary could carry console tags. CI runs with php.ini-production, which drops call arguments from traces, so the trace escaping test found nothing to check there.

Assisted-By: Claude Opus 5.5
fix(cli): print the output of a failed build literally
test(cli): keep string arguments in the trace whatever `zend.exception_string_param_max_len` is

A package's binary name may contain `<` and `>` on POSIX: an invalid style in it made the formatter throw after the file was written, before its chmod, so `get` reported a failure for an installed, non-executable binary.

Assisted-By: Claude Opus 5.5
@roxblnfk
roxblnfk merged commit 03878e9 into 1.x Oct 12, 2026
25 checks passed
@roxblnfk
roxblnfk deleted the feat/package-registry branch October 12, 2026 19:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Registry extension from direct Composer dependencies

1 participant