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

New 2.0 lockfiles #34

Merged
merged 5 commits into from
Jun 7, 2022
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 237 additions & 0 deletions design/034-new_lockfiles.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# Proposal: New Conan 2.0 lockfiles

| **Status** | |
|:------------------|:--------------------------------------------------|
| **RFC #** | [034](https://github.com/conan-io/tribe/pull/34) |
| **Submitted** | 2022-04-26 |
| **Tribe votes** | |


## Summary

Conan 2.0 will implement completely new lockfiles, with a new proposal of format, definition, behavior, usage and flow, based on the following principles:

- Lockfiles files will be json files containing only three lists of ordered references. One list for “host” requires, another list for “build” requires, another list for “python” requires.
- Lockfiles will no longer contain information about profiles, settings, options (as they did in 1.X)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know that profiles can be distributed with conan config install, but it is not always obvious which profiles or overrides were used in the build.

E.g. now a CI job produces lockfile as an artifact which can be consumed later. Especially in "testing" or "deploy" jobs which call conan lockfile install. This information will be unavailable, right?

Basically, what I'm looking for is a way to avoid discrepancy between which environment was used to build and to consume. Conan already have composable profiles and overrides via settings and options, and in our CI more overrides can come via environment variables or extra job parameters (e.g. if one wants an experimental build with specific settings).

I suspect is would be possible to implement this on top of Conan with custom CI scripts (e.g. create a profile file by aggregating data which otherwise would have been passed on the command line and pass only it as a single --profile argument and export as a CI job artifact), but maybe it's worth doing similar thing right in Conan. Like a "profile lock" which (if specified) would complement dependencies' lockfiles and have similar strict checks as with current lockfiles.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am happy to see I am not the only one who is concerned about preserving profile information
(what I did and do so far via having them in the lockfile, and that correct and for free)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also second the need to be able to capture/obtain the full settings/options context in order to be able to reconstruct the environment from binary package creation time.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually realized one more use case for that: sometimes we use same profile name which is specific to particular docker image. Similar to preparing image with a compiler and then running conan profile new default --detect. Then we build using that image and profile.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say one of the most advantages of the lock files proposal is to have decoupled "locked versions/revisions" from the "configurations", managing a CI flow for building a project is way simpler and more consistent. Preserving the information from the applied lock file can be done separately, I'll open a new issue (2.X) to discuss the ways to do it, some alternatives could be (just to clarify what I mean):

With a different command:
conan profile compose --profile:h=foo -s:h arch=x86 -c myconf=1 --profile_out:h=profile_host_windows_x86.txt

Directly with arguments:
conan lock create . --profile:h=foo -s:h arch=x86 -c myconf=1 --profile_out:h=profile_host_windows_x86.txt

To reproduce the exact build you only need to save a couple of files, I think it is doable, safe, and worth it to decouple it.

- Lockfiles will not contain information about the dependency graph, only ordered lists (ordered by version and revision timestamp) of package references.
- The default level of locking will be locking down to the recipe reference, that is, including the version and the recipe revision (``pkg/version@user/channel#recipe_revision``), but not the package-id nor the package-revision. This is aligned with the previously accepted Tribe proposal of removing the [``package_revision_mode``](https://github.com/conan-io/tribe/pull/30). Even if implementing lockfiles locking down the package revision will be possible, that will be considered the exception, and the main flows, documentation and behavior will be optimized for locking down to the recipe revision.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think leaving the package_id out of the lockfiles opens the doors for unwanted behaviors.
We use the lockfiles to achieve reproducible builds, particularly, to "ignore" new packages pushed to our repo unless we explicitly ask for them by updating our lockfiles.
Consider lib_a, which has a neon option set to False by default. If someone builds this package with neon=True and pushes it to our repo then our builds will start using a completely different package. Currently the lockfiles prevent this since the package_id is of each package is taken into account.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the current 1.X implementation doesn't substitute the package_id, it is also computed, and then validated.
There is a mode in the proof of concept that locks down to the package_revision, that includes the package_id. In the proof of concept it is only used to lock the package_revision, which is the real moving part, but I guess that validation of the package_id might be possible too, it seems adding "strict" check there is doable.

Consider lib_a, which has a neon option set to False by default. If someone builds this package with neon=True and pushes it to our repo then our builds will start using a completely different package. Currently the lockfiles prevent this since the package_id is of each package is taken into account.

In your case, someone creating a package with neon=True and pushing it to the repo, will not make it being used by consumers, unless consumers and profiles explicitly change to neon=True too. I think for the vast majority of cases this won't be an issue in practice. In the very extreme case, it would be possible to store a copy of the original profiles together with the lockfile, to be robust against possible future changes in the profiles.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I work with @ericriff FYI)

I think forcing the whole set of packages used to be locked to recipe_revision level addresses this.

We have had issues in the past where someone updates a package recipe (but not the version) and then builds that with a different option, which breaks building of older versions of our application. In our top-level application conanfile.py we specify all packages down to recipe_revision, but inter-package dependencies do not have a recipe_revision.

For example:

app/1.0 -> pkga/0.1@user/channel#RREV1 -> pkgb/0.1@user/channel. In this case app/1.0 specifies the full rrev of pkga, and pulls in pkgb implicitly with its default options (IE neon=False). Another app developer wants to use pkgb with neon enabled, but also fixes a minor issue in the recipe of pkgb when doing so. That developer also updates pkga to use pkgb with the neon=True option, and generates at least a new rrev if not also a new version (either way does not change this example). When we build our packages, we build the most recent version of all recipes and upload the binaries to our artifactory conan server.

We now have binary packages pkgb/0.1@user/channel#RREV1 with neon=False and pkgb/0.1@user/channel/RREV2 with neon=True.

The developer updates the conanfile for app/1.1 to build with pkga/0.1@user/channel#RREV2, and builds of that are happy. However, now builds of app/1.0 fail: when resolving pkgb it resolves to the most recent rrev of pkgb/0.1@user/channel#RREV2, but there is no binary of that package with neon=FALSE which is what pkga/0.1@user/channel/RREV1 requires.

We have many developers working on parallel on app from branches of various ages including release branches a few months ago, developer branches made a week or two ago, and bleeding-edge builds. Suddenly having a large portion of our developer community unable to build older code because we updated a second-order dependent conan package caused us a few big headaches, and turning on lockfiles for the app fixed that issue.

I think this current plan of simply locking all direct and indirect references down to recipe_revision is sufficient to address this issue for us, but I wanted to describe it in detail so we could all consider it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is my understanding. Lockfiles capture the direct and transitive dependencies, so it doesn't matter if some other teams create a new recipe revision that only builds with a certain option, because that will not impact at all, that new revision will not be resolved by the first developer/team.

A different discussion of course is whether building only a subset of binaries, for example, only neon=True for RREV2, and not neon=False, knowing that it will disrupt developers that might upgrade to get the latest RREV2 recipe improvements. In principle, I would tend to favor the creating of binaries when source changes. If recipe gets a new RREV2, that should be built and green for the different configurations, or otherwise rejected. Because the thing is that it is not only that there is no pre-compiled binary for neon=False, but that change might completely break that option, and when downstream consumers don't find the binary and do --build=missing that build will fail without solution, that is problematic.

But I know it is a balance or trade-off between different things, so in any case, I think it will be good with the lockfiles always locking the full dependency graph down to the recipe revision.

- Locking by default will be non-strict, that is, if some ``requires`` cannot find a matching locked version in the lockfile, it will be resolved normally. A ``--lockfile-strict`` mode will be implemented, but not the default, to enforce finding a match for declared requires in the lockfile or failing otherwise.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand this one. Isn't the whole purpose of lockfiles to be strict and prevent any changes such as overrides?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strict enforcement of the lock is most likely the single most reported issue in our support, and the origin of many frustrations. Recall that many operations imply in many occasions that it cannot be strict:

  • Capturing a lockfile for every configuration after the first one, when the first lockfile from the first configuration is provided, to guarantee it is as consistent as possible between configurations. So if we have to capture a lockfile for N configurations, that means that N-1 conan lock create commands will have to add --lockfile-no-strict
  • Every change that every developer does to a package, can potentially change the dependencies. Add a new one, update the version of a existing one, etc. This is a very common operation in the day-to-day. This also requires --lockfile-no-strict otherwise each developer change to requires will raise an error.
  • Application of a lockfile created with conan lock add to lock only some of the dependencies or transitive dependencies will also require --lockfile-no-strict, always, by definition.
  • The application of a partial lockfile, like the one used to build the changes done to a package in the middle of the graph, when we want to apply it to a downstream consumer application, will also require --lockfile-no-strict, always, by definition

One of the first implementation we had was the other way around, strict by default. When we realized that we were basically either saying strict=False in code (for some commands), or passing --lockfile-no-strict in the command line for the majority of command lines we had in our testing and examples (note that we have been working in more elaborte examples like conan-io/examples2#7, to further learn and evaluate this proposal), we decided to change the default.

Note that being non-strict doesn't mean that it will not lock things correctly. Whatever was locked will be correctly locked, unless the developer explicitly wanted to change dependencies outside of that lock. Lockfiles will prevent against any changes in the dependencies, if new versions or revisions are published. They never intended to be (and they are not in other language package managers) a mechanism to block developers to do modifications to source and recipes, and this was probably the root source of the above mentioned frustrations.

Copy link

@ericriff ericriff Apr 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My intuition tells me that lockfiles should be honored 100% of the times to ensure reproducible builds.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly:

  • If a package is referenced in the conanfile (or in some dependency in the graph) and something that matches that portion of the reference is found in the lockfile, the behavior is the same in strict and non-strict mode: the package used will match what is in the lockfile. In other words, for packages found in the lockfile, the lockfile is respected.
  • If a package is referenced in the conanfile (or in some dependency in the graph) and something that matches that portion of the reference is NOT found in the lockfile, in strict mode the operation will fail, and in non-strict mode it will fall back to using normal conan package lookup mechanisms.

Is this a correct interpretation @memsharded ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, perfect interpretation.

And this is the reason why at the moment we have proposed to be "non-strict" by default. If there is something in the conanfile that is not in the lockfile is because the users did a change to it, or is using just a partial lockfile or something like that, that is, because the user want that. Lockfiles are not a mechanism to prevent users doing changes to the graph, is a mechanism to avoid new published versions or revisions to affect users that didn't change their conanfiles, without them being able to control or block that.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In light of that, would a conditional default for lockfile strictness make sense? For example:

  • If --lockfile-out is on the command line, default to non-strict mode.
  • If --lockfile is on the command line without --lockfile-out, default to strict mode.

Specifying --lockfile-out indicates that the user wants to generate a lockfile with new information, so non-struct makes sense. If no new lockfile is being generated, then strict may be a sane default as it is only consuming a lockfile and not writing a new one.

- A single lockfile file can lock multiple configurations, for different OS, compilers, build-types, etc., as long as they belong to the same graph. Lockfiles can be constructed for these multiple configurations incrementally, or they can be merged later. The lockfiles will still not contain information about settings, options or profiles for those different configurations, but they will be able to contain different locked ``requires`` for the different configurations. The concept of “lockfile bundles” will no longer be necessary
- Lockfiles will not be a version definition mechanism, they need to be a “realizable” snapshot of a dependency graph that should be able to be evaluated in the first place. However, they will allow their usage to define overrides or definitions of versions or recipe revisions that will be used if they fit in the valid definition of the original ``requires`` (that is, if the version fits in the version range of the recipe ``requires``, or always for recipe revisions)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, basically, this is then the same as raw conanfile that does not use version ranges?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it will at least also include the recipe_revision, which is very rarely included explicitly in conanfiles.
In this specific point, lockfiles don't deviate much of what they already do in 1.X. They are still "snapshots" of a real dependency graph, a specific instance in time of the recipes requires resolved to some specific values. But they cannot define a reality that cannot be produced by recipes in the first place.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you be a bit more explicit on what a version definition mechanism means?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that means, that you cannot have a conanfile.txt containing a [requires] dep/0.1, then capture a lockfile for it, then edit the lockfile to contain dep/0.2, apply it to conanfile.txt and expect that it will resolve to dep/0.2.

Lockfiles cannot be use to define a dependency, they can only be used to lock the moving or missing pieces of a requires to a fixed value. In most cases that means a lockfile can pin an exact version of a dependency in a given version range, and a lockfile can pin a recipe revision. But the definition of the dependency in conanfile requires should always be respected.

- Lockfiles will be allowed to evolve and adding new information to them easily, at ``conan install``, ``create``, ``graph``, and ``export`` operations, specifying a ``--lockfile-out=newlockfile`` argument. That will allow evolving lockfiles when changes are done to the graph, while keeping control on those changes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this. Currently we delete and recreate the lockfiles each time we need to change 1 (or more dependencies). I'd like them to be able to change organically. poetry (python package tool) does a great job with this.
We maintain a huge, ever growing C++ project which uses conan only to consume its third party dependencies. We have lockfiles to ensure the builds are 100% reproducible, now in the past and in the future. What I mean is, we expect to be able to checkout a 6 months old commit, and as a consequence have a 6 months old lockfile and then be able to build that commit using the exact snapshop of dependencies we used at that point in time.
The problem we currently have is that when we're developing a new feature and add a new dependency or modify an existing one on our conanfile then we need to nuke our lockfiles and create new ones. As a consequence we update many packages from the lockfiles and not only the one we want (usually our conan remote moves faster than out codebase). I'd like to see a command that allow us to partially update a lockfile, like add a new dependency or change some version without affecting the rest (as much as possible).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is one of the biggest pain points at the moment in 1.X. And a lot of this proposal is intended to reduce or eliminate it.
The basic conan lock create command, can receive a --lockfile=xxxx argument, to use it as a starting point, then produce the output with --lockfile-out. That includes a lockfile from a previous point in time, from a subgraph, or from a different configuration. The resulting output lockfile will be the updated previous one, the things that can be locked from the previous one will be respected, the things that have changed or are new, will be added to the lockfile. To remove legacy, no longer used dependencies in the lockfile, the --clean argument can be provided too.


## Motivation

Lockfiles in Conan 1.X were born mainly driven by the need of doing distributed builds in CI, and ensuring the same dependencies were used in this build. They achieved this goal, but not without some pain derived from the fact that the dependency resolution algorithm in Conan 1.X is non-deterministic related to the cache state, that means that 2 different packages with the same dependency and the same version range could resolve to different versions, if some other package in between were forcing the download of a new version in that range from a server.

That resulted in the need to store the full dependency graph in the lockfiles, and as the dependency graph is so tightly coupled to configurations, it required to store one lockfile per configuration, which also added the profiles information, as a "convenience", trying to simplify the management a bit. But locking the full dependency graph made things very challenging, because the algorithm needs to fully identify a match for a package inside that graph, before being able to resolve the locked dependencies. And basically making it almost impossible to do “merge” operations over different lockfiles, and to use a partial lockfile from one graph to resolve other dependencies. It also made necessary to have a very strong locking, because losing the consistency of the dependency graph between locked and unlocked parts was unmanageable.

All of that resulted in lockfiles that have been able to partially address some of the related problems, but that in general are complex to use, understand, and implement CI with them.

So, for Conan 2.0, we are proposing a way more “traditional” approach to lockfiles, with the simplifications enumerated above, making Conan lockfiles something closer to what other package managers like NPM can do. There were a few unknowns if this was doable, so for this proposal we wanted to implement it first, check below for details.

## Detailed Design

In the surface, the basic usage of lockfiles hasn’t change much, creating and using a lockfile would be:

Lets say we have conanfile.txt:

```
[requires]
pkg/[>=0.0 <1.0]
```

And existing ``pkg/0.1`` then:

```bash
conan lock create . --lockfile-out=conan.lock
# this will create a lockfile with a “requires” list containing “pkg/0.1”
# no matter if other new version pkg/0.2 in the range is created now
# this will keep resolving to “pkg/0.1”
conan install . --lockfile=conan.lock
```

But many things have changed, lets see them.

### Non-strict vs strict

If we now do a modification to our conanfile.txt to:

```
[requires]
pkg/[>=1.0 <2.0]
```

And assuming a ``pkg/1.1`` version exist, then:

```bash
conan install . --lockfile=conan.lock # will work
```

Will not fail by default, and will be able to resolve to ``pkg1.1``. This is because the default mode is non-strict. One of the most frustrating behaviors of Conan 1.X lockfiles was when lockfiles didn’t allow you to do modifications to your conanfiles and keep working.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This itches me a bit. It means that the final resolved graph will be different than the one written in the conanfile.
If the installation fails here, it would immediately inform the developer that it has got a stale lockfile.

However, another option would be for this to automatically update the lockfile, so at least a diff would be seen during the committing to VCS.

It would be a really bad experience if you think that one state (i.e. the one from the lockfile) will be after issuing conan install ... and you end up with a totally different state, without knowing what happened (i.e. conanfile having precedence over lockfile).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this has been elaborated in the above response: #34 (comment)

It is not possible to get to a different state using a lockfile, and the same starting point, lets say a given conanfile.txt. The different is if the conanfile.txt gets some changes that are no longer resolvable against the lockfile. As this happens to be an operation that happens very often in development, and users were frustrated about it raising an error, we made it non-strict as default. Of course if you want to get guarantees that this doesn't happen, that is the reason why --lockfile-strict is there, which you can always opt-in in CI to enforce things. But annoying users and blocking them to do local changes to their conanfiles was really a support burden.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The non-strict behavior seems fine for development and testing, but if this is the default behavior I'm sure someone from my team will commit changes to the conanfile that "worked for me" but won't push and updated lockfile.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, that makes sense. I understand now the reasoning why the non-strict mode was chosen as the default.

However, I think that in the above example with pkg/1.1 at least the lockfile needs to be updated automatically because without updating it we will end up with lockfile pointing to pkg/0.1, while the actual resolved package will be pkg/1.1. In this particular case, the conanfile and lockfile are incompatible (conanfile requires range [>=1.0 <2.0], and lockfile points to version 0.1) - this needs to be resolved somehow - either by warning the developer about the compatibility (maybe it does not need to be a hard error, like in strict mode) or by updating the lockfile so that the change would be visible in the lockfile diff when committing. It feels weird that conanfile takes precedence over locfile in cases like this.

I agree that CI could always use the strict mode, but it would be even better if the developer could be informed about the incompatibility between the changed conanfile and the existing lockfile as soon as possible.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To ensure I understand correctly, if instead the conanfile was updated to:

 [requires]
 pkg/[>=0.0 <2.0]

Would your example command work and build with pkg/0.1 as described in the lockfile even if pkg/1.1 existed?

 conan install . --lockfile=conan.lock  # will work

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly. It will still lock to pkg/0.1.


If we do:

```bash
conan install . --lockfile=conan.lock --lockfile-strict # will not work
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I argue that the strict mode should be the default and give non-strict option to developers. See my previous comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it makes sense to allow the default to be set in conan.conf, so this can be propagated using existing using conan configuration synchronization mechanisms and have a sane default for different use cases?

For a group actively developing a large set of conan packages and using a complex CI mechanism with a lot of package changes being made by experts in Conan, non-strict is a great default.

On the other hand, if a group is primarily developing a large complex application that uses conan packages but most developers are not conan experts, strict is a better default.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think the criteria is the size of the application. Our rationale is more on the line of UX:

  • By default don't annoy developers being strict, or forcing them to add --lockfile-no-strict, or to configure something different with a conf that will not match what is done in the servers (recall that conf can be synced and installed with conan config install, in theory to match what the CI is doing). Let them do the changes that they want. In any case they don't have publish permissions to the servers, only CI should be able to upload packages. They won't break anything for not being stricts.
  • The CI is scripted. They know well which operations they want to be strict and what other can be not-strict, depending on their flow and process definition. As it seems the majority of lockfile operations would require non-strict for the most common flows (see above New 2.0 lockfiles #34 (comment)), we have favored the non-strict.

```

It will throw an error saying that the requirement ``pkg/[>=1.0 <2.0]`` cannot be found in the lockfile

### Multi-configuration lockfiles

If we have one conanfile like this:

```python
class Pkg(ConanFile):
settings = "os"
def requirements(self):
if self.settings.os == "Windows":
self.requires("win/[>0.0]")
else:
self.requires("nix/[>0.0]")
```
Then it will be possible to capture the lockfile for both Windows and Linux configuration in a single lockfile file, in the following way, assuming there exist packages ``win/0.1`` and ``nix/0.1``:

```bash
conan lock create . --lockfile-out=conan.lock -s os=Windows -s:b os=Windows
# That captures a lockfile with ``win/0.1``
conan lock create . --lockfile=conan.lock --lockfile-out=conan.lock -s os=Linux -s:b os=Linux
# That will augment the conan lock and it will end with both ``win/0.1`` and ``nix/0.1``
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great if it were possible to do both that in a single conan invocation, at least with profiles.

Something like:

conan lock create . --lockfile-out=conan.lock --profile windows-profile --profile linux-profile --profile ios-profile --profile android-arm64-profile --profile emscripten --profile macos-profile ...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be easily implementable with the custom commands and the public API at your will.

Note that your example, for a start is already having a different behavior, as multiple --profile arguments in CLI actually compound and collapse into a single one. So a new syntax would be necessary to define such different profiles. Multiply that by 2 (host and build profiles), and by 3 for (settings, options, conf, that are also part of the profiles), and we have made the conan lock create CLI arguments a nightmare, that would probably not satisfy many users.

But with the custom commands you can perfectly define what you want, what arguments you will be providing, the behavior that you want, and using the Conan Public API, you will be able to code it in relatively few lines (and distribute your custom commands with conan config install too)

```

The later application of the same single resulting “conan.lock” lockfile works for both Windows and Linux configurations:

```bash
conan install . --lockfile=conan.lock -s os=Windows -s:b os=Windows
# resolves to win/0.1, but of course nix/0.1 will not be part of the resulting graph

conan install . --lockfile=conan.lock -s os=Linux -s:b os=Linux
# resolves to nix/0.1, but of course win/0.1 will not be part of the resulting graph
```

This approach remains valid when different configurations resolve to different versions of the same package. If the package ``conanfile.py`` is:

```python
class Pkg(ConanFile):
settings = "os"
def requirements(self):
if self.settings.os == "Windows":
self.requires("dep/0.1")
else:
self.requires("dep/0.2")
```

When the above steps for both Windows and Linux are executed, the resulting lockfile will contain both ``dep/0.1`` and ``dep/0.2``, including the latest revision for each one, but subsequent ``conan install --lockfile`` installations will still be able to resolve to the correct one, down to the locked recipe revision. This is based on the fact that the locked version still needs to match the required one, so when in Windows, it will look in the lockfile for ``dep/0.1``, will find it there, and will obtain the locked recipe revision from the lockfile.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, essentially, this looks like all different conan v1.X lockfiles are now merged together into a single giant lockfile?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the Conan 1.X lockfiles were basically unmergeable, because they contained a graph, with numbered nodes for each package.

Conan 2.0 lockfiles are just lists of package references, merging them yes, is basically merging the lists and re-sorting them, in a largest lockfile.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I get it. This is why I stated above that it looks like a regular conanfile that does not use the version ranges. Of course, it also captures recipe revisions which we still don't use as we find it easier to reason about making a new minor/bugfix version when fixing the recipe - it's more transparent and removes the need for lockfiles which we also didn't need to use.

Yes, this does mean that we have packages like OpenCV/4.5.5.4@company/stable instead of having 4 revisions of OpenCV/4.5.5@company/stable, but it's more transparent and easier to reason about than having recipe revision hash in the conanfile or by using lockfiles.

Of course, since Conan v2.0 lockfiles are so much simpler, it may now have a sense for us to switch to using recipe revisions, but we'll see about that.

All in all, you did great work with the new lockfiles proposal!



### Partial lockfiles

Let's say we have a dependency graph ``app``->``pkgc``->``pkgb``->``pkga``.

We can capture a lockfile called “pkgb.lock”, while we are developing ``pkgb``, doing some changes to it, or building it in CI, that locks the dependencies of ``pkgb``, that is it will lock ``pkga/0.1`` Something like:

```bash
# in the pkgb/conanfile, assuming it ``requires = “pkga/[>0.0]”``
conan lock create . --lockfile-out=pkgb.lock
# this pkgb.lock will contain a locked reference to say ``pkga/0.1``
# we can apply this lockfile locking only ``pkga`` all the way down:
# moving to pkgc repo/folder
conan install . --lockfile=pkgb.lock
# this will lock only pkga/0.1, pkgb is not necessarily locked, we didn’t capture it
# moving to app repo/folder
conan install . --lockfile=pkgb.lock
# this will lock only pkga/0.1, pkgb is not necessarily locked, we didn’t capture it
```

In all the above cases, the lockfile contains ``pkga/0.1``, so when executing the different ``conan install --lockfile=pkgb.lock`` in differents ``pkgc`` or ``app`` repos, it will lock exclusively the ``pkga/0.1``, but not the other dependencies in the graph, as they were not added to the graph (it is possible to add them, it will be shown later, but they were not added in this example).

With this in mind, it is then possible to define manually a lockfile with just one single package reference locked, and apply it later to resolve a full graph. Only the defined locked version will be used, while the rest of the graph will be evaluated as always, without locks.
A ``conan lock add`` command will be provided, so it is more convenient to add things to lockfiles.

Furthermore, it will also be possible to define partial references, that is, even if a lockfile by default captures the ``pkg/version@user/channel#recipe-revision`` full recipe reference, it is still possible to define with ``conan lock add`` a given ``version`` without specifying a recipe revision. In that case, when the lockfile is used, it will use the locked version, and it will resolve dynamically (to latest) the missing piece of recipe revision.


### Modifying lockfiles

Let's say we have a dependency graph ``pkgb``->``pkga``.

We can capture first a lockfile for the dependencies of ``pkgb``, while we are developing it. It will contain only the locked version of ``pkga``. If we decide to ``conan create`` the ``pkgb`` using those locked dependencies, we can augment the existing lockfile to include the recently created ``pkgb`` version (and recipe revision) to the same or to a new lockfile:

```bash
# in the pkgb/conanfile, assuming it ``requires = “pkg/[>0.0]”``
conan lock create . --lockfile-out=pkgb.lock
# this pkgb.lock will contain a locked reference to say ``pkga/0.1``
conan create . --lockfile=pkgb.lock --lockfile-out=pkgb.lock
# now the pkgb.lock contains ALSO the locked pkgb version
```

We have commented above that a ``conan lock add`` operation will be possible, to manually add a version. In the same spirit, it will also be possible to ``conan lock merge`` different lockfiles, as long as those lockfiles belong to the same dependency graph, partially or totally. But they should have shared some common root at some point in time.

These modifications basically add new versions to an existing lockfile, but as such lockfile can be used for different configurations, it doesn’t remove non-used locked references automatically. In order to clean and strip unused references from a lockfile, an explicit ``conan lock create --clean`` creation of a lockfile will be possible.

### Lockfiles in CI

This proposal simplifies the lockfiles and removes some of the commands and functionality that Conan 1.X lockfiles implemented, and that is necessary for implementing CI flows. Conan 2.0 will enable these CI flows with the following functionality:

- ``conan graph build-order`` will output the necessary build-order for a full dependency graph, returning an ordered list of lists, by concurrent-builds levels, in which things need to be built.
- ``conan graph build-order`` will also return the necessary options that every given package in the build-order will need to apply (this information was previously hidden in the lockfile)
- It will be possible to merge different build-orders, even from different products and different dependency graphs with ``conan graph build-order-merge`` which will result in a grouped view, for every package reference, in order, which binaries need to be built.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To merge 2 build orders 2 graphs are needed => 2 recipes. Lockfiles2.0 don't contain graph, do they? And build order is an operation on graph(s) which are no longer in lockfiles.

Or do I miss something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessarily 2 recipes, but maybe the same recipe with 2 configurations, you obtain 2 different dependency graphs, and they can result in different build orders, when conditional dependencies are defined for example.

Yes, lockfiles no longer contains the graph definition, only lists of dependencies.

The key is that the build-order is no longer an operation on the lockfile itself (as it was in 1.X), but on the graph. Exactly the same as the conan graph info command (the current conan info command in 1.X), they build the entire dependency graph, without necessarily fetching the binaries. And the conan graph info and conan graph build-order can operate perfectly without a lockfile, in the same way a conan create or a conan install. The build-order has been nicely decoupled from the lockfile definition, and the responsibilities are better defined now: the responsibility of a lockfile is to lock versions and revisions, when other Conan commands are executed.

Please let me know if this clarifies it.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My main concern was how will UI / CLI for conan graph build-order-merge will look like.
It is clear that for conan graph build-order one needs a complete graph (which is produced from recipe and profiles), I'm just not sure if it is possible to merge 2 lists of lists in a general case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output of conan graph build-order is a list of lists, but it also contains dependencies information, each element in the list (as serialized to the json output file) is like:

{
                "ref": "pkg/0.1#1ac8dd17c0f9f420935abd3b6a8fa032",
                "depends": [
                    "dep/0.1#4d670581ccb765839f2239cc8dff8fbd"
                ],
                "packages": [
                    {
                        "package_id": "486166899301ccd88a8b71715c97eeea5cc3ff2b",
                        'prev': None,
                        'filenames': ["bo1"],
                        "context": "host",
                        "binary": "Build",
                        "options": []
                    }
                ]
            },

And then, multiple build-order files can be merged into a single one (typically resulting in more than one item in the packages section), as long as the depends are respected.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, that makes sense. So in a way you moved graph from lockfile into build-order output. Since the latter needs to be processed by a tool, I don't see many problems with that. Except maybe that previous build-order output could be parsed by simpler regexp, while for that one a complete json parser is needed. So bash users could be unhappy 😁 (or they will have to use tools like jq).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, exactly. The UI was weird in 1.X, in the sense that you really shouldn't need a lockfile in order to compute the build-order of a graph. So it was moved to the right location, conan graph build-order, which can work without lockfiles.

Yes, you are very right regarding the full json output, but from what we have learned from users so far is that such json is being parsed by CI in the vast majority of cases (because the list-of-lists is not that simple to parse with regex either). And in 2.0 for those who want something simpler, it will be possible very easily, for example with a user custom command, you can process the build-order and output exactly in the format that you want to process it, with very few lines of code, and integrated and managed by Conan.


The idea is that all commands work with the same syntax with and without lockfiles. For example the ``conan graph build-order`` commands above can also be used with and without passing a lockfile at all.

The features designed above will enable 2 flows that were challenging, not to say almost impossible in Conan 1.X, in a straightforward way.

Let’s say we have the ``app``->``pkgc``->``pkgb``->``pkga`` dependency graph, everything is at version ``0.1``, with version ranges like ``[>0.0 <1.0]`` and a developer does a change in ``pkgb`` and bumps its version to ``0.2``.

Two flows become possible:

1) Starting from ``pkgb`` with a partial lockfile:

- Capture a lockfile for ``pkgb``, locking ``pkga/0.1``, so it can be used to test all different configurations for ``pkgb/0.2`` without risking a concurrent ``pkga/0.2`` is published during this operation.
- Capture a lockfile for ``pkgb/0.2`` while creating ``pkgb/0.2``, including both ``pkgb/0.2`` and ``pkga/0.1``.
- When ``pkgb`` builds correctly, if we want to test the downstream application, we can now capture a new lockfile for it, just feeding the ``pkgb`` one:

```bash
# in the app repository
conan lock create . --lockfile=pkgb.lock --lockfile-out=app.lock
```

- Now use ``app.lock`` to feed it to ``conan graph build-order`` or to ``conan install`` or to ``conan create --build=missing``.

2) Starting from ``app`` full lockfile:

- Capture the ``app`` product lockfile

```bash
# in the app repository
conan lock create . --lockfile=app.lock
```

- Apply the lockfile for the testing and creation of ``pkgb``, capturing the modified lockfile, to account for the new ``pkgb/0.2``:

```bash
# in the pkgb repository
conan lock create . --lockfile=app.lock --lockfile-out=app.lock
```

- Apply the ``app.lock`` that now includes the new ``pkgb/0.2`` downstream to rebuild ``pkgc`` and ``app`` as needed.

Note that these flows do not require complicated structure or storage of multiple lockfiles. Just 1 single lockfile can be enough for the whole process. If the changes are to be tested for multiple, unrelated products (final consumers), then 1 lockfile for each one might be necessary, but still the complexity will be highly reduced.


## Implementation details

As commented above, the unknowns were many and the uncertainty high, so we implemented this proposal, it is already available in the released alpha.6. It turns, that as the complexity of the lockfiles has been reduced so much, the implementation was also way more straightforward, like one order of magnitude, compared with Conan 1.X lockfiles.

The largest implementation effort had to be done on the preconditions to have this lockfile model, basically:

- To have ordered lists of versions and revisions, it was necessary to attach the revision timestamp to every recipe reference in the whole model. That was a massive change, affecting a very large part of the codebase, and took a while to stabilize.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a new syntax to specifying recipe revisions? Can you describe how this was changed or provide a pointer to that document?

Copy link
Member Author

@memsharded memsharded Apr 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new syntax will probably be only used and visible in lockfiles, where something like pkg/version#recipe_revision%timestamp will be there, to allow the ordering of revisions.

Internally we had to change all the model of references in order to propagate together the timestamp with the revision, just for the lockfiles, but for normal operation of Conan nothing changes. The pkg/version#recipe_revision is still a full, deterministic coordinate of that recipe, it can be used as an input to commands, etc.

This is a great observation, because actually there is a case, the one that does conan lock add specifying a reference including the recipe revision, when other references for exactly the same version with their recipe revision already exist. In that case, it might be necessary to specify the timestamp if we want to define the correct ordering. I am creating a follow up issue for this, because this needs to add some testing, and there could be some UX to polish around this use case.

Update: new issue conan-io/conan#11129

- It was necessary to implement a different graph resolution algorithm, in which the resolution was deterministic, in a way that it should be impossible to resolve to different versions within a range in the same dependency graph, as that would violate the working hypothesis. As the graph resolution algorithm had to be fully re-implemented to implement the Tribe proposal for requirement-traits and package-types, we took it into account.


## Migration plan

The new lockfiles share nothing with the Conan 1.X lockfiles. The files are different, the commands are different, and the flows that they enable are different. It will be necessary to implement new pipelines in CI for these new flows. The aim of this proposal is that these new CI pipelines for Conan 2.0 will be much simpler than those that already implemented then for 1.X.

There is nothing in the recipes that affect lockfiles, so it will not be necessary to modify any ``conanfile`` to account for this proposal, only command line and lockfiles.