diff --git a/app/components/badge-build-info.js b/app/components/badge-build-info.js
new file mode 100644
index 00000000000..2fe3efbaef6
--- /dev/null
+++ b/app/components/badge-build-info.js
@@ -0,0 +1,41 @@
+import Ember from 'ember';
+
+import { formatDay } from 'cargo/helpers/format-day';
+
+export default Ember.Component.extend({
+ tagName: 'span',
+ classNames: ['build_info'],
+
+ build_info: Ember.computed('crate.max_build_info_stable', 'crate.max_build_info_beta', 'crate.max_build_info_nightly', function() {
+ if (this.get('crate.max_build_info_stable')) {
+ return 'stable';
+ } else if (this.get('crate.max_build_info_beta')) {
+ return 'beta';
+ } else if (this.get('crate.max_build_info_nightly')) {
+ return 'nightly';
+ } else {
+ return null;
+ }
+ }),
+ color: Ember.computed('build_info', function() {
+ if (this.get('build_info') === 'stable') {
+ return 'brightgreen';
+ } else if (this.get('build_info') === 'beta') {
+ return 'yellow';
+ } else {
+ return 'orange';
+ }
+ }),
+ version_display: Ember.computed('build_info', 'crate.max_build_info_stable', 'crate.max_build_info_beta', 'crate.max_build_info_nightly', function() {
+ if (this.get('build_info') === 'stable') {
+ return this.get('crate.max_build_info_stable');
+ } else if (this.get('build_info') === 'beta') {
+ return formatDay(this.get('crate.max_build_info_beta'));
+ } else {
+ return formatDay(this.get('crate.max_build_info_nightly'));
+ }
+ }),
+ version_for_shields: Ember.computed('version_display', function() {
+ return this.get('version_display').replace(/-/g, '--');
+ }),
+});
diff --git a/app/helpers/format-build-result.js b/app/helpers/format-build-result.js
new file mode 100644
index 00000000000..948862ae99c
--- /dev/null
+++ b/app/helpers/format-build-result.js
@@ -0,0 +1,13 @@
+import Ember from 'ember';
+
+export function formatBuildResult(result) {
+ if (result === true) {
+ return 'Pass';
+ } else if (result === false) {
+ return 'Fail';
+ } else {
+ return null;
+ }
+}
+
+export default Ember.Helper.helper(params => formatBuildResult(params[0]));
diff --git a/app/helpers/format-day.js b/app/helpers/format-day.js
new file mode 100644
index 00000000000..33c2d80b102
--- /dev/null
+++ b/app/helpers/format-day.js
@@ -0,0 +1,8 @@
+import Ember from 'ember';
+import moment from 'moment';
+
+export function formatDay(date) {
+ return date ? moment(date).utc().format('YYYY-MM-DD') : null;
+}
+
+export default Ember.Helper.helper(params => formatDay(params[0]));
diff --git a/app/helpers/value-or-default.js b/app/helpers/value-or-default.js
new file mode 100644
index 00000000000..7dda85e77aa
--- /dev/null
+++ b/app/helpers/value-or-default.js
@@ -0,0 +1,7 @@
+import Ember from 'ember';
+
+export function valueOrDefault(value, default_value) {
+ return value ? value : default_value;
+}
+
+export default Ember.Helper.helper(params => valueOrDefault(params[0], params[1]));
diff --git a/app/models/build-info.js b/app/models/build-info.js
new file mode 100644
index 00000000000..ebfb9367df9
--- /dev/null
+++ b/app/models/build-info.js
@@ -0,0 +1,46 @@
+import DS from 'ember-data';
+import Ember from 'ember';
+
+const TIER1 = [
+ ['x86_64-unknown-linux-gnu', 'Linux'],
+ ['x86_64-apple-darwin', 'macOS'],
+ ['x86_64-pc-windows-gnu', 'Windows (GNU)'],
+ ['x86_64-pc-windows-msvc', 'Windows (MSVC)'],
+];
+
+const last = name => (
+ Ember.computed(name, function() {
+ const items = this.get(name);
+ return items[items.length - 1];
+ })
+);
+
+export default DS.Model.extend({
+ version: DS.belongsTo('version', { async: true }),
+ ordering: DS.attr(),
+ stable: DS.attr(),
+ beta: DS.attr(),
+ nightly: DS.attr(),
+
+ has_any_info: Ember.computed('ordering', function() {
+ const ordering = this.get('ordering');
+ const num_results = ordering.stable.length + ordering.nightly.length + ordering.beta.length;
+ return num_results > 0;
+ }),
+
+ latest_stable: last('ordering.stable'),
+ latest_beta: last('ordering.beta'),
+ latest_nightly: last('ordering.nightly'),
+ tier1_results: Ember.computed('nightly', 'latest_nightly', 'beta', 'latest_beta', 'stable', 'latest_stable', function() {
+ const nightly_results = this.get('nightly')[this.get('latest_nightly')] || {};
+ const beta_results = this.get('beta')[this.get('latest_beta')] || {};
+ const stable_results = this.get('stable')[this.get('latest_stable')] || {};
+
+ return TIER1.map(([target, display]) => ({
+ display_target: display,
+ nightly: nightly_results[target],
+ beta: beta_results[target],
+ stable: stable_results[target]
+ }));
+ }),
+});
diff --git a/app/models/crate.js b/app/models/crate.js
index e5c07f40af8..ceb5cf106b4 100644
--- a/app/models/crate.js
+++ b/app/models/crate.js
@@ -16,6 +16,9 @@ export default DS.Model.extend({
documentation: DS.attr('string'),
repository: DS.attr('string'),
license: DS.attr('string'),
+ max_build_info_nightly: DS.attr('date'),
+ max_build_info_beta: DS.attr('date'),
+ max_build_info_stable: DS.attr('string'),
versions: DS.hasMany('versions', { async: true }),
badges: DS.attr(),
diff --git a/app/models/version.js b/app/models/version.js
index 0876d3cdf89..53a7cdd62f9 100644
--- a/app/models/version.js
+++ b/app/models/version.js
@@ -12,6 +12,7 @@ export default DS.Model.extend({
async: false
}),
authors: DS.hasMany('users', { async: true }),
+ build_info: DS.belongsTo('build-info', { async: true }),
dependencies: DS.hasMany('dependency', { async: true }),
version_downloads: DS.hasMany('version-download', { async: true }),
diff --git a/app/styles/crate.scss b/app/styles/crate.scss
index 05e2b453280..88249a9872c 100644
--- a/app/styles/crate.scss
+++ b/app/styles/crate.scss
@@ -256,6 +256,25 @@
}
}
+#crate-build-info {
+ padding-bottom: 50px;
+ border-bottom: 5px solid $gray-border;
+ margin-bottom: 30px;
+
+ .description {
+ margin-bottom: 30px;
+ }
+
+ table {
+ border: 1px solid $gray-border;
+ td, th {
+ border: 1px solid $gray-border;
+ padding: 5px 10px;
+ text-align: left;
+ }
+ }
+}
+
#crate-downloads {
@include display-flex;
@include flex-wrap(wrap);
diff --git a/app/templates/components/badge-build-info.hbs b/app/templates/components/badge-build-info.hbs
new file mode 100644
index 00000000000..8d51aedd7e8
--- /dev/null
+++ b/app/templates/components/badge-build-info.hbs
@@ -0,0 +1,6 @@
+{{#if build_info}}
+
+{{/if}}
\ No newline at end of file
diff --git a/app/templates/components/crate-row.hbs b/app/templates/components/crate-row.hbs
index 80c27e3e65e..d277fb93e10 100644
--- a/app/templates/components/crate-row.hbs
+++ b/app/templates/components/crate-row.hbs
@@ -10,6 +10,7 @@
{{#each crate.annotated_badges as |badge|}}
{{component badge.component_name badge=badge}}
{{/each}}
+ {{badge-build-info crate=crate}}
diff --git a/app/templates/crate/version.hbs b/app/templates/crate/version.hbs
index 30dccfaf25f..cdf5f443cd1 100644
--- a/app/templates/crate/version.hbs
+++ b/app/templates/crate/version.hbs
@@ -217,6 +217,40 @@
+
+
Build info
+
+ {{#if currentVersion.build_info.has_any_info }}
+
+ Most recent reported build information for
+ version {{ currentVersion.num }}:
+
+
+
+
+ Tier 1 Target |
+ Stable: {{ value-or-default currentVersion.build_info.latest_stable 'None' }} |
+ Beta: {{ value-or-default (format-day currentVersion.build_info.latest_beta) 'None' }} |
+ Nightly: {{ value-or-default (format-day currentVersion.build_info.latest_nightly) 'None' }} |
+
+
+ {{#each currentVersion.build_info.tier1_results as |result|}}
+
+ {{ result.display_target }} |
+ {{ format-build-result result.stable }} |
+ {{ format-build-result result.beta }} |
+ {{ format-build-result result.nightly }} |
+
+ {{/each}}
+
+ {{else}}
+
+ No build information available for this version. If you are the
+ crate owner... // TODO LINK TO DOCS
+
+ {{/if}}
+
+
Stats Overview
diff --git a/src/bin/migrate.rs b/src/bin/migrate.rs
index ce635d3db5a..07e14140bbf 100644
--- a/src/bin/migrate.rs
+++ b/src/bin/migrate.rs
@@ -831,6 +831,31 @@ fn migrations() -> Vec
{
try!(tx.execute("DROP INDEX badges_crate_type", &[]));
Ok(())
}),
+ Migration::add_table(20170127104519, "build_info", " \
+ version_id INTEGER NOT NULL, \
+ rust_version VARCHAR NOT NULL, \
+ target VARCHAR NOT NULL, \
+ passed BOOLEAN NOT NULL, \
+ created_at TIMESTAMP NOT NULL DEFAULT now(), \
+ updated_at TIMESTAMP NOT NULL DEFAULT now()"),
+ Migration::new(20170127104925, |tx| {
+ try!(tx.execute("CREATE UNIQUE INDEX build_info_combo \
+ ON build_info (version_id, rust_version, target)", &[]));
+ Ok(())
+ }, |tx| {
+ try!(tx.execute("DROP INDEX build_info_combo", &[]));
+ Ok(())
+ }),
+ Migration::run(20170127143020,
+ "ALTER TABLE crates \
+ ADD COLUMN max_build_info_stable VARCHAR, \
+ ADD COLUMN max_build_info_beta TIMESTAMP, \
+ ADD COLUMN max_build_info_nightly TIMESTAMP",
+ "ALTER TABLE crates \
+ DROP COLUMN max_build_info_stable, \
+ DROP COLUMN max_build_info_beta, \
+ DROP COLUMN max_build_info_nightly",
+ ),
];
// NOTE: Generate a new id via `date +"%Y%m%d%H%M%S"`
diff --git a/src/krate.rs b/src/krate.rs
index f0636beda4a..32a3262277b 100644
--- a/src/krate.rs
+++ b/src/krate.rs
@@ -52,6 +52,9 @@ pub struct Crate {
pub license: Option,
pub repository: Option,
pub max_upload_size: Option,
+ pub max_build_info_stable: Option,
+ pub max_build_info_beta: Option,
+ pub max_build_info_nightly: Option,
}
#[derive(RustcEncodable, RustcDecodable)]
@@ -66,6 +69,9 @@ pub struct EncodableCrate {
pub created_at: String,
pub downloads: i32,
pub max_version: String,
+ pub max_build_info_stable: Option,
+ pub max_build_info_beta: Option,
+ pub max_build_info_nightly: Option,
pub description: Option,
pub homepage: Option,
pub documentation: Option,
@@ -246,6 +252,7 @@ impl Crate {
name, created_at, updated_at, downloads, max_version, description,
homepage, documentation, license, repository,
readme: _, id: _, max_upload_size: _,
+ max_build_info_stable, max_build_info_beta, max_build_info_nightly,
} = self;
let versions_link = match versions {
Some(..) => None,
@@ -267,6 +274,9 @@ impl Crate {
categories: category_ids,
badges: badges,
max_version: max_version.to_string(),
+ max_build_info_stable: max_build_info_stable.map(|s| s.to_string()),
+ max_build_info_beta: max_build_info_beta.map(::encode_time),
+ max_build_info_nightly: max_build_info_nightly.map(::encode_time),
documentation: documentation,
homepage: homepage,
description: description,
@@ -386,11 +396,28 @@ impl Crate {
let zero = semver::Version::parse("0.0.0").unwrap();
if *ver > self.max_version || self.max_version == zero {
self.max_version = ver.clone();
+ self.max_build_info_stable = None;
+ self.max_build_info_beta = None;
+ self.max_build_info_nightly = None;
}
- let stmt = try!(conn.prepare("UPDATE crates SET max_version = $1
- WHERE id = $2 RETURNING updated_at"));
- let rows = try!(stmt.query(&[&self.max_version.to_string(), &self.id]));
+
+ let stmt = conn.prepare(" \
+ UPDATE crates \
+ SET max_version = $1,
+ max_build_info_stable = $2,
+ max_build_info_beta = $3,
+ max_build_info_nightly = $4 \
+ WHERE id = $5 \
+ RETURNING updated_at")?;
+ let rows = try!(stmt.query(&[
+ &self.max_version.to_string(),
+ &self.max_build_info_stable.as_ref().map(|vers| vers.to_string()),
+ &self.max_build_info_beta,
+ &self.max_build_info_nightly,
+ &self.id
+ ]));
self.updated_at = rows.get(0).get("updated_at");
+
Version::insert(conn, self.id, ver, features, authors)
}
@@ -460,6 +487,7 @@ impl Crate {
impl Model for Crate {
fn from_row(row: &Row) -> Crate {
let max: String = row.get("max_version");
+ let max_build_info_stable: Option = row.get("max_build_info_stable");
Crate {
id: row.get("id"),
name: row.get("name"),
@@ -474,6 +502,11 @@ impl Model for Crate {
license: row.get("license"),
repository: row.get("repository"),
max_upload_size: row.get("max_upload_size"),
+ max_build_info_stable: max_build_info_stable.map(|stable| {
+ semver::Version::parse(&stable).unwrap()
+ }),
+ max_build_info_beta: row.get("max_build_info_beta"),
+ max_build_info_nightly: row.get("max_build_info_nightly"),
}
}
fn table_name(_: Option) -> &'static str { "crates" }
diff --git a/src/lib.rs b/src/lib.rs
index f3efe3fedb2..9015f037c33 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -101,6 +101,8 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder {
api_router.delete("/crates/:crate_id/owners", C(krate::remove_owners));
api_router.delete("/crates/:crate_id/:version/yank", C(version::yank));
api_router.put("/crates/:crate_id/:version/unyank", C(version::unyank));
+ api_router.get("/crates/:crate_id/:version/build_info", C(version::build_info));
+ api_router.put("/crates/:crate_id/:version/build_info", C(version::publish_build_info));
api_router.get("/crates/:crate_id/reverse_dependencies", C(krate::reverse_dependencies));
api_router.get("/versions", C(version::index));
api_router.get("/versions/:version_id", C(version::show));
diff --git a/src/tests/all.rs b/src/tests/all.rs
index a008de37d67..74a529ca706 100755
--- a/src/tests/all.rs
+++ b/src/tests/all.rs
@@ -194,6 +194,9 @@ fn krate(name: &str) -> Crate {
created_at: time::now().to_timespec(),
downloads: 10,
max_version: semver::Version::parse("0.0.0").unwrap(),
+ max_build_info_stable: None,
+ max_build_info_beta: None,
+ max_build_info_nightly: None,
documentation: None,
homepage: None,
description: None,
diff --git a/src/tests/version.rs b/src/tests/version.rs
index 5e4b13c7de5..e40f51ae561 100644
--- a/src/tests/version.rs
+++ b/src/tests/version.rs
@@ -3,9 +3,12 @@ use rustc_serialize::json::Json;
use conduit::{Handler, Request, Method};
use semver;
+use time;
use cargo_registry::db::RequestTransaction;
-use cargo_registry::version::{EncodableVersion, Version};
+use cargo_registry::version::{EncodableVersion, Version, EncodableBuildInfo};
+use cargo_registry::upload;
+use cargo_registry::krate::Crate;
#[derive(RustcDecodable)]
struct VersionList { versions: Vec }
@@ -16,6 +19,10 @@ fn sv(s: &str) -> semver::Version {
semver::Version::parse(s).unwrap()
}
+fn ts(s: &str) -> time::Timespec {
+ time::strptime(s, "%Y-%m-%d").expect("Bad date string").to_timespec()
+}
+
#[test]
fn index() {
let (_b, app, middle) = ::app();
@@ -70,3 +77,345 @@ fn authors() {
let json = json.as_object().unwrap();
assert!(json.contains_key(&"users".to_string()));
}
+
+#[test]
+fn publish_build_info() {
+ #[derive(RustcDecodable)] struct O { ok: bool }
+ let (_b, app, middle) = ::app();
+
+ let mut req = ::new_req(app.clone(), "publish-build-info", "1.0.0");
+ ::mock_user(&mut req, ::user("foo"));
+ ::mock_crate(&mut req, ::krate("publish-build-info"));
+
+ let body = "{\
+ \"name\":\"publish-build-info\",\
+ \"vers\":\"1.0.0\",\
+ \"rust_version\":\"rustc 1.16.0-nightly (df8debf6d 2017-01-25)\",\
+ \"target\":\"x86_64-pc-windows-gnu\",\
+ \"passed\":true}";
+
+ let mut response = ok_resp!(middle.call(req.with_path(
+ "/api/v1/crates/publish-build-info/1.0.0/build_info")
+ .with_method(Method::Put)
+ .with_body(body.as_bytes())));
+ assert!(::json::(&mut response).ok);
+
+ let body = "{\
+ \"name\":\"publish-build-info\",\
+ \"vers\":\"1.0.0\",\
+ \"rust_version\":\"rustc 1.13.0 (df8debf6d 2017-01-25)\",\
+ \"target\":\"x86_64-pc-windows-gnu\",\
+ \"passed\":true}";
+
+ let mut response = ok_resp!(middle.call(req.with_path(
+ "/api/v1/crates/publish-build-info/1.0.0/build_info")
+ .with_method(Method::Put)
+ .with_body(body.as_bytes())));
+ assert!(::json::(&mut response).ok);
+
+ let body = "{\
+ \"name\":\"publish-build-info\",\
+ \"vers\":\"1.0.0\",\
+ \"rust_version\":\"rustc 1.15.0-beta (df8debf6d 2017-01-20)\",\
+ \"target\":\"x86_64-pc-windows-gnu\",\
+ \"passed\":true}";
+
+ let mut response = ok_resp!(middle.call(req.with_path(
+ "/api/v1/crates/publish-build-info/1.0.0/build_info")
+ .with_method(Method::Put)
+ .with_body(body.as_bytes())));
+ assert!(::json::(&mut response).ok);
+
+ let mut response = ok_resp!(middle.call(req.with_path(
+ "/api/v1/crates/publish-build-info/1.0.0/build_info")
+ .with_method(Method::Get)));
+
+ #[derive(RustcDecodable)]
+ struct R { build_info: EncodableBuildInfo }
+
+ let json = ::json::(&mut response);
+ assert_eq!(
+ json.build_info.ordering.get("nightly"),
+ Some(&vec![String::from("2017-01-25T00:00:00Z")])
+ );
+ assert_eq!(
+ json.build_info.ordering.get("beta"),
+ Some(&vec![String::from("2017-01-20T00:00:00Z")])
+ );
+ assert_eq!(
+ json.build_info.ordering.get("stable"),
+ Some(&vec![String::from("1.13.0")])
+ );
+}
+
+#[test]
+fn bad_rust_version_publish_build_info() {
+ let (_b, app, middle) = ::app();
+
+ let mut req = ::new_req(app.clone(), "bad-rust-vers", "1.0.0");
+ ::mock_user(&mut req, ::user("foo"));
+ ::mock_crate(&mut req, ::krate("bad-rust-vers"));
+
+ let body = "{\
+ \"name\":\"bad-rust-vers\",\
+ \"vers\":\"1.0.0\",\
+ \"rust_version\":\"rustc 1.16.0-dev (df8debf6d 2017-01-25)\",\
+ \"target\":\"x86_64-pc-windows-gnu\",\
+ \"passed\":true}";
+
+ let response = bad_resp!(middle.call(req.with_path(
+ "/api/v1/crates/bad-rust-vers/1.0.0/build_info")
+ .with_method(Method::Put)
+ .with_body(body.as_bytes())));
+
+ assert_eq!(
+ response.errors[0].detail,
+ "rust_version `rustc 1.16.0-dev (df8debf6d 2017-01-25)` \
+ not recognized as nightly, beta, or stable");
+
+ let body = "{\
+ \"name\":\"bad-rust-vers\",\
+ \"vers\":\"1.0.0\",\
+ \"rust_version\":\"1.15.0\",\
+ \"target\":\"x86_64-pc-windows-gnu\",\
+ \"passed\":true}";
+
+ let response = bad_resp!(middle.call(req.with_path(
+ "/api/v1/crates/bad-rust-vers/1.0.0/build_info")
+ .with_method(Method::Put)
+ .with_body(body.as_bytes())));
+
+ assert_eq!(
+ response.errors[0].detail,
+ "rust_version `1.15.0` not recognized; \
+ expected format like `rustc X.Y.Z (SHA YYYY-MM-DD)`");
+}
+
+#[test]
+fn no_existing_max_build_info() {
+ let (_b, app, _middle) = ::app();
+ let mut req = ::req(app, Method::Get, "/api/v1/versions");
+ ::mock_user(&mut req, ::user("foo"));
+ let (krate, version) = ::mock_crate(&mut req, ::krate("no-existing-max"));
+ let req: &mut Request = &mut req;
+ let tx = req.tx().unwrap();
+
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.14.0 (e8a012324 2016-12-16)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.15.0-beta.5 (10893a9a3 2017-01-19)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.16.0-nightly (df8debf6d 2017-01-25)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let krate = Crate::find_by_name(tx, "no-existing-max").unwrap();
+ assert_eq!(krate.max_build_info_stable, Some(sv("1.14.0")));
+ assert_eq!(krate.max_build_info_beta, Some(ts("2017-01-19")));
+ assert_eq!(krate.max_build_info_nightly, Some(ts("2017-01-25")));
+}
+
+#[test]
+fn failed_build_info_doesnt_update_max() {
+ let (_b, app, _middle) = ::app();
+ let mut req = ::req(app, Method::Get, "/api/v1/versions");
+ ::mock_user(&mut req, ::user("foo"));
+ let (krate, version) = ::mock_crate(&mut req, ::krate("failed-build"));
+ let req: &mut Request = &mut req;
+ let tx = req.tx().unwrap();
+
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.14.0 (e8a012324 2016-12-16)"),
+ target: String::from("anything"),
+ passed: false, // this is the different part
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.15.0-beta.5 (10893a9a3 2017-01-19)"),
+ target: String::from("anything"),
+ passed: false, // this is the different part
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.16.0-nightly (df8debf6d 2017-01-25)"),
+ target: String::from("anything"),
+ passed: false, // this is the different part
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let krate = Crate::find_by_name(tx, "failed-build").unwrap();
+ assert_eq!(krate.max_build_info_stable, None);
+ assert_eq!(krate.max_build_info_beta, None);
+ assert_eq!(krate.max_build_info_nightly, None);
+}
+
+#[test]
+fn not_max_version_build_info_doesnt_update_max() {
+ let (_b, app, _middle) = ::app();
+ let mut req = ::req(app, Method::Get, "/api/v1/versions");
+ ::mock_user(&mut req, ::user("foo"));
+ let (krate, version) = ::mock_crate(&mut req, ::krate("old-rust-vers"));
+ let req: &mut Request = &mut req;
+ let tx = req.tx().unwrap();
+
+ // Setup: store build info for:
+ // stable 1.14.0, beta 2017-01-19, nightly 2017-01-25
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.14.0 (e8a012324 2016-12-16)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.15.0-beta.5 (10893a9a3 2017-01-19)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.16.0-nightly (df8debf6d 2017-01-25)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ // Need to reload to see versions added in setup
+ let krate = Crate::find_by_name(tx, "old-rust-vers").unwrap();
+
+ // Report build info for:
+ // stable 1.13.0, beta 2017-01-01, nightly 2017-01-24
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.13.0 (2c6933acc 2016-11-07)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.15.0-beta.3 (beefcafe 2017-01-01)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.16.0-nightly (deadbeef 2017-01-24)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ // Max build info should still be 1.14.0, 2017-01-19, and 2017-01-25
+ let krate = Crate::find_by_name(tx, "old-rust-vers").unwrap();
+ assert_eq!(krate.max_build_info_stable, Some(sv("1.14.0")));
+ assert_eq!(krate.max_build_info_beta, Some(ts("2017-01-19")));
+ assert_eq!(krate.max_build_info_nightly, Some(ts("2017-01-25")));
+}
+
+#[test]
+fn older_crate_version_in_build_info_doesnt_update_max() {
+ let (_b, app, _middle) = ::app();
+ let mut req = ::req(app, Method::Get, "/api/v1/versions");
+ ::mock_user(&mut req, ::user("foo"));
+
+ let req: &mut Request = &mut req;
+
+ let krate = ::krate("older-crate-version");
+
+ // Publish a version of the crate
+ let (krate, _) = ::mock_crate_vers(req, krate, &sv("2.0.0"));
+
+ // Then go back and publish a lower version
+ let (krate, version) = ::mock_crate(req, krate);
+
+ assert_eq!(krate.max_version, sv("2.0.0"));
+ assert_eq!(version.num, sv("1.0.0"));
+
+ let tx = req.tx().unwrap();
+
+ // Publish the build info for the lower version
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.14.0 (e8a012324 2016-12-16)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.15.0-beta.5 (10893a9a3 2017-01-19)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.16.0-nightly (df8debf6d 2017-01-25)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let krate = Crate::find_by_name(tx, "older-crate-version").unwrap();
+ assert_eq!(krate.max_build_info_stable, None);
+ assert_eq!(krate.max_build_info_beta, None);
+ assert_eq!(krate.max_build_info_nightly, None);
+}
+
+#[test]
+fn clear_max_build_info_on_new_crate_max_version() {
+ let (_b, app, _middle) = ::app();
+ let mut req = ::req(app, Method::Get, "/api/v1/versions");
+ ::mock_user(&mut req, ::user("foo"));
+ let (krate, version) = ::mock_crate(&mut req, ::krate("no-existing-max"));
+ let req: &mut Request = &mut req;
+ {
+ let tx = req.tx().unwrap();
+
+ // Setup: publish some build info for the current max version
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.14.0 (e8a012324 2016-12-16)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.15.0-beta.5 (10893a9a3 2017-01-19)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+
+ let info = upload::VersionBuildInfo {
+ rust_version: String::from("rustc 1.16.0-nightly (df8debf6d 2017-01-25)"),
+ target: String::from("anything"),
+ passed: true,
+ };
+ version.store_build_info(tx, info, &krate).unwrap();
+ }
+
+ // Then publish a higher version of the crate
+ ::mock_crate_vers(req, krate, &sv("2.0.0"));
+
+ let tx = req.tx().unwrap();
+ let krate = Crate::find_by_name(tx, "no-existing-max").unwrap();
+ assert_eq!(krate.max_build_info_stable, None);
+ assert_eq!(krate.max_build_info_beta, None);
+ assert_eq!(krate.max_build_info_nightly, None);
+}
diff --git a/src/upload.rs b/src/upload.rs
index 3a775645f6e..c98418cc149 100644
--- a/src/upload.rs
+++ b/src/upload.rs
@@ -4,9 +4,11 @@ use std::ops::Deref;
use rustc_serialize::{Decodable, Decoder, Encoder, Encodable};
use semver;
use dependency::Kind as DependencyKind;
+use util::CargoResult;
use keyword::Keyword as CrateKeyword;
use krate::Crate;
+use version::ChannelVersion;
#[derive(RustcDecodable, RustcEncodable)]
pub struct NewCrate {
@@ -48,6 +50,19 @@ pub struct CrateDependency {
pub kind: Option,
}
+#[derive(RustcDecodable, RustcEncodable)]
+pub struct VersionBuildInfo {
+ pub rust_version: String,
+ pub target: String,
+ pub passed: bool,
+}
+
+impl VersionBuildInfo {
+ pub fn channel_version(&self) -> CargoResult {
+ self.rust_version.parse()
+ }
+}
+
impl Decodable for CrateName {
fn decode(d: &mut D) -> Result {
let s = try!(d.read_str());
diff --git a/src/version.rs b/src/version.rs
index 2265d445db8..48335e65210 100644
--- a/src/version.rs
+++ b/src/version.rs
@@ -1,4 +1,5 @@
-use std::collections::HashMap;
+use std::collections::{HashMap, BTreeSet};
+use std::str::FromStr;
use conduit::{Request, Response};
use conduit_router::RequestParams;
@@ -6,8 +7,7 @@ use pg::GenericConnection;
use pg::rows::Row;
use rustc_serialize::json;
use semver;
-use time::Duration;
-use time::Timespec;
+use time::{self, Duration, Timespec};
use url;
use {Model, Crate, User};
@@ -19,7 +19,7 @@ use git;
use upload;
use user::RequestUser;
use owner::{rights, Rights};
-use util::{RequestUtils, CargoResult, ChainError, internal, human};
+use util::{RequestUtils, CargoResult, ChainError, internal, human, CargoError};
#[derive(Clone)]
pub struct Version {
@@ -57,6 +57,60 @@ pub struct VersionLinks {
pub dependencies: String,
pub version_downloads: String,
pub authors: String,
+ pub build_info: String,
+}
+
+#[derive(RustcEncodable, RustcDecodable, Default)]
+pub struct EncodableBuildInfo {
+ id: i32,
+ pub ordering: HashMap>,
+ pub stable: HashMap>,
+ pub beta: HashMap>,
+ pub nightly: HashMap>,
+}
+
+pub enum ChannelVersion {
+ Stable(semver::Version),
+ Beta(Timespec),
+ Nightly(Timespec),
+}
+
+impl FromStr for ChannelVersion {
+ type Err = Box;
+
+ fn from_str(s: &str) -> CargoResult {
+ // Recognized formats:
+ // rustc 1.14.0 (e8a012324 2016-12-16)
+ // rustc 1.15.0-beta.5 (10893a9a3 2017-01-19)
+ // rustc 1.16.0-nightly (df8debf6d 2017-01-25)
+
+ let pieces: Vec<_> = s.split(&[' ', '(', ')'][..])
+ .filter(|s| !s.trim().is_empty())
+ .collect();
+ if pieces.len() != 4 {
+ return Err(human(format!(
+ "rust_version `{}` not recognized; \
+ expected format like `rustc X.Y.Z (SHA YYYY-MM-DD)`",
+ s
+ )));
+ }
+
+ if pieces[1].contains("nightly") {
+ Ok(ChannelVersion::Nightly(time::strptime(pieces[3], "%Y-%m-%d")?.to_timespec()))
+ } else if pieces[1].contains("beta") {
+ Ok(ChannelVersion::Beta(time::strptime(pieces[3], "%Y-%m-%d")?.to_timespec()))
+ } else {
+ let v = semver::Version::parse(pieces[1])?;
+ if v.pre.is_empty() {
+ Ok(ChannelVersion::Stable(v))
+ } else {
+ return Err(human(format!(
+ "rust_version `{}` not recognized as nightly, beta, or stable",
+ s
+ )));
+ }
+ }
+ }
}
impl Version {
@@ -117,6 +171,7 @@ impl Version {
version_downloads: format!("/api/v1/crates/{}/{}/downloads",
crate_name, num),
authors: format!("/api/v1/crates/{}/{}/authors", crate_name, num),
+ build_info: format!("/api/v1/crates/{}/{}/build_info", crate_name, num),
},
}
}
@@ -194,6 +249,72 @@ impl Version {
&[&yanked, &self.id]));
Ok(())
}
+
+ pub fn store_build_info(&self,
+ conn: &GenericConnection,
+ info: upload::VersionBuildInfo,
+ krate: &Crate) -> CargoResult<()> {
+
+ // Verify specified Rust version will parse before doing any inserting
+ let channel_version = info.channel_version()?;
+
+ // Future improvement: allow overwriting an existing row, in which case
+ // the ON CONFLICT DO should set `passed` instead of doing nothing.
+ let inserted = conn.execute("INSERT INTO build_info \
+ (version_id, rust_version, target, passed) \
+ VALUES ($1, $2, $3, $4) \
+ ON CONFLICT (version_id, rust_version, target) DO NOTHING",
+ &[&self.id, &info.rust_version, &info.target, &info.passed])?;
+
+ if inserted == 0 {
+ return Err(human(format!(
+ "Build info already specified for {} v{} with {} and target {}",
+ krate.name, self.num, info.rust_version, info.target)));
+ }
+
+ if info.passed && self.num == krate.max_version {
+ match channel_version {
+ ChannelVersion::Nightly(date) => {
+ let should_update = krate.max_build_info_nightly.as_ref().map(|max| {
+ date > *max
+ }).unwrap_or(true);
+
+ if should_update {
+ conn.execute("UPDATE crates \
+ SET max_build_info_nightly = $1 \
+ WHERE id = $2",
+ &[&date, &krate.id])?;
+ }
+ },
+ ChannelVersion::Beta(date) => {
+ let should_update = krate.max_build_info_beta.as_ref().map(|max| {
+ date > *max
+ }).unwrap_or(true);
+
+ if should_update {
+ conn.execute("UPDATE crates \
+ SET max_build_info_beta = $1 \
+ WHERE id = $2",
+ &[&date, &krate.id])?;
+ }
+ },
+ ChannelVersion::Stable(vers) => {
+ let should_update = krate.max_build_info_stable.as_ref().map(|max| {
+ vers > *max
+ }).unwrap_or(true);
+
+ if should_update {
+ conn.execute("UPDATE crates \
+ SET max_build_info_stable = $1 \
+ WHERE id = $2",
+ &[&vers.to_string(), &krate.id])?;
+ }
+ },
+ }
+ }
+
+ Ok(())
+ }
}
impl Model for Version {
@@ -324,6 +445,73 @@ pub fn downloads(req: &mut Request) -> CargoResult {
Ok(req.json(&R{ version_downloads: downloads }))
}
+/// Handles the `GET /crates/:crate_id/:version/build_info` route.
+pub fn build_info(req: &mut Request) -> CargoResult {
+ let (version, _) = try!(version_and_crate(req));
+
+ let tx = try!(req.tx());
+
+ let stmt = try!(tx.prepare("SELECT * FROM build_info
+ WHERE version_id = $1"));
+ let rows = stmt.query(&[&version.id])?;
+
+ let mut build_info = EncodableBuildInfo::default();
+ build_info.id = version.id;
+ let mut stables = BTreeSet::new();
+ let mut betas = BTreeSet::new();
+ let mut nightlies = BTreeSet::new();
+
+ for row in &rows {
+ let rust_version: String = row.get("rust_version");
+ let rust_version: ChannelVersion = rust_version.parse()?;
+ let target = row.get("target");
+ let passed = row.get("passed");
+
+ match rust_version {
+ ChannelVersion::Stable(semver) => {
+ let key = semver.to_string();
+ stables.insert(semver);
+ build_info.stable.entry(key)
+ .or_insert_with(HashMap::new)
+ .insert(target, passed);
+ }
+ ChannelVersion::Beta(date) => {
+ let key = ::encode_time(date);
+ betas.insert(date);
+ build_info.beta.entry(key)
+ .or_insert_with(HashMap::new)
+ .insert(target, passed);
+ }
+ ChannelVersion::Nightly(date) => {
+ let key = ::encode_time(date);
+ nightlies.insert(date);
+ build_info.nightly.entry(key)
+ .or_insert_with(HashMap::new)
+ .insert(target, passed);
+ }
+ }
+ }
+
+ build_info.ordering.insert(
+ String::from("stable"),
+ stables.into_iter().map(|s| s.to_string()).collect()
+ );
+
+ build_info.ordering.insert(
+ String::from("beta"),
+ betas.into_iter().map(::encode_time).collect()
+ );
+
+ build_info.ordering.insert(
+ String::from("nightly"),
+ nightlies.into_iter().map(::encode_time).collect()
+ );
+
+ #[derive(RustcEncodable)]
+ struct R { build_info: EncodableBuildInfo }
+ Ok(req.json(&R{ build_info: build_info }))
+}
+
/// Handles the `GET /crates/:crate_id/:version/authors` route.
pub fn authors(req: &mut Request) -> CargoResult {
let (version, _) = try!(version_and_crate(req));
@@ -371,3 +559,26 @@ fn modify_yank(req: &mut Request, yanked: bool) -> CargoResult {
struct R { ok: bool }
Ok(req.json(&R{ ok: true }))
}
+
+/// Handles the `POST /crates/:crate_id/:version/build_info` route.
+pub fn publish_build_info(req: &mut Request) -> CargoResult {
+ let mut body = String::new();
+ try!(req.body().read_to_string(&mut body));
+ let info: upload::VersionBuildInfo = try!(json::decode(&body).map_err(|e| {
+ human(format!("invalid upload request: {:?}", e))
+ }));
+
+ let (version, krate) = try!(version_and_crate(req));
+ let user = try!(req.user());
+ let tx = try!(req.tx());
+ let owners = try!(krate.owners(tx));
+ if try!(rights(req.app(), &owners, &user)) < Rights::Publish {
+ return Err(human("must already be an owner to publish build info"))
+ }
+
+ version.store_build_info(tx, info, &krate)?;
+
+ #[derive(RustcEncodable)]
+ struct R { ok: bool }
+ Ok(req.json(&R{ ok: true }))
+}