Packaging has always been a critical topic in IT. Its purpose is to address fundamental software management objectives: Continuous Testing, Continuous Delivery/Deployment, and Continuous Integration. Historically, we rely on package management systems of varying complexity to handle dependencies, build software, and package the resulting artifacts (compressed if necessary), along with integration scripts, dependency manifests, and digital signatures.
Over time, as modern programming languages evolved, we’ve seen the rise of language-specific build tools that offer rudimentary dependency management, delivery, and integration capabilities. Examples include (in no particular order):
- CPAN (Perl)
- CRAN (R)
- Maven and related tools (Java)
- PyPI (Python)
- Gem (Ruby/Ruby on Rails)
- npm (Node.js/JavaScript)
- Cabal (Haskell)
- go get (integrated into Go)
- Cargo (Rust)
- Hex (Erlang)
- …and many others.
Nearly every new language now includes a tool for downloading libraries required to run applications. Initially, these were seen as competitors to established package systems like RPM, DEB, AUR, and Portage. However, the perspective has shifted — they are now viewed as plugins or backends that complement traditional package management.
Despite their convenience, these tools come with notable limitations:
- Poor cross-language integration: They struggle to resolve dependencies outside their own ecosystem (e.g., packages using FFI or dynamic library loading).
- Weak security practices: Many lack robust vulnerability scanning or signing mechanisms.
- Version duplication: multiple library versions (including those with known security flaws or critical bugs) often coexist without cleanup.
- Unreliable dependency resolution: some (like PyPI) occasionally generate unsolvable version conflicts.
- Slow critical updates: Proactive patches for essential libraries are often delayed or unenforced.
Given the widespread adoption of language-specific tools, we’ve chosen not to fight user habits but instead to combine the strengths of traditional package management and these newer systems. The solution? Declarative packaging.
For decades, we relied on procedural packaging, where we explicitly defined every step: how to build, install, test, and deploy software. Now, we’ve shifted to leveraging language-specific tools for their native workflows while supplementing them with what traditional systems do best:
- Dependency resolution (including cross-ecosystem graphs).
- Software Bill of Materials (SBOM) generation.
- Security and compliance checks (e.g., CVE scanning, FHS/SELinux enforcement).
Instead of micromanaging how tasks are done, we now declare what needs to be achieved — letting plugins handle the implementation. This approach drastically reduces package script complexity. Below are illustrative examples from Fedora’s packaging:
Currently, declarative packaging support in RPM remains limited. Only Python (via pyproject
set of tools) and the Meson build system are fully supported. Other ecosystems, like Cabal (Haskell), Golang, and Cargo (Rust), rely on a temporary workaround: instead of dynamically generating rules, they produce complex packaging scripts. However, this approach is slated to change in the near future.
Another challenge stems from nonstandard practices within language ecosystems, often due to historical baggage. We’re actively addressing these edge cases by aligning applications and libraries with standardized workflows. This effort isn’t just technical — it requires community collaboration. Sometimes, it means persuading stakeholders (and yes, occasionally dealing with stubbornness).