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. 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 alternatative stack. That enables users to install packages and refer to dependencie under the default names. (Features #1 and #2) Now we are done with RPM-level compatibility. To asssure 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 futher, 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 a 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 (kikstart 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 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. ((No) 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). 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). 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 for a proof of concept. All packages are prefixed with "test-" string to provide an isolated test environment. After enabling postmodular0 repository and 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 into test-stream-stack-default stream. You can try: # dnf install test-application # dnf install test-stack # dnf swap --allow-erasing test-stream-stack-default test-stream-stack-2