brioche-dev / brioche

A delicious package manager
https://brioche.dev
MIT License
343 stars 5 forks source link

Support packages with multiple simultaneous versions #97

Open kylewlacy opened 3 months ago

kylewlacy commented 3 months ago

Projects like Postgres support multiple simultaneous major versions at a time. Users also are likely to want to be able to install a specific major version, e.g. installing Postgres 15.x specifically because that's the version the underlying database targets. Major version upgrades for Postgres are disruptive, and we can't assume all users will be able to upgrade easily, and we should also be able to offer the latest security patches released for previous versions that still get support updates.

Many package managers solve this by having separate packages for each major version, e.g. postgres14 and postgres15 (Nixpkgs, Ubuntu apt, etc). Others have a single package name with a specific version number, e.g. postgres@14 and postgres@15 (Homebrew, Docker, etc).

Separate packages for each version are straightforward and simple, but lead to duplicated code across versions, plus it doesn't scale if we want lots of packages to support multiple simultaneous versions.

kylewlacy commented 3 months ago

I designed the Brioche Registry to organize package versions in a way that's very similar to Docker: basically, the project is identified by a hash of its contents. Then, we associate the package name plus a tag to a given hash. One project can have multiple tags, each tag can change over time, and we offer a special latest tag that by convention refers to the latest version of a project.

(Currently, Brioche itself will always resolve a dependency using the latest tag, this was done as part of the MVP implementation. But, a version tag is also published based on the value of project.version too)

Here's how I'm currently thinking about handling this feature:

  1. Update the export const projects = {} object to support setting a otherVersions key, listing all other "active" versions of a project. version would still be used as the "latest" version (open to bikeshedding for the names here)
  2. When a project is published, it gets a tag for each version listed in versions (plus latest)
  3. When a dependency is resolved with a version (e.g. import postgres from "postgres@16.0.0"), we use the version number to find the right project in the registry
  4. Internally, when the module is loaded, we add a query param that lets the runtime know what the version of the dependency is (or some other runtime magic)
  5. We add a special Brioche.projectVersion constant. Really, this could just be a JS getter function that uses (4) to determine the version number

And here's what it would look like:

import * as std from "std";

export const project = {
  name: "postgresql",
  version: "16.0.0",
  versions: [
    "15.0.0",
    "14.0.0",
  ],
};

export const sources = {
  "16.0.0": std.download({ /* ... */ }),
  "15.0.0": std.download({ /* ... */ }),
  "14.0.0": std.download({ /* ... */ }),
} as const;

export default function() {
  const projectVersion = Brioche.projectVersion;
  // => Returns "16.0.0", "15.0.0", or "14.0.0" depending on which version was imported

  const source = sources[projectVersion];
  // ... build Postgres from source ...
}

This I think would be an MVP version of the feature. This could further be extended to handle things like version aliases, like stable / beta / nightly in Rust