Repository: conan-io/conan

Issue:
Conan lockfile keeps changing
Every time I run `conan graph lock ...`, the lockfile is re-written. The core content hasn't changed, but hashes are regenerated and sometimes the order of the nodes changes. This makes it an awful pain to store that lockfile in the VCS repository.

Am I doing something wrong? Is there a workflow change I need to make? An option that can be added to lock command? Is this a bug?

- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).


Discussion:
Hi @DavidZemon 

Yes, this is intended as a feature. If some package actually changes during some process (like --build=missing and some dependency is re-built from sources), the lockfile will store those changes, as it is the only way of keeping that information. If for some reason, the original lockfile needs to be maintained, then it is necessary to make a copy of it.

A different issue is that the information of the lockfile is not changing at all, but just the way it is stored (possibly the order of the nodes, which are unordered dict internally), and that is causing those differences, then we should try to improve that. I guess that doing some sort() internally in the serialization to file should be enough. Could you give some details of the differences you are seeing? Maybe pasting here a couple of those diffs? Thanks!
Old file:

```json
{
 "profile_host": "[settings]\narch=armv7hf\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++11\ncompiler.version=7\nos=Linux\nos_build=Linux\nplatform=sitara\n[options]\n[build_requires]\n[env]\n",
 "graph_lock": {
  "nodes": {
   "12962a47-0d3f-11ea-ace0-9941403aca0b": {
    "pref": "linux/4.9.119@wsbu/stable#2df0cc52a6a94aae8049f01f133b1f19:01de722bad232946f9b792f33249c157fb84c24c",
    "options": "install_source=True"
   },
   "12962a48-0d3f-11ea-ace0-9941403aca0b": {
    "pref": "wsbu-docgen/0.1.3@wsbu/stable#d85759233340fc32cc4127c5afeba471:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
    "options": ""
   },
   "12962a49-0d3f-11ea-ace0-9941403aca0b": {
    "pref": "cJSON/1.7.0@wsbu/stable#8015fa7ba346d318e29ffb0ef2ce5b01:f576e5b9d63ca3a96f350e00218d1fba5d292dd4",
    "options": "shared=True\ntest=True"
   },
   "12962a4a-0d3f-11ea-ace0-9941403aca0b": {
    "pref": "esp-idf/3.1.3004@wsbu/stable#bfeacbaf0da7613d75aa164ecfdd5b17:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
    "options": ""
   },
   "12962a4b-0d3f-11ea-ace0-9941403aca0b": {
    "pref": "Catch2/2.6.1@catchorg/stable#0:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
    "options": ""
   },
   "12962a4c-0d3f-11ea-ace0-9941403aca0b": {
    "pref": "googletest/1.8.1@wsbu/stable#76b5d3f2d6d39ba4fb4e2dbb7952c1af:09a5aca59994424cb00fe5de5f037b562a308fef",
    "options": "run_tests=False\nshared=False"
   },
   "12962a4e-0d3f-11ea-ace0-9941403aca0b": {
    "pref": null,
    "options": "cJSON:shared=True\ncJSON:test=True\niodb:build_modsim=True\niodb:public_docs=False\niodb:shared=True\niodb:with_docs=True\nlinux:install_source=True",
    "requires": {
     "cJSON/1.7.0@wsbu/stable#8015fa7ba346d318e29ffb0ef2ce5b01": "12962a49-0d3f-11ea-ace0-9941403aca0b",
     "esp-idf/3.1.3004@wsbu/stable#bfeacbaf0da7613d75aa164ecfdd5b17": "12962a4a-0d3f-11ea-ace0-9941403aca0b",
     "iodb/2.3.1@wsbu/stable#5481b8d7a83e2c08c0c507e6151499af": "12962a4d-0d3f-11ea-ace0-9941403aca0b",
     "linux/4.9.119@wsbu/stable#2df0cc52a6a94aae8049f01f133b1f19": "12962a47-0d3f-11ea-ace0-9941403aca0b"
    },
    "path": "/home/david/reusable/Documents/Programming/RedLion/base/.cache/conan/s5t/conanfile.txt"
   },
   "12962a4d-0d3f-11ea-ace0-9941403aca0b": {
    "pref": "iodb/2.3.1@wsbu/stable#5481b8d7a83e2c08c0c507e6151499af:fb7050f1c2ebbdccd6142c43cdd4ddcaa44891ca",
    "options": "build_modsim=True\npublic_docs=False\nshared=True\nwith_docs=True",
    "requires": {
     "Catch2/2.6.1@catchorg/stable": "12962a4b-0d3f-11ea-ace0-9941403aca0b",
     "googletest/1.8.1@wsbu/stable": "12962a4c-0d3f-11ea-ace0-9941403aca0b",
     "wsbu-docgen/0.1.3@wsbu/stable": "12962a48-0d3f-11ea-ace0-9941403aca0b"
    }
   }
  }
 },
 "version": "0.1"
}
```

New file

```json
{
 "profile_host": "[settings]\narch=armv7hf\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++11\ncompiler.version=7\nos=Linux\nos_build=Linux\nplatform=sitara\n[options]\n[build_requires]\n[env]\n",
 "graph_lock": {
  "nodes": {
   "71e48849-0fac-11ea-8943-a9b58d6911e8": {
    "pref": "linux/4.9.119@wsbu/stable#2df0cc52a6a94aae8049f01f133b1f19:01de722bad232946f9b792f33249c157fb84c24c",
    "options": "install_source=True"
   },
   "71e4884d-0fac-11ea-8943-a9b58d6911e8": {
    "pref": "iodb/2.3.1@wsbu/stable#5481b8d7a83e2c08c0c507e6151499af:fb7050f1c2ebbdccd6142c43cdd4ddcaa44891ca",
    "options": "build_modsim=True\npublic_docs=False\nshared=True\nwith_docs=True",
    "requires": {
     "Catch2/2.6.1@catchorg/stable": "71e4884a-0fac-11ea-8943-a9b58d6911e8",
     "googletest/1.8.1@wsbu/stable": "71e4884b-0fac-11ea-8943-a9b58d6911e8",
     "wsbu-docgen/0.1.3@wsbu/stable": "71e4884c-0fac-11ea-8943-a9b58d6911e8"
    }
   },
   "71e4884e-0fac-11ea-8943-a9b58d6911e8": {
    "pref": "cJSON/1.7.0@wsbu/stable#8015fa7ba346d318e29ffb0ef2ce5b01:f576e5b9d63ca3a96f350e00218d1fba5d292dd4",
    "options": "shared=True\ntest=True"
   },
   "71e4884b-0fac-11ea-8943-a9b58d6911e8": {
    "pref": "googletest/1.8.1@wsbu/stable#76b5d3f2d6d39ba4fb4e2dbb7952c1af:09a5aca59994424cb00fe5de5f037b562a308fef",
    "options": "run_tests=False\nshared=False"
   },
   "71e48850-0fac-11ea-8943-a9b58d6911e8": {
    "pref": null,
    "options": "cJSON:shared=True\ncJSON:test=True\niodb:build_modsim=True\niodb:public_docs=False\niodb:shared=True\niodb:with_docs=True\nlinux:install_source=True",
    "requires": {
     "cJSON/1.7.0@wsbu/stable#8015fa7ba346d318e29ffb0ef2ce5b01": "71e4884e-0fac-11ea-8943-a9b58d6911e8",
     "esp-idf/3.1.3004@wsbu/stable#bfeacbaf0da7613d75aa164ecfdd5b17": "71e4884f-0fac-11ea-8943-a9b58d6911e8",
     "iodb/2.3.1@wsbu/stable#5481b8d7a83e2c08c0c507e6151499af": "71e4884d-0fac-11ea-8943-a9b58d6911e8",
     "linux/4.9.119@wsbu/stable#2df0cc52a6a94aae8049f01f133b1f19": "71e48849-0fac-11ea-8943-a9b58d6911e8"
    },
    "path": "/home/david/reusable/Documents/Code/base/.cache/conan/s5t/conanfile.txt"
   },
   "71e4884f-0fac-11ea-8943-a9b58d6911e8": {
    "pref": "esp-idf/3.1.3004@wsbu/stable#bfeacbaf0da7613d75aa164ecfdd5b17:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
    "options": ""
   },
   "71e4884a-0fac-11ea-8943-a9b58d6911e8": {
    "pref": "Catch2/2.6.1@catchorg/stable#0:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
    "options": ""
   },
   "71e4884c-0fac-11ea-8943-a9b58d6911e8": {
    "pref": "wsbu-docgen/0.1.3@wsbu/stable#d85759233340fc32cc4127c5afeba471:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
    "options": ""
   }
  }
 },
 "version": "0.1"
}
```

Diff

```diff
Index: targets/s5t/conan/conan.lock
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
--- targets/s5t/conan/conan.lock	(revision bb62e9952c9fc21e7a7a73a7bd0f896eb40893bc)
+++ targets/s5t/conan/conan.lock	(date 1574704390791)
@@ -2,49 +2,49 @@
  "profile_host": "[settings]\narch=armv7hf\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++11\ncompiler.version=7\nos=Linux\nos_build=Linux\nplatform=sitara\n[options]\n[build_requires]\n[env]\n",
  "graph_lock": {
   "nodes": {
-   "12962a47-0d3f-11ea-ace0-9941403aca0b": {
+   "71e48849-0fac-11ea-8943-a9b58d6911e8": {
     "pref": "linux/4.9.119@wsbu/stable#2df0cc52a6a94aae8049f01f133b1f19:01de722bad232946f9b792f33249c157fb84c24c",
     "options": "install_source=True"
    },
-   "12962a48-0d3f-11ea-ace0-9941403aca0b": {
-    "pref": "wsbu-docgen/0.1.3@wsbu/stable#d85759233340fc32cc4127c5afeba471:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
-    "options": ""
+   "71e4884d-0fac-11ea-8943-a9b58d6911e8": {
+    "pref": "iodb/2.3.1@wsbu/stable#5481b8d7a83e2c08c0c507e6151499af:fb7050f1c2ebbdccd6142c43cdd4ddcaa44891ca",
+    "options": "build_modsim=True\npublic_docs=False\nshared=True\nwith_docs=True",
+    "requires": {
+     "Catch2/2.6.1@catchorg/stable": "71e4884a-0fac-11ea-8943-a9b58d6911e8",
+     "googletest/1.8.1@wsbu/stable": "71e4884b-0fac-11ea-8943-a9b58d6911e8",
+     "wsbu-docgen/0.1.3@wsbu/stable": "71e4884c-0fac-11ea-8943-a9b58d6911e8"
+    }
    },
-   "12962a49-0d3f-11ea-ace0-9941403aca0b": {
+   "71e4884e-0fac-11ea-8943-a9b58d6911e8": {
     "pref": "cJSON/1.7.0@wsbu/stable#8015fa7ba346d318e29ffb0ef2ce5b01:f576e5b9d63ca3a96f350e00218d1fba5d292dd4",
     "options": "shared=True\ntest=True"
    },
-   "12962a4a-0d3f-11ea-ace0-9941403aca0b": {
-    "pref": "esp-idf/3.1.3004@wsbu/stable#bfeacbaf0da7613d75aa164ecfdd5b17:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
-    "options": ""
-   },
-   "12962a4b-0d3f-11ea-ace0-9941403aca0b": {
-    "pref": "Catch2/2.6.1@catchorg/stable#0:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
-    "options": ""
-   },
-   "12962a4c-0d3f-11ea-ace0-9941403aca0b": {
+   "71e4884b-0fac-11ea-8943-a9b58d6911e8": {
     "pref": "googletest/1.8.1@wsbu/stable#76b5d3f2d6d39ba4fb4e2dbb7952c1af:09a5aca59994424cb00fe5de5f037b562a308fef",
     "options": "run_tests=False\nshared=False"
    },
-   "12962a4e-0d3f-11ea-ace0-9941403aca0b": {
+   "71e48850-0fac-11ea-8943-a9b58d6911e8": {
     "pref": null,
     "options": "cJSON:shared=True\ncJSON:test=True\niodb:build_modsim=True\niodb:public_docs=False\niodb:shared=True\niodb:with_docs=True\nlinux:install_source=True",
     "requires": {
-     "cJSON/1.7.0@wsbu/stable#8015fa7ba346d318e29ffb0ef2ce5b01": "12962a49-0d3f-11ea-ace0-9941403aca0b",
-     "esp-idf/3.1.3004@wsbu/stable#bfeacbaf0da7613d75aa164ecfdd5b17": "12962a4a-0d3f-11ea-ace0-9941403aca0b",
-     "iodb/2.3.1@wsbu/stable#5481b8d7a83e2c08c0c507e6151499af": "12962a4d-0d3f-11ea-ace0-9941403aca0b",
-     "linux/4.9.119@wsbu/stable#2df0cc52a6a94aae8049f01f133b1f19": "12962a47-0d3f-11ea-ace0-9941403aca0b"
+     "cJSON/1.7.0@wsbu/stable#8015fa7ba346d318e29ffb0ef2ce5b01": "71e4884e-0fac-11ea-8943-a9b58d6911e8",
+     "esp-idf/3.1.3004@wsbu/stable#bfeacbaf0da7613d75aa164ecfdd5b17": "71e4884f-0fac-11ea-8943-a9b58d6911e8",
+     "iodb/2.3.1@wsbu/stable#5481b8d7a83e2c08c0c507e6151499af": "71e4884d-0fac-11ea-8943-a9b58d6911e8",
+     "linux/4.9.119@wsbu/stable#2df0cc52a6a94aae8049f01f133b1f19": "71e48849-0fac-11ea-8943-a9b58d6911e8"
     },
-    "path": "/home/david/reusable/Documents/Programming/RedLion/base/.cache/conan/s5t/conanfile.txt"
+    "path": "/home/david/reusable/Documents/Code/base/.cache/conan/s5t/conanfile.txt"
    },
-   "12962a4d-0d3f-11ea-ace0-9941403aca0b": {
-    "pref": "iodb/2.3.1@wsbu/stable#5481b8d7a83e2c08c0c507e6151499af:fb7050f1c2ebbdccd6142c43cdd4ddcaa44891ca",
-    "options": "build_modsim=True\npublic_docs=False\nshared=True\nwith_docs=True",
-    "requires": {
-     "Catch2/2.6.1@catchorg/stable": "12962a4b-0d3f-11ea-ace0-9941403aca0b",
-     "googletest/1.8.1@wsbu/stable": "12962a4c-0d3f-11ea-ace0-9941403aca0b",
-     "wsbu-docgen/0.1.3@wsbu/stable": "12962a48-0d3f-11ea-ace0-9941403aca0b"
-    }
+   "71e4884f-0fac-11ea-8943-a9b58d6911e8": {
+    "pref": "esp-idf/3.1.3004@wsbu/stable#bfeacbaf0da7613d75aa164ecfdd5b17:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
+    "options": ""
+   },
+   "71e4884a-0fac-11ea-8943-a9b58d6911e8": {
+    "pref": "Catch2/2.6.1@catchorg/stable#0:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
+    "options": ""
+   },
+   "71e4884c-0fac-11ea-8943-a9b58d6911e8": {
+    "pref": "wsbu-docgen/0.1.3@wsbu/stable#d85759233340fc32cc4127c5afeba471:5ab84d6acfe1f23c4fae0ab88f26e3a396351ac9",
+    "options": ""
    }
   }
  },

```

This is trivial to reproduce:

```
david@jesko:~/tmp$ conan new Foo/1.0
File saved: conanfile.py
david@jesko:~/tmp$ conan graph lock .
Requirements
Packages

Generated lockfile
david@jesko:~/tmp$ mv conan.lock conan.lock.first
david@jesko:~/tmp$ conan graph lock .
Requirements
Packages

Generated lockfile
david@jesko:~/tmp$ diff conan.lock conan.lock.first
5c5
<    "4c499c53-0fad-11ea-8943-a9b58d6911e8": {
---
>    "46dabdfb-0fad-11ea-8943-a9b58d6911e8": {
david@jesko:~/tmp$ 
```
Hi @DavidZemon 

In your example it makes sense, as you are generating the lockfile twice. Nodes are identified by UUIDs, it is impossible by definition that they will be the same. There is nothing in a ``conan graph lock`` command that would guarantee that the resulting lockfile will be the same, as the dependencies, revisions, etc, could have changed.

I thought you were talking about doing ``install|create`` operations using a lockfile. Does it also happens in that case?
No, if I run `install`, the original lockfile is not modified, as expected.

However, this behavior of using newly generated UUIDs for nodes in the graph is real problem. I expect to be able to commit this file to VCS _and I expect it not to change_ if nothing else has changed. I expect to be able to run `conan graph lock` and then review the resulting diff and say "oh good, nothing has changed, I don't need to re-run all of my tests." I _do not_ expect that my git history is going to be filled to high heaven if one of the steps in my CI server config is to run `conan graph lock` followed by a commit to ensure the full firmware image has the latest versions of everything.
The problem is that nodes in the graph needs unique identifiers, as the package reference might actually correspond to more than one node in the graph. There is no easy way to make the node IDs to be the same between consecutive calls that doesn't share any data. Comparing 2 different lockfiles captured independently in 2 different times hasn't been considered as a use case in the design of lockfiles. 

I understand the issue, and I think it would be a good thing to have, but honestly, I cannot think of any reasonable solution that would achieve this. I will try to think of something, but suggestions welcome. 


starting at leaf nodes with no dependencies, the text of the node could be hashed and used the node ID. go up from there. that would work, right?

For my purposes, I have no need to combine build order and lock file features in one. My idea of a lockfile is something that tells me all of the components in a build... no graph is needed, let alone UUIDs for nodes in that graph.
> starting at leaf nodes with no dependencies, the text of the node could be hashed and used the node ID. go up from there. that would work, right?

That is the point, no, that will not work. For example, you can have a "build_requires" to ``cmake_installer`` package. This package has no dependencies (leaf node). As "build_requires" are private, you get one private node for each node in the graph that needs to be built from sources. They are not the same node because they are private, they can in fact be different versions, or have different options even if it is the same version. So they require to have some distinct unique ID, so they can be addressed and differentiated from the others.
I think I understand your situation, but I don't understand where the problem comes from. Here's how I interpreted your situation:

Package A (my full firmware image) depends on B and C. B has a `build_requires` on X and, in its `configure()` method, it sets `X['with_foo'] = True`. C also has `build_requires` on X, but set's `X['with_foo'] = False`.

If that is, indeed, the situation you're referring to, it should be fine. This means you expect your lockfile to have 5 nodes total: A, B, C, X:with_foo=True, X:with_foo=False. If you hash the strings for each of these nodes, starting with the two X's, you'll get unique hashes for each one of them.

Am I misunderstanding some part of this?
The problem there is what defines the string to be hashed. The full state of a node? All variables, settings, options, dependencies? What happens to configuration erasure (like when data is provided as input to a recipe, but later is cleaned, because it doesn't affect the packageID, like ``
self.cpp_info.header_only()``? Then you should use the "input" configuration instead of the final configuration? But sometimes this configuration doesn't even apply, will introduce unnecessary variability too.

Even in the case that you have exactly the same state of a node, you still need a unique different ID for those, otherwise your graph model would be broken, as those nodes collapse into one, when they are really different nodes.

I think that using the state is not a possibility, I am thinking of ways of the graph generating incrementing integers in a deterministic way, but doesn't seem easy, especially if we want to cover in the future the case of locally modifying lockfiles.

Resolving patch:
diff --git a/conans/client/graph/graph.py b/conans/client/graph/graph.py
--- a/conans/client/graph/graph.py
+++ b/conans/client/graph/graph.py
@@ -1,5 +1,3 @@
-import uuid
-
 from conans.model.ref import PackageReference
 
 
@@ -54,8 +52,6 @@ def __init__(self, ref, conanfile, recipe=None, path=None):
 
     @property
     def id(self):
-        if self._id is None:
-            self._id = str(uuid.uuid1())
         return self._id
 
     @id.setter
@@ -192,12 +188,16 @@ def __hash__(self):
 
 
 class DepsGraph(object):
-    def __init__(self):
+    def __init__(self, initial_node_id=None):
         self.nodes = set()
         self.root = None
         self.aliased = {}
+        self._node_counter = initial_node_id if initial_node_id is not None else -1
 
     def add_node(self, node):
+        if node.id is None:
+            self._node_counter += 1
+            node.id = str(self._node_counter)
         if not self.nodes:
             self.root = node
         self.nodes.add(node)
diff --git a/conans/client/graph/graph_builder.py b/conans/client/graph/graph_builder.py
--- a/conans/client/graph/graph_builder.py
+++ b/conans/client/graph/graph_builder.py
@@ -23,7 +23,8 @@ def __init__(self, proxy, output, loader, resolver, recorder):
     def load_graph(self, root_node, check_updates, update, remotes, profile_host,
                    graph_lock=None):
         check_updates = check_updates or update
-        dep_graph = DepsGraph()
+        initial = graph_lock.initial_counter if graph_lock else None
+        dep_graph = DepsGraph(initial_node_id=initial)
         # compute the conanfile entry point for this dependency graph
         name = root_node.name
         root_node.public_closure = OrderedDict([(name, root_node)])
@@ -322,7 +323,7 @@ def _create_new_node(self, current_node, dep_graph, requirement, name_req,
         conanfile_path, recipe_status, remote, new_ref = result
 
         locked_id = requirement.locked_id
-        lock_python_requires = graph_lock.python_requires(locked_id) if locked_id else None
+        lock_python_requires = graph_lock.python_requires(locked_id) if locked_id is not None else None
         dep_conanfile = self._loader.load_conanfile(conanfile_path, profile,
                                                     ref=requirement.ref,
                                                     lock_python_requires=lock_python_requires)
@@ -348,7 +349,7 @@ def _create_new_node(self, current_node, dep_graph, requirement, name_req,
         new_node.ancestors = current_node.ancestors.copy()
         new_node.ancestors.add(current_node.name)
 
-        if locked_id:
+        if locked_id is not None:
             new_node.id = locked_id
 
         dep_graph.add_node(new_node)
diff --git a/conans/model/graph_lock.py b/conans/model/graph_lock.py
--- a/conans/model/graph_lock.py
+++ b/conans/model/graph_lock.py
@@ -1,5 +1,6 @@
 import json
 import os
+from collections import OrderedDict
 
 from conans.client.graph.graph import RECIPE_VIRTUAL, RECIPE_CONSUMER,\
     BINARY_BUILD
@@ -13,7 +14,7 @@
 
 
 LOCKFILE = "conan.lock"
-LOCKFILE_VERSION = "0.1"
+LOCKFILE_VERSION = "0.2"
 
 
 class GraphLockFile(object):
@@ -43,6 +44,9 @@ def loads(text, revisions_enabled):
         version = graph_json.get("version")
         if version:
             version = Version(version)
+            if version < "0.2":
+                raise ConanException("This lockfile was created with a previous incompatible "
+                                     "version. Please regenerate the lockfile")
             # Do something with it, migrate, raise...
         profile_host = graph_json.get("profile_host") or graph_json["profile"]
         # FIXME: Reading private very ugly
@@ -137,6 +141,11 @@ def __init__(self, graph=None):
                                            requires, node.path)
                 self._nodes[node.id] = graph_node
 
+    @property
+    def initial_counter(self):
+        # IDs are string, we need to compute the maximum
+        return max(int(x) for x in self._nodes.keys())
+
     def root_node_ref(self):
         """ obtain the node in the graph that is not depended by anyone else,
         i.e. the root or downstream consumer
@@ -169,8 +178,8 @@ def as_dict(self):
         that can be converted to json
         """
         result = {}
-        nodes = {}
-        for id_, node in self._nodes.items():
+        nodes = OrderedDict()  # Serialized ordered, so lockfiles are more deterministic
+        for id_, node in sorted(self._nodes.items()):
             nodes[id_] = node.as_dict()
         result["nodes"] = nodes
         return result


Test patch:
diff --git a/conans/test/functional/graph_lock/graph_lock_test.py b/conans/test/functional/graph_lock/graph_lock_test.py
--- a/conans/test/functional/graph_lock/graph_lock_test.py
+++ b/conans/test/functional/graph_lock/graph_lock_test.py
@@ -93,6 +93,41 @@ def test(self):
         self._check_lock("PkgB/0.1@")
 
 
+class ReproducibleLockfiles(unittest.TestCase):
+    def reproducible_lockfile_test(self):
+        client = TestClient()
+        client.save({"conanfile.py": GenConanfile().with_name("PkgA").with_version("0.1")})
+        client.run("create . PkgA/0.1@user/channel")
+
+        # Use a consumer with a version range
+        client.save({"conanfile.py": GenConanfile().with_name("PkgB").with_version("0.1")
+                                                   .with_require_plain("PkgA/[>=0.1]@user/channel")})
+        client.run("graph lock .")
+        lockfile = client.load(LOCKFILE)
+        client.run("graph lock .")
+        lockfile2 = client.load(LOCKFILE)
+        self.assertEqual(lockfile, lockfile2)
+
+    def reproducible_lockfile_txt_test(self):
+        client = TestClient()
+        client.save({"conanfile.txt": ""})
+        client.run("install .")
+        lockfile = client.load("conan.lock")
+        client.run("install .")
+        lockfile2 = client.load("conan.lock")
+        self.assertEqual(lockfile, lockfile2)
+
+    def error_old_format_test(self):
+        client = TestClient()
+        client.save({"conanfile.txt": ""})
+        client.run("install .")
+        lockfile = client.load("conan.lock")
+        lockfile = lockfile.replace('"0.2"', '"0.1"').replace('"0"', '"UUID"')
+        client.save({"conan.lock": lockfile})
+        client.run("install . --lockfile", assert_error=True)
+        self.assertIn("This lockfile was created with a previous incompatible version", client.out)
+
+
 class GraphLockVersionRangeTest(unittest.TestCase):
     consumer = GenConanfile().with_name("PkgB").with_version("0.1")\
                              .with_require_plain("PkgA/[>=0.1]@user/channel")
@@ -121,7 +156,7 @@ def setUp(self):
         client.save({"conanfile.py": str(self.consumer)})
 
     def _check_lock(self, ref_b, rev_b=""):
-        lock_file = load(os.path.join(self.client.current_folder, LOCKFILE))
+        lock_file = self.client.load(LOCKFILE)
         lock_file_json = json.loads(lock_file)
         self.assertEqual(2, len(lock_file_json["graph_lock"]["nodes"]))
         self.assertIn("PkgA/0.1@user/channel#fa090239f8ba41ad559f8e934494ee2a:"
@@ -382,9 +417,10 @@ def package_info(self):
         self._check_lock("PkgB/0.1@")
 
         # If we create a new PkgA revision, for example adding info
-        client.save({"conanfile.py": GenConanfile().with_name("PkgA").with_version("0.1")
-                                        .with_package_info(cpp_info={"libs": ["mylibPkgA0.1lib"]},
-                                                           env_info={"MYENV": ["myenvPkgA0.1env"]})})
+        pkga = GenConanfile().with_name("PkgA").with_version("0.1")
+        pkga.with_package_info(cpp_info={"libs": ["mylibPkgA0.1lib"]},
+                               env_info={"MYENV": ["myenvPkgA0.1env"]})
+        client.save({"conanfile.py": pkga})
 
         client.run("create . PkgA/0.1@user/channel")
         client.save({"conanfile.py": str(consumer)})
@@ -651,4 +687,4 @@ def test(self):
         client.save({"conanfile.py": GenConanfile().with_require_plain("zlib/1.0")})
         client.run("install . --lockfile", assert_error=True)
         self.assertIn("ERROR: 'zlib' cannot be found in lockfile for this package", client.out)
-        self.assertIn("If it is a new requirement, you need to create a new lockile", client.out)
\ No newline at end of file
+        self.assertIn("If it is a new requirement, you need to create a new lockile", client.out)