Skip to content

Commit 91a2cd4

Browse files
committed
Auto merge of #3867 - ehuss:features-v2, r=Turbo87
Add support for new feature syntax (RFC 3143) This adds support for the new feature syntax described in [RFC 3143](https://rust-lang.github.io/rfcs/3143-cargo-weak-namespaced-features.html). There are two new feature values that are allowed in the `[features]` table: weak dependencies (`foo?/feat-name`) and namespaced dependencies (`dep:bar`). When these style of features are detected, they are placed in the index in a separate field called `features2` to prevent older versions of cargo from breaking on them. Additionally, it sets a new version field called `v` to indicate that this new field is present, which prevents versions starting at 1.51 from selecting those versions.
2 parents 1ffbe8f + 2b4430c commit 91a2cd4

File tree

6 files changed

+176
-7
lines changed

6 files changed

+176
-7
lines changed

src/controllers/krate/publish.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use flate2::read::GzDecoder;
44
use hex::ToHex;
55
use sha2::{Digest, Sha256};
6+
use std::collections::HashMap;
67
use std::io::Read;
78
use std::sync::Arc;
89
use swirl::Job;
@@ -213,15 +214,29 @@ pub fn publish(req: &mut dyn RequestExt) -> EndpointResult {
213214
.uploader()
214215
.upload_crate(&app, tarball, &krate, vers)?;
215216

217+
let (features, features2): (HashMap<_, _>, HashMap<_, _>) =
218+
features.into_iter().partition(|(_k, vals)| {
219+
!vals
220+
.iter()
221+
.any(|v| v.starts_with("dep:") || v.contains("?/"))
222+
});
223+
let (features2, v) = if features2.is_empty() {
224+
(None, None)
225+
} else {
226+
(Some(features2), Some(2))
227+
};
228+
216229
// Register this crate in our local git repo.
217230
let git_crate = git::Crate {
218231
name: name.0,
219232
vers: vers.to_string(),
220233
cksum: hex_cksum,
221234
features,
235+
features2,
222236
deps: git_deps,
223237
yanked: Some(false),
224238
links,
239+
v,
225240
};
226241
worker::add_crate(git_crate).enqueue(&conn)?;
227242

src/git.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,41 @@ pub struct Crate {
9898
pub deps: Vec<Dependency>,
9999
pub cksum: String,
100100
pub features: HashMap<String, Vec<String>>,
101+
/// This field contains features with new, extended syntax. Specifically,
102+
/// namespaced features (`dep:`) and weak dependencies (`pkg?/feat`).
103+
///
104+
/// It is only populated if a feature uses the new syntax. Cargo merges it
105+
/// on top of the `features` field when reading the entries.
106+
///
107+
/// This is separated from `features` because versions older than 1.19
108+
/// will fail to load due to not being able to parse the new syntax, even
109+
/// with a `Cargo.lock` file.
110+
#[serde(skip_serializing_if = "Option::is_none")]
111+
pub features2: Option<HashMap<String, Vec<String>>>,
101112
pub yanked: Option<bool>,
102113
#[serde(default)]
103114
pub links: Option<String>,
115+
/// The schema version for this entry.
116+
///
117+
/// If this is None, it defaults to version 1. Entries with unknown
118+
/// versions are ignored by cargo starting with 1.51.
119+
///
120+
/// Version `2` format adds the `features2` field.
121+
///
122+
/// This provides a method to safely introduce changes to index entries
123+
/// and allow older versions of cargo to ignore newer entries it doesn't
124+
/// understand. This is honored as of 1.51, so unfortunately older
125+
/// versions will ignore it, and potentially misinterpret version 2 and
126+
/// newer entries.
127+
///
128+
/// The intent is that versions older than 1.51 will work with a
129+
/// pre-existing `Cargo.lock`, but they may not correctly process `cargo
130+
/// update` or build a lock from scratch. In that case, cargo may
131+
/// incorrectly select a new package that uses a new index format. A
132+
/// workaround is to downgrade any packages that are incompatible with the
133+
/// `--precise` flag of `cargo update`.
134+
#[serde(skip_serializing_if = "Option::is_none")]
135+
pub v: Option<u32>,
104136
}
105137

106138
#[derive(Serialize, Deserialize, Debug)]

src/models/krate.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -297,12 +297,13 @@ impl Crate {
297297

298298
/// Validates a whole feature string, `features = ["THIS", "ALL/THIS"]`.
299299
pub fn valid_feature(name: &str) -> bool {
300-
let mut parts = name.split('/');
301-
let name_part = parts.next_back(); // required
302-
let prefix_part = parts.next_back(); // optional
303-
parts.next().is_none()
304-
&& name_part.map_or(false, Crate::valid_feature_name)
305-
&& prefix_part.map_or(true, Crate::valid_feature_prefix)
300+
match name.split_once('/') {
301+
Some((dep, dep_feat)) => {
302+
let dep = dep.strip_suffix('?').unwrap_or(dep);
303+
Crate::valid_feature_prefix(dep) && Crate::valid_feature_name(dep_feat)
304+
}
305+
None => Crate::valid_feature_name(name.strip_prefix("dep:").unwrap_or(name)),
306+
}
306307
}
307308

308309
/// Return both the newest (most recently updated) and
@@ -498,6 +499,11 @@ mod tests {
498499
assert!(Crate::valid_feature("c++20"));
499500
assert!(Crate::valid_feature("krate/c++20"));
500501
assert!(!Crate::valid_feature("c++20/wow"));
502+
assert!(Crate::valid_feature("foo?/bar"));
503+
assert!(Crate::valid_feature("dep:foo"));
504+
assert!(!Crate::valid_feature("dep:foo?/bar"));
505+
assert!(!Crate::valid_feature("foo/?bar"));
506+
assert!(!Crate::valid_feature("foo?bar"));
501507
}
502508
}
503509

src/tests/builders/publish.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub struct PublishBuilder {
3636
readme: Option<String>,
3737
tarball: Vec<u8>,
3838
version: semver::Version,
39+
features: HashMap<u::EncodableFeatureName, Vec<u::EncodableFeature>>,
3940
}
4041

4142
impl PublishBuilder {
@@ -55,6 +56,7 @@ impl PublishBuilder {
5556
readme: None,
5657
tarball: EMPTY_TARBALL_BYTES.to_vec(),
5758
version: semver::Version::parse("1.0.0").unwrap(),
59+
features: HashMap::new(),
5860
}
5961
}
6062

@@ -166,11 +168,22 @@ impl PublishBuilder {
166168
self
167169
}
168170

171+
// Adds a feature.
172+
pub fn feature(mut self, name: &str, values: &[&str]) -> Self {
173+
let values = values
174+
.iter()
175+
.map(|s| u::EncodableFeature(s.to_string()))
176+
.collect();
177+
self.features
178+
.insert(u::EncodableFeatureName(name.to_string()), values);
179+
self
180+
}
181+
169182
pub fn build(self) -> (String, Vec<u8>) {
170183
let new_crate = u::EncodableCrateUpload {
171184
name: u::EncodableCrateName(self.krate_name.clone()),
172185
vers: u::EncodableCrateVersion(self.version),
173-
features: HashMap::new(),
186+
features: self.features,
174187
deps: self.deps,
175188
description: self.desc,
176189
homepage: None,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
[
2+
{
3+
"request": {
4+
"uri": "http://alexcrichton-test.s3.amazonaws.com/crates/foo/foo-1.0.0.crate",
5+
"method": "PUT",
6+
"headers": [
7+
[
8+
"accept",
9+
"*/*"
10+
],
11+
[
12+
"content-length",
13+
"35"
14+
],
15+
[
16+
"host",
17+
"alexcrichton-test.s3.amazonaws.com"
18+
],
19+
[
20+
"accept-encoding",
21+
"gzip"
22+
],
23+
[
24+
"content-type",
25+
"application/x-tar"
26+
],
27+
[
28+
"authorization",
29+
"AWS AKIAICL5IWUZYWWKA7JA:uDc39eNdF6CcwB+q+JwKsoDLQc4="
30+
],
31+
[
32+
"date",
33+
"Fri, 15 Sep 2017 07:53:06 -0700"
34+
]
35+
],
36+
"body": "H4sIAAAAAAAA/+3AAQEAAACCIP+vbkhQwKsBLq+17wAEAAA="
37+
},
38+
"response": {
39+
"status": 200,
40+
"headers": [
41+
[
42+
"x-amz-request-id",
43+
"26589A5E52F8395C"
44+
],
45+
[
46+
"ETag",
47+
"\"f9016ad360cebb4fe2e6e96e5949f022\""
48+
],
49+
[
50+
"date",
51+
"Fri, 15 Sep 2017 14:53:07 GMT"
52+
],
53+
[
54+
"content-length",
55+
"0"
56+
],
57+
[
58+
"x-amz-id-2",
59+
"JdIvnNTw53aqXjBIqBLNuN4kxf/w1XWX+xuIiGBDYy7yzOSDuAMtBSrTW4ZWetcCIdqCUHuQ51A="
60+
],
61+
[
62+
"Server",
63+
"AmazonS3"
64+
]
65+
],
66+
"body": ""
67+
}
68+
}
69+
]

src/tests/krate/publish.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use flate2::Compression;
1313
use http::StatusCode;
1414
use std::collections::HashMap;
1515
use std::io::Read;
16+
use std::iter::FromIterator;
1617
use std::time::Duration;
1718
use std::{io, thread};
1819

@@ -954,3 +955,36 @@ fn publish_rate_limit_doesnt_affect_existing_crates() {
954955
token.enqueue_publish(new_version).good();
955956
app.run_pending_background_jobs();
956957
}
958+
959+
#[test]
960+
fn features_version_2() {
961+
let (app, _, user, token) = TestApp::full().with_token();
962+
963+
app.db(|conn| {
964+
// Insert a crate directly into the database so that foo_new can depend on it
965+
CrateBuilder::new("bar", user.as_model().id).expect_build(conn);
966+
});
967+
968+
let dependency = DependencyBuilder::new("bar");
969+
970+
let crate_to_publish = PublishBuilder::new("foo")
971+
.version("1.0.0")
972+
.dependency(dependency)
973+
.feature("new_feat", &["dep:bar", "bar?/feat"])
974+
.feature("old_feat", &[]);
975+
token.enqueue_publish(crate_to_publish).good();
976+
app.run_pending_background_jobs();
977+
978+
let crates = app.crates_from_index_head("foo");
979+
assert_eq!(crates.len(), 1);
980+
assert_eq!(crates[0].name, "foo");
981+
assert_eq!(crates[0].deps.len(), 1);
982+
assert_eq!(crates[0].v, Some(2));
983+
let features = HashMap::from_iter([("old_feat".to_string(), vec![])]);
984+
assert_eq!(crates[0].features, features);
985+
let features2 = HashMap::from_iter([(
986+
"new_feat".to_string(),
987+
vec!["dep:bar".to_string(), "bar?/feat".to_string()],
988+
)]);
989+
assert_eq!(crates[0].features2, Some(features2));
990+
}

0 commit comments

Comments
 (0)