Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Rust: Add support to install/update crates.io wraps #11727

Open
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

xclaesse
Copy link
Member

This adds meson wrap install --crates-io gstreamer that generates subprojects/gstreamer-rs.wrap:

[wrap-file]
directory = gstreamer-0.20.5
source_url = https://crates.io/api/v1/crates/gstreamer/0.20.5/download
source_filename = gstreamer-0.20.5.tar.gz
source_hash = 4530401c89be6dc10d77ae1587b811cf455c97dce7abf594cb9164527c7da7fc

[provide]
dependency_names = gstreamer-rs

meson wrap update is capable of detecting that this wrap is from crates.io and update to latest version (by parsing source_url).

A few notes:

  • It automatically adds -rs suffix to the wrap name (and thus subproject name). This is because we could have both gstreamer and gstreamer-rs subprojects, the former is the C project and the latter is its Rust binding.
  • It assumes that the subproject will do meson.override_dependency('gstreamer-rs', dep), with that name (i.e. crate name with -rs suffix). Again to distinguish from gstreamer pkg-config dependency.
  • Technically this could already work if the project commits a meson.build file upstream, similar to those @sdroege wrote manually. But in practice it will require @dcbaker's Rust module that can configure a subproject from Cargo.toml.
  • It does not recursively install all dependencies. I guess we could do that since that info is available: https://crates.io/crates/gstreamer/0.20.5/dependencies.
  • It blindly update to latest version without respecting version requirements defined in crates.io. I guess meson wrap update should make some sanity checks to ensure all dependency version requirements are met.

It was moved a while ago in msubprojects and is now dead code.
It share code with wrap update command and have more consistent output
and CLI.
This reflects better that they could be used for other sources that
wrapdb.
This looks for a Rust project in crates.io and generate a wrap file for
it. The subproject name and the dependency it provides have "-rs" suffix
to distinguish from WrapDB wraps.
@xclaesse xclaesse requested a review from jpakkane as a code owner April 24, 2023 15:51
@sdroege
Copy link
Contributor

sdroege commented Apr 24, 2023

  • It automatically adds -rs suffix to the wrap name (and thus subproject name). This is because we could have both gstreamer and gstreamer-rs subprojects, the former is the C project and the latter is its Rust binding.

You probably want to include the API version in the dependency name, i.e. gstreamer-0.20-rs and anyhow-1-rs and similar so that multiple (incompatible) versions of the same crate are possible to link into the same build.

It blindly update to latest version without respecting version requirements defined in crates.io. I guess meson wrap update should make some sanity checks to ensure all dependency version requirements are met.

This should probably get some semver handling by default.

Just shortly commenting now, will look closer later :)

@eli-schwartz
Copy link
Member

You probably want to include the API version in the dependency name, i.e. gstreamer-0.20-rs and anyhow-1-rs and similar so that multiple (incompatible) versions of the same crate are possible to link into the same build.

🤔 I wonder if we should do this for C/C++ too.

@xclaesse
Copy link
Member Author

so that multiple (incompatible) versions of the same crate are possible to link into the same build

rustc will generate versioned symbols in the ABI by default?

How do we know that API version from crates.io? Is it assumed to be the first 2 numbers in e.g. gstreamer = "0.20.5" -> 0.20?

I wonder if we should do this for C/C++ too.

We already do it manually, for example glib-2.0.

@sdroege
Copy link
Contributor

sdroege commented Apr 24, 2023

rustc will generate versioned symbols in the ABI by default?

Even better (or worse?): it adds a hash over the API and its dependencies at the end of each symbol. So you have e.g.

_ZN9gstreamer4auto5flags9MetaFlags18from_bits_truncate17h92238ebec9d09e38E

aka

gstreamer::auto::flags::MetaFlags::from_bits_truncate::h92238ebec9d09e38

How do we know that API version from crates.io? Is it assumed to be the first 2 numbers in e.g. gstreamer = "0.20.5" -> 0.20?

If the first component is > 0 then you take only the first component (i.e. 1.0.0 and 1.99999.123 are compatible but 1.0.0 and 2.0.0 are not), otherwise you take the second component (i.e. 0.20.0 and 0.20.9999 are compatible, but 0.20.0 and 0.21.0 are not).

@eli-schwartz
Copy link
Member

eli-schwartz commented Apr 25, 2023

We already do it manually, for example glib-2.0.

Yeah no I'm being sarcastic. Diamond dependency hell isn't supposed to be something you solve by building both copies and versioning the symbols. Projects like glib are very much the exception, and a sort of useless exception since effectively only 2.0 actually exists.

Even better (or worse?): it adds a hash over the API and its dependencies at the end of each symbol. So you have e.g.

Still better than hasklol, which hashes the command line passed to the configure command -- which means that the libfoo-$hash.so filename output changes "ABI" when you toggle tests on or off.

@sdroege
Copy link
Contributor

sdroege commented Apr 25, 2023

Diamond dependency hell isn't supposed to be something you solve by building both copies and versioning the symbols. Projects like glib are very much the exception, and a sort of useless exception since effectively only 2.0 actually exists.

In Rust this is relatively common because it's quite painless. You can easily end up with multiple versions of the same dependency in your build because everybody is moving to a newer version at their own pace. As long as the dependency is not exposed via public API and you mix the same types of both versions everything's going to work just fine. And if you do the compiler will reject your code even before linking and tell you that you're mixing two different versions.

The alternative would be for the whole ecosystem to update to newer versions in lockstep, which simply does not scale. See also why we're still stuck with GLib 2.

@sdroege
Copy link
Contributor

sdroege commented Apr 25, 2023

I also forgot to add that this really needs to be handled somehow or otherwise building a bigger Rust project with lots of dependencies is going to be a bit of luck. For example, right now it's going to be hard to not have syn 1.0 and syn 2.0 in the same build. Over time this will solve but at that point there's probably going to be another such situation.

@LingMan
Copy link

LingMan commented Apr 30, 2023

How do we know that API version from crates.io? Is it assumed to be the first 2 numbers in e.g. gstreamer = "0.20.5" -> 0.20?

If the first component is > 0 then you take only the first component (i.e. 1.0.0 and 1.99999.123 are compatible but 1.0.0 and 2.0.0 are not), otherwise you take the second component (i.e. 0.20.0 and 0.20.9999 are compatible, but 0.20.0 and 0.21.0 are not).

And if both the major and the minor version are zero, then you compare the patch version. 0.0.6 and 0.0.8 are considered incompatible.

The official rule from the docs is:

Versions are considered compatible if their left-most non-zero major/minor/patch component is the same.

@eli-schwartz
Copy link
Member

For the record I'm officially NACKing anything that involves teaching meson wrap install -crates-io or whatever, to write meson.build/wrap files that allow building multiple versions of a single crate.

There's a couple reasons that I don't like the idea, but there is absolutely no way at all that I'm even slightly comfortable implementing algorithms for determining whether or not two dependencies are "compatible enough".

This works for glib and similar C/C++ libraries because those projects have gone out of their way to provide robust buildsystem support for avoiding clashes at multiple levels -- shared libraries, command-line tools, man pages, data files, etc. -- and then explicitly marked the dependency name themselves, and made those dependency names non-conflicting by adding manual versioning to the names.

It is not meson's job to know how to make this judgment call. Stop trying so darned hard to convince me that it's not meson's job to know what the rust programming language is1. 😂

Footnotes

  1. It is absolutely meson's job. But discussions like this make me want to burn the entire technology industry to the ground.

@eli-schwartz
Copy link
Member

As long as the dependency is not exposed via public API and you mix the same types of both versions everything's going to work just fine.

So basically how it works in C/C++, gotcha.

And if you do the compiler will reject your code even before linking and tell you that you're mixing two different versions.

So kind of like how it works in C++. Also in C but opt-in via common technology like symbol versioning scripts.

The alternative would be for the whole ecosystem to update to newer versions in lockstep, which simply does not scale. See also why we're still stuck with GLib 2.

I doubt that has the slightest relationship to why we're still "stuck with" glib-2.0. It was, after all, carefully designed to do what rust just magically does for you using heuristics.

Even if that weren't the case, glib still makes it possible to do exactly what C/C++ libraries have done for a long time, and make your code build on both versions, which lets you test that it works on both versions, then let system integrators build a single monolithic stack against whichever version of glib they wish to use.

Rust makes it easy to not support this. 😂

@sdroege
Copy link
Contributor

sdroege commented May 2, 2023

There's a couple reasons that I don't like the idea, but there is absolutely no way at all that I'm even slightly comfortable implementing algorithms for determining whether or not two dependencies are "compatible enough".

The "algorithm" here is simple semver, extended to be stricter for <1.0 versions (semver allows everything there, Rust has the same rules as for >=1.0 shifted by one component). That's enforced by the ecosystem, there's tooling for checking compatibility (for maintainers to make sure their releases don't break the rules), and it's part of dependency resolution.

A tool like proposed in this PR will need to implement that to select the correct versions / release series of a dependency, or else it simply won't work.

You have some level of consistency and know what to expect without learning each project's numbering scheme, just like with e.g. npm. Which is also why it actually works as part of tooling and doesn't require workarounds like including the API version in the name.

This works for glib and similar C/C++ libraries because those projects have gone out of their way to provide robust buildsystem support for avoiding clashes at multiple levels

Like you said multiple times yourself, "So basically how it works in C/C++". Just that it's built-in instead of constructed on top and handled slightly differently by different projects.

I doubt that has the slightest relationship to why we're still "stuck with" glib-2.0. It was, after all, carefully designed

Except that it was not (it's parallel installable though). For GLib/GObject at the API level (it's going to be part of your public API so you can't mix two versions in the same build target), for anything on top of GObject also at the ABI level even if they wanted to (GObject type names for example, see e.g. libsoup).

But we're digressing.

It was, after all, carefully designed to do what rust just magically does for you using heuristics.

I'm not sure what magic you're talking about. There's no magic but very simple rules consistently used by the whole ecosystem. If there was magic (aka complex rules and heuristics) then this wouldn't actually work in practice.

I have the feeling we're talking about completely different things, and you're talking about something unrelated to the problem at hand in this PR but I'm not sure what exactly.

@eli-schwartz
Copy link
Member

As you pointed out above, rust does not make this work, it just promises to make it a compiler error if you mess up. Same thing C++ promises.

I don't know that this can actually be guaranteed 100% though, probably only 99.9%, because one dependency "A" could be directly depending on "C" 1.x and making decisions based on it, and passing those decisions as booleans upward to projfoo, which then calls into dependency "B" that utilizes those decisions against "C" 2.x. Not every compatibility break is a type violation. Dirtying the tree upwards is actually just saying "resolve a single consistent version everywhere".

I mislike the idea of meson creating new autogenerated wraps that perform version suffixing and relax the usual rules which make this sort of thing a configure time error rather than a build time or runtime error, and I certainly mislike the idea of changing the subproject dependency resolution algorithm to make this work the way rust does under the hood. It's just asking for trouble. If project maintainers wish to manually rename their dependencies, they can do that for C also, and this will work with rust exactly as you wish.

@xclaesse
Copy link
Member Author

xclaesse commented May 2, 2023

I certainly mislike the idea of changing the subproject dependency resolution algorithm to make this work the way rust does under the hood.

That's not the idea here. What I think would make sense is to generate gstreamer-0.20-rs.wrap file that provides gstreamer-0.20-rs dependency name. As far as meson is concerned, having multiple versions makes completely different subprojects that have different names. Just like we could have gtk3.wrap and gtk4.wrap.

The only potential issue is with installed files, if the crate is not made to also add version into installed library/executable names. But that's something we can do automatically in cargo subprojects. ATM it does not install any file anyway AFAIK.

@sdroege
Copy link
Contributor

sdroege commented May 2, 2023

That's irrelevant for this PR though, just loosely related. If there are multiple versions of the same crate appearing somewhere (and that will happen), then it's your job to make sure things fit together (i.e. not multiple versions of the same crate being used interchangeably in API as that will give a compiler error). That's not meson's job, and can't be meson's job, but whoever puts together the set of subprojects.

However what has to happen in this PR is

  • we need to define a mapping from Rust's / crates.io's / cargo's dependency namespace to meson's. Which means mapping a (name, API version) tuple to a flat name for the meson dependency. There's one obvious way of doing this by default, but I could imagine that adding a parameter for the user to override this at subproject-import-time could be useful under certain circumstances.

  • handle versions when resolving dependencies recursively when adding subprojects from crates.io. If you want to add a subproject from crates.io, then you probably also want to that subproject's dependencies. That requires the code added in this PR to actually resolve and add a compatible version, which is a matter of following semver version constraints. That doesn't relate to any existing subproject dependency resolution in meson, but requires specific code that handles this as part of the code added here to grab subprojects from crates.io. From that point onwards (the subprojects are in place) meson can handle things the same way as it does now, assuming the first bullet point here is solved correctly.

@xclaesse
Copy link
Member Author

xclaesse commented May 2, 2023

Note that fetching dependencies recursively is a whole other story that I discussed with @thiblahute in PM last week. I'm not sure exactly how that will work.

The problem is making meson wrap update work. If we initially generate all wraps by respecting the semver rules, how do we update them afterward? This PR will just take latest version for them all and that won't respect version constrains, and at that point Meson has no idea which wraps are direct dependencies of the main project and thus should be updated to latest, and which wraps are just transitive dependencies that should take a version that match constrains and not latest.

Deps could come from Cargo.toml or Cargo.lock as git URL, etc.

My idea currently is to only write .wrap files for direct dependencies using this PR. Once Meson configure a cargo subproject it generates PackageDefinition objects (the class that represents a parsed .wrap file) based on info from Cargo.lock. See that as in-memory wraps that have no .wrap file written. This is similar to normal subprojects that have git submodules in their subprojects/ without corresponding wrap file.

@sdroege
Copy link
Contributor

sdroege commented May 2, 2023

Yeah let's worry about that separately, I guess. Handling dependencies recursively could be a future step.

Deps could come from Cargo.toml or Cargo.lock as git URL, etc.

Cargo.lock is not used when resolving dependencies. It's only used to freeze the currently configured selection of versions of dependencies. I don't think there's any place where meson should concern itself with handling Cargo.lock.

@LingMan
Copy link

LingMan commented May 2, 2023

So, I'm not really familiar with meson and thus chances are that this is naive, but why not just let cargo metadata handle the dependency resolution and downloading?
Just give it a manifest (aka Cargo.toml) an it gives you a nice json including everything (I hope?) you should need to handle the actual build (which crate should depend on which versions of which other crates, paths to their Cargo.tomls etc).

I can understand wanting to handle cargo build inside meson, but recreating all this dependency handling sounds like a lot of headache for no gain and with a less ergonomic result. The existing cargo tooling for this is pretty great.

@alyssais
Copy link
Contributor

I tried this out, and I have a concern about the automatic suffixing with "-rs":

When I have a missing dependency, it'll complain about whatever-rs being missing. If I'm not paying attention, I'll try to meson wrap install --crates-io whatever-rs instead of whatever, which opens up a whole new class of candidates for typosquatting.

It's also a bit unintuitive. I understand that sometimes you'd have both C and Rust gstreamer etc. libraries, but that's probably the exception, right? Wouldn't it make more sense to have a way to rename dependencies in case of conflict, rather than renaming all Rust dependencies by default?

@eli-schwartz
Copy link
Member

I'm not sure what you actually mean by "rename dependencies in case of conflict", but keep in mind that the major conflict is probably not between C and rust implementations of the same project, rather, C and rust libraries that both try to do some common thing, and weren't previously concerned about using the same project name to do it with.

@xclaesse
Copy link
Member Author

The problem is Cargo is its own namespace for project names. Unlike the rest of the world that needs to make sure to have a unique e.g. pkg-config name, crates only have to be unique within crates.io. It does not seems to be the exception, pretty much every C library wrapped in Rust have a potential name conflict.

The problem becomes more important with #11856 where I also use the -rs suffix convention for Cargo dependencies.

Note that I use -rs suffix because that's what @sdroege used for his experiment there: https://gitlab.freedesktop.org/slomo/gst-plugin-meson-test. I personally like it, but we could also decide another prefix/suffix I guess.

If I'm not paying attention, I'll try to meson wrap install --crates-io whatever-rs instead of whatever

We could easily allow both. To be fair, I'm not completely sure we want meson wrap install --crates-io CLI anyway. There could be better ideas, such as not requiring wraps at all and make dependency('syn', version : '>=0.50', method : 'crates-io') automatically pull from crates.io as suggested there: #11856 (comment). TBD.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants