-
Notifications
You must be signed in to change notification settings - Fork 30
CEP XXXX: Improving dependency export infrastructure #129
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
base: main
Are you sure you want to change the base?
Conversation
|
pre-commit.ci autofix |
for more information, see https://pre-commit.ci
|
pre-commit.ci autofix |
|
pre-commit.ci autofix |
Wolf mentioned in private that we don't necessarily have to go to a v2 schema over this, because despite being renamed semantically, the new keys would just be extending the v1 schema, not breaking it. Of course, we'd then have to mandate mutual exclusivity between The same approach (consider the new keys if present, error if not mutually exclusive with the old way) could even by used by conda-build to support the CEP1, which would be great because a lot of our compiler feedstocks that would need this the most are not necessarily ready to be ported to v1 yet. :) If people are in agreement over this approach, I can try to write up specification for it. In any case, I think this is in a good enough state to ask for a first round of feedback; I'd be very curious to hear the thoughts of Footnotes
|
|
I had another think on this and while this could be solved as conditional dependency, it could still be listed under Or we could normalize this in rattler build and turn these "exports" into conditional dependencies :) |
Although my first reaction was somewhat reserved, I now think that conditional dependencies are probably a better fit for solving the use-cases that the |
We could add the following condition.
i.e. we need to have This would make us not have to run multiple solves. |
Once the upstream package So I don't see how that would solve the "avoid build constraints at runtime" concern that was the motivation for the transitive exports. As I wrote above, I think this kind of thing would be solved more cleanly with conditional dependencies, i.e. |
No it doesn't come along in
I don't understand this. Can you elaborate? |
|
Currently run exports are only included from packages on which the recipe directly depends, so not for transitive dependencies but if I read the above discussion correctly we have to make an exception for |
Can we agree that for an output requirements:
run:
- foo >=2
# the below is not relevant for the question I'm asking
exports:
build_to_build:
- foo >=2spec, the requirement Because you seem to be saying that's not the case, which goes against everything I've understood about how conda works.
I agree that it's a valuable usecase, which is why I originally included it here. But I now think that exports are the wrong tool for this, and that conditional dependencies would be a better fit. This is because exports are fundamentally about cross-environment interaction. If you want to use it for intra-environment injections it massively complicates the job for the solver, the implementation, and the design (to find a reasonable way to control this). In contrast, if you take conditional dependencies (syntax only for illustration, not intended as a concrete proposal), this works more naturally IMO # output foo
requirements:
host:
- bar-devel
run:
# regular run-export from bar
# - bar
# additional requirement when compiling against foo; same effect as host_to_host
- ${{ pin_compatible("bar-devel") }}; if env.HOST
exports:
host_to_run:
- ${{ pin_compatible("foo") }}
Unless someone unexpectedly still comes up with a magic fix for the issues that have been identified with shoehorning this use-case into
The way I think about the example you gave is that foo has an additional constraint (on the version of bar) that's only relevant at compile time. We don't want it to be present at runtime, because then the version of the bar headers is irrelevant and we don't want pointless conflicts. What I was saying is that I don't see how your suggestion to match |
|
I don't know if conditional dependencies will be faster for the solver. After all it's NP-hard so three solves might not be slower than one with more constraints (to be fair I'm unsure about it either way). Also for most recipe that need this, solving time pales in comparison to compile time. Another thing about the doing it with conditional dependencies do end up in the final package dependencies. Looking at the recipe I don't think conditional dependencies do a good job at understanding what they are for. I also think being conditional on an environment variable is fragile (although that could be changed for some static key passed to the solver that is only set to true by conda/rattler passing it to the solver). |
The two features are not interdependent. This CEP stands alone and solves an important issue (host-exports; which is the reason the flang migration has been stuck for almost a year, dealing with C++ modules, etc.). Transitive dependencies can follow later, either through
You don't know which repodata entries to read before solving the environment, and of course different variants of the same package can have very different exports (so you can't just guess in advance). Once you add the So it's not just a question of whether the process can be split into separate solves, it's that the solve results become very fragile to minor details, including how exactly that logic is implemented. It would be very hard to avoid degenerate corner cases IMO (non-convergence, oscillation, randomly unstable solves, etc.), and at the scale of conda-forge we're almost certain to hit them all.
This is exactly what In any case, as the author of this CEP I'm making the decision to exclude the "transitive exports" use-case. It would have been nice to solve it en passant, but I'm not going to jeopardise solving the problem I set out to do for a bonus extension that's not such a natural fit after all. |
|
I don't know why everyone is talking about conditional dependencies. This is not about adding a dependency to |
Because adding dependencies to the same environment (e.g. from host to host) is 100% equivalent to a dependency that triggers under certain circumstances (like being in host).
This is yet another case from what you've described before (a variant of Please respect my decision that in this CEP, your use-case is out of scope. You can write your own CEP for that (either building on top of this, or competing with mine if you must). |
Sure |
|
I read the whole thread and think the proposed solution is a good addition. (From the perspective of writing recipes.) |
Thanks a lot for the feedback @cbouss, I appreciate you taking the time! That reminds me that I wanted to post a comment1 from a recent discussion on zulip which covers an open point of the design here: how to deal with Concretely, if we have # output: a_complicated_package
requirements:
exports:
build_to_host:
- some_package_with_a_run_exportand # output: some_package_with_a_run_export
requirements:
exports:
host_to_run:
- the_export_in_questionand then consume it # output: mypkg
requirements:
build:
- a_complicated_package
host:
# from a_complicated_package's build_to_host
# - some_package_with_a_run_export
run:
# ...should the run-export of some_package_with_a_run_export get triggered here?!
# - the_export_in_questionthe question is whether, why and how we trigger an export of a package that's not explicitly named in the recipe. Obviously this is quite an impactful question, and we certainly do not want to trigger all exports of packages that happen to transitively make it into an environment. On zulip, I discussed this in the context of how injected exports could/would work. Despite not wanting to cover self-exports in this CEP, I've used the motivating use-case for them as an example here, because it illustrates the situation more concretely than just some abstract arithmetic between environments. As I note at the end, this CEP could go either way (i.e. whether Let's take the clearest case that I'm aware of that's been in contention around self-exports vs. conditional dependencies. I'm rephrasing here for consistent language: In other words, if Let's look at the recipes: # output: libA
requirements:
[...] # regular build: / host: / run:
exports:
host_to_run:
- ${{ pin_compatible("libA") }}That's the easy part, which we already know as # output: libB
requirements:
[...] # regular build:
host:
- libA
run:
# regular run-export from libA
# - libA >={{ver_A}},<{{next_ver_A}}
# additional requirement when compiling against libB; same effect as host_to_host
- ${{ pin_compatible("libA") }}; if env.HOST # how to spell this is TBD!
exports:
host_to_run:
- ${{ pin_compatible("libB") }}Now, whether as a conditional dependency or a # output: mypkg
requirements:
[...] # regular build:
host:
- libB
# injected!
# - libA # matches libA-constraint of libB
run:
# regular run-export from libB
# - libB >={{ver_B}},<{{next_ver_B}}
# run-export from injected libA!
# - libA >={{ver_A}},<{{next_ver_A}}
- some_regular_depIn my mental model, the process is as follows:
The "or injected" part is the most open aspect of the design space, but I believe the example with Coming back to your example [for automatically injecting # mycompiler
requirements:
run:
- ${{ stdlib("c") }}
# conditional dependency when in build, which then participates in run-exports; same as build_to_build
- ${{ stdlib("c") }}; if env.BUILDThis would work with the "or injected" scheme above, but as you can see, it's neither very elegant nor obvious why you'd have to repeat the same dependency for IMO that's because conditional dependencies and self-exports are features that are only truly needed for niche cases; any feature can be misused, so any additional expressivity needs to be guarded (e.g. by the linter etc.). As a consequence, I'm convinced that we shouldn't abuse this mechanism for something which every compiled recipe needs. In other words, This comment is already too long, so I'll just note that a more restricted form of [this PR] without the "or injected" is possible, and that this would already solve a lot of the cases where we need host-exports, e.g. the ABI of C++/Fortran modules. It's only when we get to stuff like the Footnotes
|
One thing that gnawed at me since posting that comment on zulip is that I don't like the relationship "conditional dependencies participate in run-exports", which seems too magical, and is by far not explicit enough in the recipe IMO. However, I just had an idea that might solve this case: we can use # output: libB
requirements:
exports:
host_to_run:
- ${{ pin_compatible("libB") }}
host_to_host:
# implemented not as an export, but as a conditional dependency of libB when it appears in `host:`!
- ${{ pin_compatible("libA") }}That would IMO be the best of both worlds: explicit syntax for the most unusual case, as well as a sane environment resolution process, and conditional dependencies don't have to be imbued with some magical pixie dust (pun intended 😉). This would also simplify the rule I had posited above to "we apply exports from packages that are either explicitly named in the environment, or have been injected via exports This would then give the following dependencies between the different proposals ---
config:
securityLevel: loose
---
flowchart TB
this["this CEP"]-->se["self exports CEP"]
cond["conditional dependencies CEP"]-->se
|
|
Nice that we are finding paths forward! Is this similar to the solution I proposed in #129 (comment), or does it differ in some way? If that’s the case, can you elaborate how it’s different? |
Glad to hear it. It's not for lack of wanting to solve the issue that I had descoped self-exports; now I'm beginning to see a path that allows the various pieces to work together in a non-hacky way (where before I couldn't see it at all).
It's different in that we do not have to add a condition like " I've been getting down into the nitty gritty details at least one level deeper (e.g. @jaimergp opened another rabbit hole under my feet about the mechanics of |
First draft after discussion in #77. Does not contain (much) specification yet, because I'm unsure how to go about changing the schema of v1 recipes (does it need a bump in the schema version, or do we specify build tools must translate between them?), and how to deal with the repodata side of things. This is my first CEP, please excuse my lack of experience with a lot of the underlying details.
Help on these questions would be much appreciated! I decided to write up the design in more comprehensive form than originally in this comment though, in order to hopefully facilitate more effective discussion of how to solve the transition issues posed by the new design.
Closes #77 (eventually)