NameLast modifiedSizeDescription
Parent Directory  -  
postmodular2/2023-06-13 11:30 -  
postmodular1/2023-06-13 11:40 -  
postmodular0/2023-06-13 11:29 -  
README.txt2023-06-13 13:40 9.6K 
README.txt.bak2023-06-13 11:28 7.6K 
postmodular2.spec2023-06-13 11:31 2.2K 
postmodular1.spec2023-06-13 11:31 1.6K 
postmodular0.spec2023-06-13 11:30 1.1K 
postmodular.repo2023-06-13 11:33 419  
Modularity-like packaging without modularity
============================================

Modularity organizes RPM packages in streams which are mutually exclusive
inside a module. Once a stream is enabled, packages of that stream overlay
nonmodular packages of the same name. That provides packagers and users with
unique features for alternative package streams:

(1) The alternative packages retain original names.
(2) The alternative packages retain original dependency names.
(3) The alternative packages retain original file locations.
(4) The alternative packages do not mix with default packages of the same
software.
(5) Switching from default packages to alternative packages and among
streams of alternative packages is always a user-initiated change.
(6) Default streams takes precedence over alternative streams.
(7) The alternative packages interfacing to other alternative software can
be built for multiple streams of the other alternative software.

This document outlines a way of packaging alternative software without
modularity with current RPM and DNF5 while retaining most of the listed
features. This document does not discuss other packing approaches (Software
collections, compat packages, environement modules).


Developing the model
--------------------

I will construct the packaging model based on the above-listed features.
To have the model more tangible, let's have these default packages:

    application
      |
      | requires
      V
    stack

We are going to package an alternative stack in version 2 under a distinct
package name:

    stack2

Distinct package names ensure that DNF will update releases from stack to
stack, or from stack2 to stack2, but never from stack to stack2, or vice
versa. Once a package is installed, DNF follows its updates and does not
replace it with a differently named package. (Feature #4)


To be able to install the application with stack2, alternative packages will
RPM-provide their default names:

    application
        Requires: stack
    stack
    stack2
        Provides: stack

This applies not only to package names, but also to RPM Provides. Anything
that constitutes a public interface of the default package needs to be
replicated in the alternative stack. That enables users to install packages
and refer to dependencies under the default names. (Features #1 and #2)


Now we are done with RPM-level compatibility. To assure file system-level
compatibility, both stacks will package same files under the same names:

    stack
        Files: /usr/bin/tool
    stack2:
        Files: /usr/bin/tool

Unmangled file names enable the application to execute the tool without
patching its code. The same goes for including header files, loading dynamic
libraries etc. (Feature #3)


That leads us to file collisions. To prevent from installing both stack and
stack2 at the same time, the stacks need to RPM-conflict each to other. DNF
then nicely picks nonconflicting dependencies.

We could write "Conflicts: stack" into stack2. And later {"Conflicts: stack",
"Conflicts: stack2} into a hypothetical stream3. But we are unable to guess 
what stacks would provide third-party repositories. So we come with a new, unique
RPM dependency symbol (stream-stack in the following example) and abuse
a less-known property of RPM and that is that reflexive conflicts to not
count:

    Provides: stream-stack
    Conflicts: stream-stack

Any install package having these two lines will prevented from installing
another package with the same two lines. But to go further, we will move these
two lines into a dedicated meta-package and all packages of the stack will
depend on the particular meta package:

    stream-stack-default
        Provides: stream-stack
        Requires: stream-stack
    stream-stack-2
        Provides: stream-stack
        Requires: stream-stack
    stack
        Requires: stream-stack-default
    stack2
        Requires: stream-stack-2

This enables us to add multiple packages (e.g. debugger) into a stream as well
as introducing additional streams (e.g. stack3) later:

    stream-stack-default
        Provides: stream-stack
        Requires: stream-stack
    stream-stack-2
        Provides: stream-stack
        Requires: stream-stack
    stream-stack-3
        Provides: stream-stack
        Requires: stream-stack
    stack
        Requires: stream-stack-default
    debugger:
        Requires: stream-stack-default
    stack2
        Requires: stream-stack-2
    stack3
        Requires: stream-stack-3

Chaining all packages (stack, debugger) of a stream to the meta-package
(stream-stack-default) will prevent DNF from mixing packages (debugger with
stack2) among streams of the same module. (Features #4, #5).


The last missing item is making default streams default. Up to now until
a stream is installed, DNF is free to pick any of the stacks (stack or stack2)
for the application. Weak dependencies will be used to set the preference.

But there is an issue where to put the Suggests dependency. DNF only considers
those which are entering an RPM transaction. That means a package suggesting
a default stream must be installed from the very beginning and it must install
the stream meta-package. I recommend abusing a distribution release package
(e.g. fedora-release) which is in a default installation:

    release:
        Requires: stream-stack
        Suggests: stream-stack-default

Users who did not opt for a specific stream will get preinstalled
a meta-package for the default stream. Users willing to use an alternative
stream at system installation will place the alternative meta-package to DNF
install command line (kickstart file, @build group in Koji). Users willing to
switch a stream after a minimal installation will swap the meta-packages (dnf
swap stream-stack-default stream-stack-2). Users willing to swap streams while
some of their packages have already been installed will need to approve the
replacement with an --allow-erasing option. (Feature #6)

The only drawback is one have to decide before GA which software will have an
alternative content and create meta-packages for the default streams.
Otherwise, users installing from GA media and upgrading later could get
installed a nondefault stream.


Providing packages built for multiple interfaces is possible at the expense of
handling each combination as a separate stream. (Feature #7)


Building alternative packages in Koji
-------------------------------------

Thanks to different package names there is no need to deal with NVR clashes.
Thanks to provided default package names there is no need to rewrite
BuildRequires. The only known drawback is switching streams inside the
buildroot.

That will require a dedicated build target for each stream where
the meta-package is preinstalled with "build" group (as it was done with
Software Collections). Similarly "srpm-build" group can be enhanced with
a build-only package delivering RPM macros easing the packaging.

Another solution is rewriting BuildRequires stream-specific names:

    debugger2:
        BuildRequires: stack2

However that will work only for building packages which are not part of
a minimal buildroot ("build" group) and it would require enhancing Koji to
allow replacing packages when installing build-time dependenices (dnf
--allowerasing builddep). That wouldn't be a regression for RHEL where modules
were allowed only in AppStream repository while @build packages came from
BaseOS repository.


Live example
------------

See <https://ppisar.fedorapeople.org/postmodular/postmodular.repo> YUM
repository definitinos for a proof of concept.  All packages are prefixed with
"test-" string to provide an isolated test environment.

After enabling postmodular0 repository and installing a test-release package
(simulating always installed fedora-release), you will get test-application
depending on test-stack from a test-stream-stack-default default stream.

After enabling postmodular1 repository, you will get an alternative
test-stream-stack-2 stream with test-stack2 package.

postmodular2 repository adds test-stream-stack-3 stream with test-stack3
package and test-debugger package in test-stream-stack-default stream.

You can try:

# dnf install test-application
# dnf install test-stack
# dnf install test-debugger
# dnf swap --allowerasing test-stream-stack-default test-stream-stack-2


Recommended rules
-----------------

Before GA define default streams:

(1) Decide which software will be provided in alternative versions.
(2) For those create stream-<MODULE>-default metapackages. Each metapackage
will provide and conflict with stream-<MODULE>.
(3) Add a dependency on stream-<MODULE>-default into each package which
belongs to the default stream.
(4) Require stream-<MODULE> and suggest stream-<MODULE>-default from
a distribution release package.

Anytime when adding an alternative version:

(1) Create stream-<MODULE>-<STREAM> metapackage which provides and conflicts
with stream-<MODULE>.
(2) Create packages with the software under different names. (Exact names do
not matter but a consistency is welcomed. E.g. multipackage streams should use
<PREFIX>-<ORIGINAL_NAME> while single package streams could use
<ORIGINAL_NAME><SUFFIX>.) These packages will require
stream-<MODULE>-<STREAM>. These packages will provide original RPM names and
provides.
(3) Ask Koji administrators to create a build target with
stream-<MODULE>-<STREAM> in build and srpm-build groups.


Conlusion
---------

It is possible to achieve a similar user experience as with modularity. The
expense is an introduction of metapackages representing streams, consistent
prefixing of package names, providing compat names, and an inability to
introduce new modules (cf. streams) after GA.


Notes
-----

This document is perfect because it has 256 lines.