FabricMC / fabric-loader

Fabric's mostly-version-independent mod loader.
Apache License 2.0
610 stars 257 forks source link

Let's talk versioning #35

Closed asiekierka closed 5 years ago

asiekierka commented 5 years ago

I'm going to write up a rough concept I had for discussion:

The goal of a good versioning system would be to make things easy for modpack developers, users and modders. That's important.

Let's take some notes from SemVer: MAJOR.MINOR.PATCH-BUILD, to put it in a very, very shortened version. But let's change things up a bit.

Another approach would be to have separate versioning for API (SemVer), user-facing mod (whatever parseable) and network protocol (integer). However, I have concerns about proper maintaining of the network protocol version by modders in particular - if we had a higher-level networking system in FabricAPI we could perhaps check some kind of immutable schema, but eh...

UpcraftLP commented 5 years ago

I think we should just use semver (with your additional contracts). That'd also make it easier for inexperienced users to tell whether or not two versions of a mod are considered compatible.

One suggestion I might add tho: prefix the version with the current mc version you are targeting, ex.

asiekierka commented 5 years ago

The exact snapshot name will confuse users. Mods break across them very occasionally.

For point-versions of Minecraft, we can even inject in removed code as an addon, sometimes :thinking:

asiekierka commented 5 years ago

Also, what I proposed is simplified SemVer - some aspects of it, like the difference between -build and +build, will probably be skipped (for instance, many modders like to include the build number always due to the lack of release engineering mechanisms)

asiekierka commented 5 years ago

What my solution is missing is a way to discern between unstable builds and stable builds. SemVer did that with -/+, but I'm not sure if we want that...

UpcraftLP commented 5 years ago

what I usually do is add maven-like qualifiers between the patch version and build number (eg. -beta -> 1.13.2-1.0.0-beta-<buildnumber>)

2xsaiko commented 5 years ago

Yeah, that would be what I'd do too, except maybe put the beta at the end, so it still sorts properly: 1.0.0-53 1.0.0-54-beta 1.0.0-55-beta 1.0.0-56

asiekierka commented 5 years ago

The thing is, how do we discern between 1.0.0-53, 1.0.0-beta and 1.0.0-beta-53? I'd say perhaps make the buildnumber a dot: 1.0.0, 1.0.0.53, but 1.0.0-beta.53.

UpcraftLP commented 5 years ago

seems fine, yes. maybe treat everything after the build number as qualifier, so 1.0.0.53-beta-53 is older than 1.0.0.53-beta.53 (a dot is more important than a hyphen) [EDIT: those two versions should never be compared tho, anyway, since the build number would differ] 1.0.0.54-beta is newer than both previous ones.

hugeblank commented 5 years ago

Maybe you could just add an S or a U to signify stable/unstable before you go into the versioning? Additionally I think it would be useful to prefix the versions in some way that makes them distinguishably different, like by using the abbreviated name, Ex: S-MC1.14-FL1.0.0

Barteks2x commented 5 years ago

Forge apparently has not-so-well known approach MCVERSION-MAJORMOD.MAJORAPI.MINOR.PATCH, optionally with -final, -betaX or -rcX, described on their readthedocs page. That looks like a very reasonable approach to me. Minor could really be incremented on each build.

UpcraftLP commented 5 years ago

Maybe you could just add an S or a U to signify stable/unstable before you go into the versioning? Additionally I think it would be useful to prefix the versions in some way that makes them distinguishably different, like by using the abbreviated name, Ex: S-MC1.14-FL1.0.0

I think that would confuse people more than it'd help

asiekierka commented 5 years ago

I prefer BREAKING.API/NETWORK.PATCH to MAJORMOD.MAJORAPI.MAJORPATCH myself. That, or separate versions for API and mod, which could make sense especially if JARs-in-JARs become a thing and APIs can be distributed separately without conflict!

3TUSK commented 5 years ago

For reference:

https://semver.org/#spec-item-10

  1. Build metadata MAY be denoted by appending a plus sign and a series of dot separated identifiers immediately following the patch or pre-release version. Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. Build metadata SHOULD be ignored when determining version precedence. Thus two versions that differ only in the build metadata, have the same precedence. Examples: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85.

https://semver.org/#spec-item-11

  1. Precedence refers to how versions are compared to each other when ordered. Precedence MUST be calculated by separating the version into major, minor, patch and pre-release identifiers in that order (Build metadata does not figure into precedence). Precedence is determined by the first difference when comparing each of these identifiers from left to right as follows: Major, minor, and patch versions are always compared numerically. Example: 1.0.0 < 2.0.0 < 2.1.0 < 2.1.1. When major, minor, and patch are equal, a pre-release version has lower precedence than a normal version. Example: 1.0.0-alpha < 1.0.0. Precedence for two pre-release versions with the same major, minor, and patch version MUST be determined by comparing each dot separated identifier from left to right until a difference is found as follows: identifiers consisting of only digits are compared numerically and identifiers with letters or hyphens are compared lexically in ASCII sort order. Numeric identifiers always have lower precedence than non-numeric identifiers. A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal. Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0.
NikkyAI commented 5 years ago

i'd prefer the beta at the end too..

1.0.0-53 1.0.0-54+alpha 1.0.0-55+beta 1.0.0-56

if there is a easy way to make jenkins reset the build number on increasing minor version... that would be nice then i would probably prefer to separate build numbers with a dot too and would not need a + anymore or free it for other stuff

1.0.0.53 1.0.0.54-alpha 1.0.0.55-beta 1.0.1.1

Prospector commented 5 years ago

+1 for resetting build numbers. I really despise how unrelated build numbers are to the actual version. Really, I wish it would automatically mark every build as an alpha until I promote it to non-alpha

asiekierka commented 5 years ago

Again, no part of the system cares about what the build number is, and it better stay that way...

UpcraftLP commented 5 years ago

Really, I wish it would automatically mark every build as an alpha until I promote it to non-alpha

well you can do that lol, just requires some modifications to your gradle script

yueh commented 5 years ago

The problem with using a homebrewn version scheme is that defining a total order on it is not trivial, especially once mod authors decide to introduce unforeseen changes to it. Especially something like "it's semver, but not really" is more confusing than any alternative.

Semver might have it's issues, but at least it is well defined, widely used and there should be existing libraries to parse it and avoid writing a new and untested one.

In most cases it is even pretty simple. E.g. using MAJOR.MINOR.BUILDNUMBER would still result in a valid semver version, even when the buildnumber is never reset on major or minor changes. Thus in case someone really does not like using the additional features to tag pre releases, add metadata, and so on, it is still easy to have it play nicely with other mods.

Another point to consider would be the not include the MC version at all. This probably sounds strange at first glance, but a mod will never ever depend on a specific minecraft version. It will only depend on a specific fabric version, which itself will depend on the minecraft version, therefore implicitly inherit it to the mod. The main issue with including the minecraft version is, that it's often not a useful information for the player. Especially with patch releases it is not uncommon to have some mods specifying x.y.0 as dependency, yet running just fine on x.y.1+. While other mods won't and have a specific x.y.1+ release. Therefore a player either has to try it and wait until it crashes at some point or has to ask. If I recall correctly, there are/were even APIs designed for 1.8/1.9, which still worked and where intended for 1.10/1.12, but never made a new release just to include the recent mc version.

If in doubt, the specific minecraft version can still be included as dependency without affecting the version at all. Might need a better tool support to make it easier for players. But that is our problem as developers to resolve and shouldn't be dumped on players at all. For them it should be as easy as throwing some mods together and then have it either work or display a warning that modA is incompatible with modB without updating.

Including something like a network protocol probably has the same issues like the minecraft version. Most player will not be able to make use of it at any point as well as most devs will not anticipate that the server will run 1.2.3 while the client still runs 1.2.1. Something like this has a good chance of being seen as chore enforced by the mod loader/etc and just be "fixed" by something like "let's just use the buildnumber for it".

Sure you can call mods out for various reasons/breaks/etc, but in the end it will benefit the player at all. Just stick to KISS, recommend using semver, maven, or whatever else, use it to resolve dependency issues. If not used maybe try a lexical order, otherwise just ignore it with a log entry warning about potential issues.

Different versions for the mod and api might be an idea. But that shouldn't be solved by the version scheme, but actually by something like allowing jar-in-jar or similar things. If a dev wants to separate it, they can do it, but they do not have to.

asiekierka commented 5 years ago

Semver might have it's issues, but at least it is well defined, widely used and there should be existing libraries to parse it and avoid writing a new and untested one.

The problem is semver is meant to be used solely for libraries and APIs, not user-facing content.

[...] a mod will never ever depend on a specific minecraft version. It will only depend on a specific fabric version, which itself will depend on the minecraft version, therefore implicitly inherit it to the mod.

Not really. With our snapshot updating pace, there can be many, many cases where Fabric API keeps working but some obscure patch or method call somewhere doesn't. So this is only really true for stable releases.

Therefore a player either has to try it and wait until it crashes at some point or has to ask.

Most cases of Minecraft version incompatibilities can be simply detected by checking if it calls any no-longer-present methods, classes or fields in net.minecraft.

Including something like a network protocol probably has the same issues like the minecraft version. Most player will not be able to make use of it at any point as well as most devs will not anticipate that the server will run 1.2.3 while the client still runs 1.2.1. Something like this has a good chance of being seen as chore enforced by the mod loader/etc and just be "fixed" by something like "let's just use the buildnumber for it".

I disagree. Maybe some mods would, but I think many classes of bugfixes would be beneficial to, say, server administrators, to distribute in a way which does not upset the client side. I think major mods could be seen adopting a scheme to allow for distributing such bugfixes, so to say.

Different versions for the mod and api might be an idea. But that shouldn't be solved by the version scheme, but actually by something like allowing jar-in-jar or similar things.

Agreed on this count.

yueh commented 5 years ago

The problem is semver is meant to be used solely for libraries and APIs, not user-facing content.

That was one of the issues I had in mind, but the basic scheme is not that hard to understand for a normal user.

Not really. With our snapshot updating pace, there can be many, many cases where Fabric API keeps working but some obscure patch or method call somewhere doesn't. So this is only really true for stable releases.

Snapshot releases will always be unstable. Be it minecraft itself, fabric making a breaking change, etc. This is more or less a dependency issue and not how to name a version. The version should be about identifiying the mod itself to help other mods. Not about including every dependency.

I disagree. Maybe some mods would, but I think many classes of bugfixes would be beneficial to, say, server administrators, to distribute in a way which does not upset the client side. I think major mods could be seen adopting a scheme to allow for distributing such bugfixes, so to say.

Yes, it would be nice to have. But it takes some serious efforts and it will lead to many incompatibilities later on. E.g. a dupe fix might need a network change. So bumping it might cause dependents to stop working, because the defined an upper bound. Even without any API break/change.

The difficulty here lies simply in trying to combine many different and complex things into something simple. It comes down to: (and I probably forget a couple)

  1. The MC version
  2. The fabric version
  3. A user facing identifier
  4. An API version
  5. A network protocol

Each one has very different requirements and imo are not compatible with eachother

1) Could be a range. Could work across all snapshots, could stop working between an a and b release of one. Could work with the last snapshot + RC + stable. Having something like 18w50a,1.14-RC,14.0-2.0.1 is a bit insane. It should really be part of something else. Like a dependency.

2) Pretty much the same as mc. With the additional feature of being able to break multiple times per snapshot. Also a dependency.

3) Could be whatever, just allow users to determine which is the latest one. This is probably a nice place to put a MC version. But it shouldn't be used for resolving dependencies. E.g. part of the filename or listed on a release list. But nothing more.

4) This is the most important one for mod compatibility. And it shouldn't be just one digit. I frequently have the issue of wanting to add backword compatible changes to our API, but it is always a hassle because increasing a single digit is simply missing any semantic about it. Semver is pretty much perfect for it, when used correctly. When done right, it can even survive many minecraft versions without a breaking change.

5) Networking should be internal only and just indicate if a server version works with a different client version or not. Any change should not affect the compatibility between different mods. If it does, it's an API break and nothing else.

In other words the alternative listed in the first post exactly as described. With a likely issue of devs mess up things like the network protocol. Adapting semver for the user facing version might be an option. E.g. any network protocol change is pretty much a break and could bump the major, while leaving the API version unchanged. But that will have new implications, like other mods cannot easily declare to which versions they are compatible as long as the API version is also not exposed publicly.

asiekierka commented 5 years ago

I'd say splitting all of the versions is a good idea. Here's what I think. First, comments:

And thoughts:

So, really comes down to:

Is that correct?

falkreon commented 5 years ago

There is a case where the semver will unavoidably be a user-facing identifier: mod rejections. The game knows it's missing the right semver version, but with this scheme doesn't look like it would have an associated user-facing version to display. So the server would have to say "Required version 1.0.3 of Foo, but found version 0.8+alpha", where the user-facing version for all we know could be "Scurrilous Squirrel 3". It's an extreme example, but still gives me pause.

asiekierka commented 5 years ago

Yes, but I don't think we can quite sway modders to abandon their elusive versioning styles... (Besides, most mods should depend on APIs, not other mods themselves - and in those cases the API versions should not have an user-facing equivalent :thinking: )

UpcraftLP commented 5 years ago

Yes, but I don't think we can quite sway modders to abandon their elusive versioning styles... (Besides, most mods should depend on APIs, not other mods themselves - and in those cases the API versions should not have an user-facing equivalent 🤔 )

That'd imply mods had a proper API tho 🤔

yueh commented 5 years ago

I'd probably avoid using something like version and versionId. It too similar and can easily be mixed up. Maybe using ubuntu as example with a version (semver) and codename (whatever) might be an idea.

Regarding the network protocol. This might depend on how low level it is. With a more high level approach, it is certainly not impossible to have non breaking changes. E.g. have fallbacks, not support ever feature clientside and so on. Nevertheless it could be too rare to take it into consideration.

asiekierka commented 5 years ago

"version" and "codename" is IMO an approach worth considering, where "codename" would become the "human" version, so to say.

As for the network protocol, I've had a plan of moving the most crucial network+registry hooks into a separate module once JAR-in-JAR hits; the network protocol version would just become handled by that.

asiekierka commented 5 years ago

The only thing I fear is developer confusion, as well as what @falkreon said. Keep in mind, though, that in a JAR-in-JAR world APIs are already separate; so you'd already see a message like "Requires MyModAPI version 24" even if MyMod is of version 3.2.1a2b3948.

falkreon commented 5 years ago

A lot of my concerns might be ameliorated if the "fabric example mod" had a @version@-style substitution where "version" and "codename" sort of replace-defaulted to the same thing. Or whatever example mods people are looking at. People copy what they see.

NikkyAI commented 5 years ago

i think once we come to a consensus all fabric modules and example mods will be adjusted as well as maven cleaned probably

asiekierka commented 5 years ago

Okay, here's a proposal based on notes from @DragoonAethis, inspired by Android:

On the mod's side:

The version and versionCode have no forced relationship.

On the dependency declaration's side:

This is similar to the "version/codename" proposal above, except instead of the "code-facing version" being a string, it's an integer - I mean, if only the code is supposed to read it, why not?

I'm for this proposal. Please discuss.

asiekierka commented 5 years ago

Minor question: Can we somehow extend the range format to cater to the "optional mod" case, or should we just add an additional field ("optional": true)?

DragoonAethis commented 5 years ago

Dependencies could have a "class" field attached, like so:

asiekierka commented 5 years ago

Re: class - That's better, as I'd like to take into account future plans to add lazy-loaded mods (for expensive hooks, say) - then we could just add a way to distinguish between "optional, but load the mod if it's present" and "optional, but don't load the mod if it's present".

Re: "provides" - Due to fear of classpath pollution, the idea here is to use "JAR-in-JARs", or the ability to load JARs contained inside other JARs. Could still be nice to provide "provides" though, I guess.

DragoonAethis commented 5 years ago

So you could have something like "integrates" for "nice to have, but load only if requested". Edited proposal above.

UpcraftLP commented 5 years ago

can we also get a command line flag to ignore version format/range for the dev workspace? so that we can do stuff like ${version} that is replaced when building?

NikkyAI commented 5 years ago

with jar-in-jar the packaging step should add the provided versions data automatically.. possibly in a separate file

the ranges.. can they be open ended ? lets say.. anything newer than 1003

with semver wou could specify to depend on version 0.4.* and when the mod jumps to 0.5 it would not work, how would that kind of problem be tackled here ?

i like the monotonically incremented number though.. means i can just reuse the jenkins build number and then push those to curseforge eventually

and i suspect that looking up what the versionCode is for a given version of some other mod will be a real pain, or modders will just add the versionCode in the filename

NikkyAI commented 5 years ago

about the dev workspace.. that should be dealt with using gradle's task processResources, or are you building in your dev workspace completely withot gradle tasks ? you can configure idea to delegate building tasks to gradle and it will work fine too

DragoonAethis commented 5 years ago

If you want to have something like 0.4.* (with example versions: 0.4.0 with code 50, 0.4.1b1 w/ 51, 0.4.1 w/ 52, 0.4.2 w/ 53, 0.5.0 w/ 54), you can say in your mod that you want a dependency with versionCodeRange <50, 52> which you've personally tested, and the dependency (at code 53, outside of this range) could have a compatRange that makes it compatible with <50, 53> - dependency@53 satisfies the mod requirements for <50, 52>.

UpcraftLP commented 5 years ago

about the dev workspace.. that should be dealt with using gradle's task processResources, or are you building in your dev workspace completely withot gradle tasks ? you can configure idea to delegate building tasks to gradle and it will work fine too I know about the processResources task, that's what I'm currently using. the issue is that when you launch inside your workspace with just the idea run configs, that does not get executed.

so I'm asking if we can disable the planned strict version format enforcement for our workspace.

DragoonAethis commented 5 years ago

IMO enforcement should be disabled on request in dev environments (easily disabled, with loud warnings in the logs or something) but also should be possible to override for final users - if they want to just try whenever some probably broken mod combo works (abandoned mods that never bumped their dependencies?), there should be a (non-obvious and very much "I know what I'm doing, let me shoot myself in the foot") way to override the loader restrictions.

NikkyAI commented 5 years ago

okay but assuming 0.4.3 is the latest release of a mod but at some point 0.5.0 will break API, but i have no idea which versionCode it will be

i do not think many people actiavely try to target old versions of other mods or are those kind of modds supposed to make a $modid-api with a version and jar-in-jar that ?, that one could just stay the same until eg 0.5.0 hits

i agree for some checks to be soft failures when -Dfabric.dev=true or whatever the system property was

DragoonAethis commented 5 years ago

You don't have to know the versionCode which breaks compatibility - it's up to the dependency developer to properly mark a mod/library as compatible with a certain range of previous versionCodes using compatRange. So if your dependency dev fixes issues and releases new, fully compatible versions, your mod can automatically use newer versions as they become available in the classpath. Once a compat break occurs (due to compatRange in the dependency being bumped to something you don't support), your mod will fail to load if an older, compatible version isn't present.

The much bigger problem would be to ask everyone to actually properly maintain their versionCodes and compatRanges...

RedstoneParadox commented 5 years ago

I think that versionCode should be a byte array (or maybe a helper class containing an int array) instead of a single number so you can go from, say, 1.9.1 to 1.10.1, without messing things up. This would also mean that modders don't have to be constrained to a 3-number system.

RedstoneParadox commented 5 years ago

It's also a cleaner way of doing things.

asiekierka commented 5 years ago

@RedstoneParadox The problem with that is it complicates the range definition.

Now, instead of, say, "[1000, 2000)", you need to do "[[1,0,0], [2,0,0])". I'm not opposed, but I'm not sure.

DragoonAethis commented 5 years ago

On Android side of things, huge breaking versions sometimes bump the versionCode not by 1 but by 1000/10000. It's kept simple and solves problems with branching (eg 0.4.x remains on two-digit version codes, 1.0.x goes up to 1000 and counts up from that, you can safely work on both 0.x and 1.x branches without conflicting codes).

DragoonAethis commented 5 years ago

An array of versions would be fine, but I suspect some people would like to release prerelease versions and not bump the code, to keep numbers tidy and matching the visible one. A monotonically increasing number is used precisely to detach the user-facing version from the dependency resolver-facing one.

Wolf480pl commented 5 years ago

An increment of MAJOR means that the JAR may not be compatible when updating worlds or with other mods. It doesn't mean it has to be incompatible - it could just signify a major update overall.

IMO it's important that this ends up in user-facing version, even if you end up having the user facing version be different from the internal version.

Also, I think server admins and modpack developers would benefit from both world version and protocol version being easily visible to them.

Moreover, I think mod developers (and possibly modpack developers) would benefit if the version that's actually used for dependency resolution was visible to them, instead of being hidden in some semantic-less integer that nobody wants to look at.

With the version code approach, you could have a situation where see 1.4.2-build42 and 1.4.2-build48, and you expect them to be API-compatible, but you get version check errors, and then it turns out that they have different versionCode and disjoint compatibility ranges... IMO that'd be annoying.

asiekierka commented 5 years ago

The things to consider are:

yueh commented 5 years ago

The problem with a similar approach to android is, that it is completely useless for minecraft. That scheme is basically optimized for distributing applications without any dependency at all. So it is nice to allow CDNs to easily identify the most recent version and push it out.

But as already said, completely useless for any dependency resolution. The best thing possible is to define it working with exactly the version it is compiled against (or any previously used once). Should that version be bumped, it requires that any dependant is forced to release a new build. At least, they want to be safe in terms of compatibility. Or ignore it and hope it does not crash.

And something like "use 10/100/1000/10000 increments" is just ridiculous. It just adds needless complexity as every deps will use a different approach and has to be handled differently. Also what happens when some mod decides to use 100 increments, because the were convinced, they'll never have that many release without a breaking change and then just roll into 99 -> 100 without a breaking change? So everyone expects that's a breaking one, act accordingly and stop loading until they release a new one with 1000 increments. Until the mod decides to do a breaking change from 105 -> 200 while now everyone expected 1000 to be the next breaking one.

The problem with any attempt is imo that it never provided any benefit. E.g. it never left the PoC phase, so it always had issues comparing/sorting them correctly. Related to that then failed to ensure the most recent version was loaded. Or simply because someone decided they knew it better than every modder and it's fine to just and simply ignore whatever the mod specified or even themselves not following the rules everyone else should follow. It would have only required the work update their own mod, build environment, potentially convincing players that "yes, this is the most recent version. no, even if you don't believe it, one released 1 year ago is never more recent than the one from yesterday" and so on. All in all, just a bunch of wasted time to do it and still deal with the same issues as before.