Skip to content

Commit 4fb8e11

Browse files
committed
Plan for a V3 lockfile format
This commit lays the groundwork for an eventual V3 of the lock file format. The changes in this format are: * A `version` indicator will be at the top of the file so we don't have to guess what format the lock is in, we know for sure. Additionally Cargo now reading a super-from-the-future lock file format will give a better error. * Git dependencies with `Branch("master")` will be encoded with `?branch=master` instead of with nothing. The motivation for this change is to eventually switch Cargo's interpretation of default git branches.
1 parent 32f52fd commit 4fb8e11

File tree

6 files changed

+171
-43
lines changed

6 files changed

+171
-43
lines changed

src/cargo/core/resolver/encode.rs

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@
4242
//! Listed from most recent to oldest, these are some of the changes we've made
4343
//! to `Cargo.lock`'s serialization format:
4444
//!
45+
//! * A `version` marker is now at the top of the lock file which is a way for
46+
//! super-old Cargos (at least since this was implemented) to give a formal
47+
//! error if they see a lock file from a super-future Cargo. Additionally as
48+
//! part of this change the encoding of `git` dependencies in lock files
49+
//! changed where `branch = "master"` is now encoded with `branch=master`
50+
//! instead of with nothing at all.
51+
//!
4552
//! * The entries in `dependencies` arrays have been shortened and the
4653
//! `checksum` field now shows up directly in `[[package]]` instead of always
4754
//! at the end of the file. The goal of this change was to ideally reduce
@@ -89,25 +96,24 @@
8996
//! special fashion to make sure we have strict control over the on-disk
9097
//! format.
9198
92-
use std::collections::{BTreeMap, HashMap, HashSet};
93-
use std::fmt;
94-
use std::str::FromStr;
95-
99+
use super::{Resolve, ResolveVersion};
100+
use crate::core::{Dependency, GitReference, Package, PackageId, SourceId, Workspace};
101+
use crate::util::errors::{CargoResult, CargoResultExt};
102+
use crate::util::interning::InternedString;
103+
use crate::util::{internal, Graph};
104+
use anyhow::bail;
96105
use log::debug;
97106
use serde::de;
98107
use serde::ser;
99108
use serde::{Deserialize, Serialize};
100-
101-
use crate::core::{Dependency, Package, PackageId, SourceId, Workspace};
102-
use crate::util::errors::{CargoResult, CargoResultExt};
103-
use crate::util::interning::InternedString;
104-
use crate::util::{internal, Graph};
105-
106-
use super::{Resolve, ResolveVersion};
109+
use std::collections::{BTreeMap, HashMap, HashSet};
110+
use std::fmt;
111+
use std::str::FromStr;
107112

108113
/// The `Cargo.lock` structure.
109114
#[derive(Serialize, Deserialize, Debug)]
110115
pub struct EncodableResolve {
116+
version: Option<u32>,
111117
package: Option<Vec<EncodableDependency>>,
112118
/// `root` is optional to allow backward compatibility.
113119
root: Option<EncodableDependency>,
@@ -136,8 +142,19 @@ impl EncodableResolve {
136142
let path_deps = build_path_deps(ws);
137143
let mut checksums = HashMap::new();
138144

139-
// We assume an older format is being parsed until we see so otherwise.
140-
let mut version = ResolveVersion::V1;
145+
let mut version = match self.version {
146+
Some(1) => ResolveVersion::V3,
147+
Some(n) => bail!(
148+
"lock file version `{}` was found, but this version of Cargo \
149+
does not understand this lock file, perhaps Cargo needs \
150+
to be updated?",
151+
n,
152+
),
153+
// Historically Cargo did not have a version indicator in lock
154+
// files, so this could either be the V1 or V2 encoding. We assume
155+
// an older format is being parsed until we see so otherwise.
156+
None => ResolveVersion::V1,
157+
};
141158

142159
let packages = {
143160
let mut packages = self.package.unwrap_or_default();
@@ -176,7 +193,7 @@ impl EncodableResolve {
176193
// that here, and we also bump our version up to 2 since V1
177194
// didn't ever encode this field.
178195
if let Some(cksum) = &pkg.checksum {
179-
version = ResolveVersion::V2;
196+
version = version.max(ResolveVersion::V2);
180197
checksums.insert(id, Some(cksum.clone()));
181198
}
182199

@@ -213,7 +230,7 @@ impl EncodableResolve {
213230
let by_source = match &enc_id.version {
214231
Some(version) => by_version.get(version)?,
215232
None => {
216-
version = ResolveVersion::V2;
233+
version = version.max(ResolveVersion::V2);
217234
if by_version.len() == 1 {
218235
by_version.values().next().unwrap()
219236
} else {
@@ -245,7 +262,7 @@ impl EncodableResolve {
245262
// the lock file
246263
} else if by_source.len() == 1 {
247264
let id = by_source.values().next().unwrap();
248-
version = ResolveVersion::V2;
265+
version = version.max(ResolveVersion::V2);
249266
Some(*id)
250267

251268
// ... and failing that we probably had a bad git merge of
@@ -317,7 +334,7 @@ impl EncodableResolve {
317334
// If `checksum` was listed in `[metadata]` but we were previously
318335
// listed as `V2` then assume some sort of bad git merge happened, so
319336
// discard all checksums and let's regenerate them later.
320-
if !to_remove.is_empty() && version == ResolveVersion::V2 {
337+
if !to_remove.is_empty() && version >= ResolveVersion::V2 {
321338
checksums.drain();
322339
}
323340
for k in to_remove {
@@ -539,13 +556,13 @@ impl<'a> ser::Serialize for Resolve {
539556

540557
let mut metadata = self.metadata().clone();
541558

542-
if *self.version() == ResolveVersion::V1 {
559+
if self.version() == ResolveVersion::V1 {
543560
for &id in ids.iter().filter(|id| !id.source_id().is_path()) {
544561
let checksum = match self.checksums()[&id] {
545562
Some(ref s) => &s[..],
546563
None => "<none>",
547564
};
548-
let id = encodable_package_id(id, &state);
565+
let id = encodable_package_id(id, &state, self.version());
549566
metadata.insert(format!("checksum {}", id.to_string()), checksum.to_string());
550567
}
551568
}
@@ -566,9 +583,10 @@ impl<'a> ser::Serialize for Resolve {
566583
source: encode_source(id.source_id()),
567584
dependencies: None,
568585
replace: None,
569-
checksum: match self.version() {
570-
ResolveVersion::V2 => self.checksums().get(id).and_then(|x| x.clone()),
571-
ResolveVersion::V1 => None,
586+
checksum: if self.version() >= ResolveVersion::V2 {
587+
self.checksums().get(id).and_then(|x| x.clone())
588+
} else {
589+
None
572590
},
573591
})
574592
.collect(),
@@ -578,6 +596,10 @@ impl<'a> ser::Serialize for Resolve {
578596
root: None,
579597
metadata,
580598
patch,
599+
version: match self.version() {
600+
ResolveVersion::V3 => Some(1),
601+
ResolveVersion::V2 | ResolveVersion::V1 => None,
602+
},
581603
}
582604
.serialize(s)
583605
}
@@ -589,7 +611,7 @@ pub struct EncodeState<'a> {
589611

590612
impl<'a> EncodeState<'a> {
591613
pub fn new(resolve: &'a Resolve) -> EncodeState<'a> {
592-
let counts = if *resolve.version() == ResolveVersion::V2 {
614+
let counts = if resolve.version() >= ResolveVersion::V2 {
593615
let mut map = HashMap::new();
594616
for id in resolve.iter() {
595617
let slot = map
@@ -613,11 +635,14 @@ fn encodable_resolve_node(
613635
state: &EncodeState<'_>,
614636
) -> EncodableDependency {
615637
let (replace, deps) = match resolve.replacement(id) {
616-
Some(id) => (Some(encodable_package_id(id, state)), None),
638+
Some(id) => (
639+
Some(encodable_package_id(id, state, resolve.version())),
640+
None,
641+
),
617642
None => {
618643
let mut deps = resolve
619644
.deps_not_replaced(id)
620-
.map(|(id, _)| encodable_package_id(id, state))
645+
.map(|(id, _)| encodable_package_id(id, state, resolve.version()))
621646
.collect::<Vec<_>>();
622647
deps.sort();
623648
(None, Some(deps))
@@ -630,16 +655,30 @@ fn encodable_resolve_node(
630655
source: encode_source(id.source_id()),
631656
dependencies: deps,
632657
replace,
633-
checksum: match resolve.version() {
634-
ResolveVersion::V2 => resolve.checksums().get(&id).and_then(|s| s.clone()),
635-
ResolveVersion::V1 => None,
658+
checksum: if resolve.version() >= ResolveVersion::V2 {
659+
resolve.checksums().get(&id).and_then(|s| s.clone())
660+
} else {
661+
None
636662
},
637663
}
638664
}
639665

640-
pub fn encodable_package_id(id: PackageId, state: &EncodeState<'_>) -> EncodablePackageId {
666+
pub fn encodable_package_id(
667+
id: PackageId,
668+
state: &EncodeState<'_>,
669+
resolve_version: ResolveVersion,
670+
) -> EncodablePackageId {
641671
let mut version = Some(id.version().to_string());
642-
let mut source = encode_source(id.source_id()).map(|s| s.with_precise(None));
672+
let mut id_to_encode = id.source_id();
673+
if resolve_version <= ResolveVersion::V2 {
674+
if let Some(GitReference::Branch(b)) = id_to_encode.git_reference() {
675+
if b == "master" {
676+
id_to_encode =
677+
SourceId::for_git(id_to_encode.url(), GitReference::DefaultBranch).unwrap();
678+
}
679+
}
680+
}
681+
let mut source = encode_source(id_to_encode).map(|s| s.with_precise(None));
643682
if let Some(counts) = &state.counts {
644683
let version_counts = &counts[&id.name()];
645684
if version_counts[&id.version()] == 1 {

src/cargo/core/resolver/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1071,7 +1071,7 @@ fn check_duplicate_pkgs_in_lockfile(resolve: &Resolve) -> CargoResult<()> {
10711071
let mut unique_pkg_ids = HashMap::new();
10721072
let state = encode::EncodeState::new(resolve);
10731073
for pkg_id in resolve.iter() {
1074-
let encodable_pkd_id = encode::encodable_package_id(pkg_id, &state);
1074+
let encodable_pkd_id = encode::encodable_package_id(pkg_id, &state, resolve.version());
10751075
if let Some(prev_pkg_id) = unique_pkg_ids.insert(encodable_pkd_id, pkg_id) {
10761076
anyhow::bail!(
10771077
"package collision in the lockfile: packages {} and {} are different, \

src/cargo/core/resolver/resolve.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ pub enum ResolveVersion {
6666
/// listed inline. Introduced in 2019 in version 1.38. New lockfiles use
6767
/// V2 by default starting in 1.41.
6868
V2,
69+
/// A format that explicitly lists a `version` at the top of the file as
70+
/// well as changing how git dependencies are encoded. Dependencies with
71+
/// `branch = "master"` are no longer encoded the same way as those without
72+
/// branch specifiers.
73+
V3,
6974
}
7075

7176
impl Resolve {
@@ -374,8 +379,8 @@ unable to verify that `{0}` is the same as when the lockfile was generated
374379

375380
/// Returns the version of the encoding that's being used for this lock
376381
/// file.
377-
pub fn version(&self) -> &ResolveVersion {
378-
&self.version
382+
pub fn version(&self) -> ResolveVersion {
383+
self.version
379384
}
380385

381386
pub fn summary(&self, pkg_id: PackageId) -> &Summary {

src/cargo/core/source/source_id.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -555,9 +555,6 @@ impl GitReference {
555555
pub fn pretty_ref(&self) -> Option<PrettyRef<'_>> {
556556
match self {
557557
GitReference::DefaultBranch => None,
558-
// See module comments in src/cargo/sources/git/utils.rs for why
559-
// `DefaultBranch` is treated specially here.
560-
GitReference::Branch(m) if m == "master" => None,
561558
_ => Some(PrettyRef { inner: self }),
562559
}
563560
}

src/cargo/ops/lockfile.rs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ fn resolve_to_string_orig(
120120
}
121121
}
122122

123+
if let Some(version) = toml.get("version") {
124+
out.push_str(&format!("version = {}\n\n", version));
125+
}
126+
123127
let deps = toml["package"].as_array().unwrap();
124128
for dep in deps {
125129
let dep = dep.as_table().unwrap();
@@ -147,12 +151,9 @@ fn resolve_to_string_orig(
147151
// encodings going forward, though, we want to be sure that our encoded lock
148152
// file doesn't contain any trailing newlines so trim out the extra if
149153
// necessary.
150-
match resolve.version() {
151-
ResolveVersion::V1 => {}
152-
_ => {
153-
while out.ends_with("\n\n") {
154-
out.pop();
155-
}
154+
if resolve.version() >= ResolveVersion::V2 {
155+
while out.ends_with("\n\n") {
156+
out.pop();
156157
}
157158
}
158159

tests/testsuite/lockfile_compat.rs

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
33
use cargo_test_support::git;
44
use cargo_test_support::registry::Package;
5-
use cargo_test_support::{basic_manifest, lines_match, project};
5+
use cargo_test_support::{basic_lib_manifest, basic_manifest, lines_match, project};
66

77
#[cargo_test]
88
fn oldest_lockfile_still_works() {
@@ -636,3 +636,89 @@ dependencies = [
636636
let lock = p.read_lockfile();
637637
assert_lockfiles_eq(&lockfile, &lock);
638638
}
639+
640+
#[cargo_test]
641+
fn v3_and_git() {
642+
let (git_project, repo) = git::new_repo("dep1", |project| {
643+
project
644+
.file("Cargo.toml", &basic_lib_manifest("dep1"))
645+
.file("src/lib.rs", "")
646+
});
647+
let head_id = repo.head().unwrap().target().unwrap();
648+
649+
let lockfile = format!(
650+
r#"# This file is automatically @generated by Cargo.
651+
# It is not intended for manual editing.
652+
version = 1
653+
654+
[[package]]
655+
name = "dep1"
656+
version = "0.5.0"
657+
source = "git+{}?branch=master#{}"
658+
659+
[[package]]
660+
name = "foo"
661+
version = "0.0.1"
662+
dependencies = [
663+
"dep1",
664+
]
665+
"#,
666+
git_project.url(),
667+
head_id,
668+
);
669+
670+
let p = project()
671+
.file(
672+
"Cargo.toml",
673+
&format!(
674+
r#"
675+
[project]
676+
name = "foo"
677+
version = "0.0.1"
678+
authors = []
679+
680+
[dependencies]
681+
dep1 = {{ git = '{}', branch = 'master' }}
682+
"#,
683+
git_project.url(),
684+
),
685+
)
686+
.file("src/lib.rs", "")
687+
.file("Cargo.lock", "version = 1")
688+
.build();
689+
690+
p.cargo("fetch").run();
691+
692+
let lock = p.read_lockfile();
693+
assert_lockfiles_eq(&lockfile, &lock);
694+
}
695+
696+
#[cargo_test]
697+
fn lock_from_the_future() {
698+
let p = project()
699+
.file(
700+
"Cargo.toml",
701+
r#"
702+
[project]
703+
name = "foo"
704+
version = "0.0.1"
705+
authors = []
706+
"#,
707+
)
708+
.file("src/lib.rs", "")
709+
.file("Cargo.lock", "version = 10000000")
710+
.build();
711+
712+
p.cargo("fetch")
713+
.with_stderr(
714+
"\
715+
error: failed to parse lock file at: [..]
716+
717+
Caused by:
718+
lock file version `10000000` was found, but this version of Cargo does not \
719+
understand this lock file, perhaps Cargo needs to be updated?
720+
",
721+
)
722+
.with_status(101)
723+
.run();
724+
}

0 commit comments

Comments
 (0)