-
Notifications
You must be signed in to change notification settings - Fork 425
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
Packaging and resolution (buildTimeOnly dependencies) #816
Comments
cc Yarn peeps @kittens @bestander @cpojer @dxu |
This makes a lot of sense, the big problem with the dedupe of the dependencies during resolution was the transitive dependencies you explain above allowing for potentially tons of different package versions. The approach of adding the I'm wondering if there's a way to automate this. It would be nice if there were some post-processing step during the publishing of the npm package where it just went through and maybe parsed the scripts and checked against the bin's of the dependencies or something to see if they were build time only. That way you wouldn't have to rely on the developer specifying fields, and also would be an offloaded task that you could potentially just run on all existing packages as well if it worked. Also, not really sure what the plans are for yarn and compatibility with the existing registry but it seems like this would force it to start diverging further from npm packages. e.g, why not just have runTimeDependencies and buildTimeDependencies instead of dependencies at that point? |
I'm glad you asked. We discussed allowing a package to mark itself as |
|
I don't believe this is true, but feel free to explain a specific case. |
I think we still need "peerDependencies" here somehow for cases like this:
In this case, reason only produces artifacts with layout of ocaml@4.2 , while if A resolves to ocaml@4.3, it won't understand the artifacts produced by reason. |
Maybe this issue applies more broadly (does it?) but in the case of reason, we want it to parse to a compiler version agnostic format that can work with any ocaml version. I thought that would solve this problem, but maybe it still comes up other places? |
Sorry, my choice of words was misleading here. I meant to say that by adding the new
Does this mean the developer marks itself as buildTimeOnly? or we somehow automatically detect if a package (or dependency) is buildTimeOnly? The former would have the same issue for existing packages, but the latter would be completely fine. |
@dxu My thinking was that a package This allows We have a couple of options for how to best use this information.
I prefer option 2 as it would be much more clear to people who read So what can we do once we classify and validate
Since I preferred option 2 above, it means that |
This is quite old, and some of things is going into the esy package manager. I'm going to close this for now. |
We need to support multiple versions of packages being installed. Supporting multiple packages being installed does not mean we support linking multiple versions of packages into one final executable (that is not even well defined at this point, though it's worth solving eventually).
So what then do we need to support immediately? Well, often, dependencies are only required in order to assist in helping to generate build artifacts that will eventually be linked into the final executable. This build tooling itself is never linked into a final executable. This ends up opening a ton of opportunities to relax the requirement that there be one global version per package.
This is not just something that is unique to
Reason
or compiled languages. Anything with an equivalent "link" step (such as bundling of web resources) would benefit from the same ability to flatten, but relax flattening constraints so that dependencies merely required for building artifacts do not increase the surface area for flattening conflicts.Build time dependencies are a good opportunity to relax flattening requirements because build tooling could have tons of dependencies, all of which might conflict with versions that our app requires at run-time. Since there's no conceptual conflict, we want to allow multiple package versions that are only used at build time (and with no runtime component) to exist on disk, and be used by their dependees in ways we know are always well defined.
Let's assume that
findlib
is powering all of the lookups for "where my dependencies are". We have complete control over constructing whatfindlib
sees while building a particular package, and that's what I'll describe here - how we construct what is visible, to which packages, and at which steps of the build process. Most packages usefindlib
to find packages.rebel
does not by default, but we can change that.Some definitions:
A --> B
meansA
has a "build-time" dependency on another packageB
.A --> B
iff: Some ofB
's artifacts are required in order to generateA
's artifacts (either individual compilation artifacts, or executable artifact). Build-time dependencies are not inherently transitive, meaning just becauseA
has a build time dependency onB
it does not mean thatA
has any kind of of dependency onB
's dependencies. The intuition is that typically build-time dependencies are dependencies on build utilities such asjenga
/rebel
/make
, and you only care about that immediate dependency's (executable) artifact and have no idea how that executable artifact was created, or what dependencies were needed to generate it.A ==> B
meansA
has a run-time dependency onB
.A ==> B
iff: linkingA
's compilation artifacts into an executable implies thatB
's artifacts must also be linked into that executable. It follows thatA ==> B
&&B ==> C
impliesA ==> C
. run-time dependencies are inherently transitive, but we can further distinguish "immediate run-time dependency" from "transitive run-time dependency" if needed.We start with some constraints:
A
into an executable, requires supplying all ofA
's run-time dependencies (which by definition must be transitively computed).P
, whose artifacts are linked into any single executable.The package manager will ask every package to "build yourself". When asked to do so, we apply a convention which allows multiple versions of a package
P
to, in some cases, be installed in the sandbox, but while maintaining the constraints above. We break the build process into two stages, "compiling" and "linking".A
's means producing its linkable/executable artifacts, and requires runningA
's "compiling step". During the compiling step,A
should only be allowed to "see" artifacts from the subset of run-time dependencies that are immediate, as well as the artifacts of its build-time dependencies (which are by definition always immediate).A
means joining together all of the transitive run-time dependencies ofA
together into one binary. During the linking step,A
should be able to "see" all of the artifacts of its run-time dependencies (which means transitively) and the artifacts of its build-time dependencies (which are by definition always immediate). Not every package will want to produce an executable, so the linking portion of the build step may be skipped.dependencies
is considered a run-time==>
dependency.A
's dependency onB
as build-time-->
only, add the name"B"
toA
'spackage.json
'sbuildTimeOnly
field. This preventsB
's artifacts (andB
's transitive artifacts) from being visible to packageZ
's linking step (supposing thatZ
has a run-time dependency onA
).Here's how this solves various problems we encounter:
flappy-bird ==> yojson-1.0.0
(run-time) andflappy-bird
produces an executable. There cannot be two versions ofyojson
linked into the finalflappy-bird
executable. However, that doesn't mean that there can't be two versions ofyojson
existing inside the package sandbox, and being used at other times in the whole build process.For example, suppose
flappy-bird ==> yojson-1.0.0
(run-time) andflappy-bird --> rebel
becauseflappy-bird
includedrebel
in itsbuildTimeOnly
list (this makes sense becauseflappy-bird
just needs therebel
binary to start its build process). Then suppose thatrebel ==> yojson-1.2
(run-time). Normally, if our package manager had to resolve everything to a single version per package we'd be out of luck because bothyojson-1.0.0
andyojson-1.2.0
are both needed at some point. But in this case, there is no real conflict becauserebel
needsyojson-1.2.0
at its run-time, in order to generate a single executable artifactrebel
.flappy-bird
then depends on thatrebel
artifact in order to build, but does not require thatrebel
artifact at run time. Furthermore, since build-time dependencies aren't transitive, whild buildingflappy-bird
seesrebel
, and its version ofyojson-1.0.0
, and does not see any ofrebel
's dependencies.flappy-bird
depends onreason-1.0.0
, and suppose thatyojson-1.0.0
depends onreason-1.2.0
. Reason introduces no runtime artifacts that are linked into the final executable soflappy-bird
andyojson
both mark theirreason
asbuildTimeOnly
depenency.flappy-bird
only needsreason-1.0.0
so that it can use the binaryrefmt-1.0.0
at build time, andyojson
only depends onreason-1.2.0
so that it can use therefmt-1.2.0
at build time.(
devDependencies
have nothing to do with any of this - they are useless to us and don't do what we want at all).(
peerDependencies
could have been useful butnpm3
completely changed their behavior so let's ignore them).(
yarn
's--flat
installation is not what we want/need here, because we want to allow multiple versions to exist, but we wish thatyarn
had a--flatten-runtime
option which would flatten runtime-dependencies, but allow multiple versions ofbuildTimeOnly
dependencies. For now, we can just live withnpm
installing multiple versions for everything and then flatten after the fact. Hopefullyyarn
can provide better support here).@yunxing @vramana
The text was updated successfully, but these errors were encountered: