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/controllers/crate/build-info.js b/app/controllers/crate/build-info.js
new file mode 100644
index 00000000000..d27182af5ee
--- /dev/null
+++ b/app/controllers/crate/build-info.js
@@ -0,0 +1,49 @@
+import Ember from 'ember';
+
+const { computed } = Ember;
+
+function flattenBuildInfo(buildOrdering, builds) {
+ if (!buildOrdering || !builds) {
+ return [];
+ }
+
+ return buildOrdering.map(version => {
+ const thisVersion = builds[version];
+
+ return {
+ version,
+ 'x86_64-apple-darwin': thisVersion['x86_64-apple-darwin'],
+ 'x86_64-pc-windows-gnu': thisVersion['x86_64-pc-windows-gnu'],
+ 'x86_64-pc-windows-msvc': thisVersion['x86_64-pc-windows-msvc'],
+ 'x86_64-unknown-linux-gnu': thisVersion['x86_64-unknown-linux-gnu'],
+ };
+ });
+}
+
+export default Ember.Controller.extend({
+ id: computed.alias('model.crate.id'),
+ name: computed.alias('model.crate.name'),
+ version: computed.alias('model.num'),
+ build_info: computed.alias('model.build_info'),
+ stable_build: computed('build_info.ordering.stable', 'build_info.stable', function() {
+ const ordering = this.get('build_info.ordering.stable');
+ const stable = this.get('build_info.stable');
+
+ return flattenBuildInfo(ordering, stable);
+ }),
+ beta_build: computed('build_info.ordering.beta', 'build_info.beta', function() {
+ const ordering = this.get('build_info.ordering.beta');
+ const beta = this.get('build_info.beta');
+
+ return flattenBuildInfo(ordering, beta);
+ }),
+ nightly_build: computed('build_info.ordering.nightly', 'build_info.nightly', function() {
+ const ordering = this.get('build_info.ordering.nightly');
+ const nightly = this.get('build_info.nightly');
+
+ return flattenBuildInfo(ordering, nightly);
+ }),
+ has_stable_builds: computed.gt('stable_build.length', 0),
+ has_beta_builds: computed.gt('beta_build.length', 0),
+ has_nightly_builds: computed.gt('nightly_build.length', 0),
+});
diff --git a/app/helpers/build-info-pass-fail.js b/app/helpers/build-info-pass-fail.js
new file mode 100644
index 00000000000..200199fc2d5
--- /dev/null
+++ b/app/helpers/build-info-pass-fail.js
@@ -0,0 +1,13 @@
+import Ember from 'ember';
+
+export function buildInfoPassFail(status) {
+ if (status === true) {
+ return '✅ Pass';
+ } else if (status === false) {
+ return '❌ Fail';
+ } else {
+ return '';
+ }
+}
+
+export default Ember.Helper.helper(params => buildInfoPassFail(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/target-icons.js b/app/helpers/target-icons.js
new file mode 100644
index 00000000000..bd98ae6a70f
--- /dev/null
+++ b/app/helpers/target-icons.js
@@ -0,0 +1,10 @@
+import Ember from 'ember';
+
+export function targetIcons(targets) {
+ return Ember.String.htmlSafe(targets.map(function(target) {
+ const filename = target.split(' ')[0].toLowerCase();
+ return `
`;
+ }).join(''));
+}
+
+export default Ember.Helper.helper(params => targetIcons(params[0]));
diff --git a/app/models/build-info.js b/app/models/build-info.js
new file mode 100644
index 00000000000..a2e1d5ea516
--- /dev/null
+++ b/app/models/build-info.js
@@ -0,0 +1,58 @@
+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 caseInsensitive = (a, b) => a.toLowerCase().localeCompare(b.toLowerCase());
+
+export default DS.Model.extend({
+ version: DS.belongsTo('version', { async: true }),
+ ordering: DS.attr(),
+ stable: DS.attr(),
+ beta: DS.attr(),
+ nightly: DS.attr(),
+
+ latest_positive_results: Ember.computed('ordering', 'stable', 'beta', 'nightly', function() {
+ const passingTargets = results => (
+ Object.entries(results)
+ .filter(([, value]) => value === true)
+ .map(([key]) => TIER1[key])
+ .sort(caseInsensitive)
+ );
+
+ const positiveResults = (versionOrdering, channelResults) => {
+ const latestVersion = versionOrdering[versionOrdering.length - 1];
+ const latestResults = channelResults[latestVersion] || {};
+ return [latestVersion, passingTargets(latestResults)];
+ };
+
+ let results = {};
+
+ const addChannelToResults = (key) => {
+ const channelOrdering = this.get('ordering')[key];
+ const channelResults = this.get(key);
+
+ const [version, targets] = positiveResults(channelOrdering, channelResults);
+
+ if (targets.length > 0) {
+ results[key] = { version, targets };
+ }
+ };
+
+ addChannelToResults('stable');
+ addChannelToResults('beta');
+ addChannelToResults('nightly');
+
+ return results;
+ }),
+
+ has_any_positive_results: Ember.computed('latest_positive_results', function() {
+ const results = this.get('latest_positive_results');
+ return Object.keys(results).length > 0;
+ }),
+});
diff --git a/app/models/crate.js b/app/models/crate.js
index 9bd5893ad33..9d2e40fd78f 100644
--- a/app/models/crate.js
+++ b/app/models/crate.js
@@ -17,6 +17,9 @@ export default DS.Model.extend({
documentation: DS.attr('string'),
repository: DS.attr('string'),
exact_match: DS.attr('boolean'),
+ 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 3da95b4300d..6d02500a208 100644
--- a/app/models/version.js
+++ b/app/models/version.js
@@ -15,6 +15,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/router.js b/app/router.js
index fb1be2e9a59..53015a9de93 100644
--- a/app/router.js
+++ b/app/router.js
@@ -18,6 +18,7 @@ Router.map(function() {
this.route('download');
this.route('versions');
this.route('version', { path: '/:version_num' });
+ this.route('build_info', { path: '/:version_num/build_info' });
this.route('reverse_dependencies');
diff --git a/app/routes/crate/build_info.js b/app/routes/crate/build_info.js
new file mode 100644
index 00000000000..e31b685aa2e
--- /dev/null
+++ b/app/routes/crate/build_info.js
@@ -0,0 +1,24 @@
+import Ember from 'ember';
+
+export default Ember.Route.extend({
+ model(params) {
+ const requestedVersion = params.version_num;
+ const crate = this.modelFor('crate');
+
+ // Find version model
+ return crate.get('versions')
+ .then(versions => {
+ const version = versions.find(version => version.get('num') === requestedVersion);
+ if (!version) {
+ this.controllerFor('application').set('nextFlashError',
+ `Version '${requestedVersion}' of crate '${crate.get('name')}' does not exist`);
+ }
+ return version;
+ });
+ },
+
+ serialize(model) {
+ let version_num = model ? model.get('num') : '';
+ return { version_num };
+ },
+});
diff --git a/app/styles/crate.scss b/app/styles/crate.scss
index 5eae23ec0c5..00dd2aa9f49 100644
--- a/app/styles/crate.scss
+++ b/app/styles/crate.scss
@@ -352,6 +352,35 @@
}
}
+.build-info {
+ .build-info-channel {
+ margin: 10px 0;
+ img {
+ width: 20px;
+ margin-bottom: -3px;
+ }
+ }
+}
+
+#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..f0a6cb8d356
--- /dev/null
+++ b/app/templates/components/badge-build-info.hbs
@@ -0,0 +1,6 @@
+{{#if build_info}}
+
+{{/if}}
diff --git a/app/templates/components/crate-row.hbs b/app/templates/components/crate-row.hbs
index 8337a333805..12b06c61421 100644
--- a/app/templates/components/crate-row.hbs
+++ b/app/templates/components/crate-row.hbs
@@ -11,6 +11,7 @@
{{#each crate.annotated_badges as |badge|}}
{{component badge.component_name badge=badge data-test-badge=badge.badge_type}}
{{/each}}
+ {{badge-build-info crate=crate}}
diff --git a/app/templates/crate/build-info.hbs b/app/templates/crate/build-info.hbs
new file mode 100644
index 00000000000..05b337d6d2c
--- /dev/null
+++ b/app/templates/crate/build-info.hbs
@@ -0,0 +1,83 @@
+{{title name}}
+
+
+ {{#link-to 'crate' id}}⬅ Back to {{ name }}{{/link-to}}
+
+
+
+
+
+
![](/assets/crate.svg)
+
Build info for {{ name }}
+
{{ version }}
+
+
+
+
+
+ {{#if has_stable_builds}}
+
Stable channel
+
+
+ Rust Version |
+ Linux 64 bit |
+ macOS 64 bit |
+ Windows (GNU) 64 bit |
+ Windows (MSVC) 64 bit |
+
+ {{#each stable_build as |build|}}
+
+ {{ build.version }} |
+ {{ build-info-pass-fail build.x86_64-unknown-linux-gnu }} |
+ {{ build-info-pass-fail build.x86_64-apple-darwin }} |
+ {{ build-info-pass-fail build.x86_64-pc-windows-gnu }} |
+ {{ build-info-pass-fail build.x86_64-pc-windows-msvc }} |
+
+ {{/each}}
+
+ {{/if}}
+
+ {{#if has_beta_builds}}
+
Beta channel
+
+
+ Rust Version |
+ Linux 64 bit |
+ macOS 64 bit |
+ Windows (GNU) 64 bit |
+ Windows (MSVC) 64 bit |
+
+ {{#each beta_build as |build|}}
+
+ {{ format-day build.version }} |
+ {{ build-info-pass-fail build.x86_64-unknown-linux-gnu }} |
+ {{ build-info-pass-fail build.x86_64-apple-darwin }} |
+ {{ build-info-pass-fail build.x86_64-pc-windows-gnu }} |
+ {{ build-info-pass-fail build.x86_64-pc-windows-msvc }} |
+
+ {{/each}}
+
+ {{/if}}
+
+ {{#if has_nightly_builds}}
+
Nightly channel
+
+
+ Rust Version |
+ Linux 64 bit |
+ macOS 64 bit |
+ Windows (GNU) 64 bit |
+ Windows (MSVC) 64 bit |
+
+ {{#each nightly_build as |build|}}
+
+ {{ format-day build.version }} |
+ {{ build-info-pass-fail build.x86_64-unknown-linux-gnu }} |
+ {{ build-info-pass-fail build.x86_64-apple-darwin }} |
+ {{ build-info-pass-fail build.x86_64-pc-windows-gnu }} |
+ {{ build-info-pass-fail build.x86_64-pc-windows-msvc }} |
+
+ {{/each}}
+
+ {{/if}}
+
diff --git a/app/templates/crate/version.hbs b/app/templates/crate/version.hbs
index 32f6cb4bf06..5af909a80d0 100644
--- a/app/templates/crate/version.hbs
+++ b/app/templates/crate/version.hbs
@@ -123,6 +123,37 @@
{{/each}}
+ {{#if currentVersion.build_info.has_any_positive_results }}
+
+
Works on
+
+ {{#if currentVersion.build_info.latest_positive_results.stable }}
+
+ Stable {{ currentVersion.build_info.latest_positive_results.stable.version }}
+ on {{ target-icons currentVersion.build_info.latest_positive_results.stable.targets }}
+
+ {{/if}}
+
+ {{#if currentVersion.build_info.latest_positive_results.beta }}
+
+ Beta {{ format-day currentVersion.build_info.latest_positive_results.beta.version }}
+ on {{ target-icons currentVersion.build_info.latest_positive_results.beta.targets }}
+
+ {{/if}}
+
+ {{#if currentVersion.build_info.latest_positive_results.nightly }}
+
+ Nightly {{ format-day currentVersion.build_info.latest_positive_results.nightly.version }}
+ on {{ target-icons currentVersion.build_info.latest_positive_results.nightly.targets }}
+
+ {{/if}}
+
+ {{#link-to 'crate.build_info' currentVersion.num}}
+ More build info
+ {{/link-to}}
+
+ {{/if}}
+
Authors
diff --git a/migrations/20170309163659_add_build_info/down.sql b/migrations/20170309163659_add_build_info/down.sql
new file mode 100644
index 00000000000..37165aa7212
--- /dev/null
+++ b/migrations/20170309163659_add_build_info/down.sql
@@ -0,0 +1 @@
+DROP TABLE build_info;
diff --git a/migrations/20170309163659_add_build_info/up.sql b/migrations/20170309163659_add_build_info/up.sql
new file mode 100644
index 00000000000..bef524f93f2
--- /dev/null
+++ b/migrations/20170309163659_add_build_info/up.sql
@@ -0,0 +1,10 @@
+CREATE TABLE 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(),
+ PRIMARY KEY (version_id, rust_version, target)
+);
+SELECT diesel_manage_updated_at('build_info');
diff --git a/public/assets/linux.svg b/public/assets/linux.svg
new file mode 100644
index 00000000000..fb432127fa1
--- /dev/null
+++ b/public/assets/linux.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/public/assets/macos.svg b/public/assets/macos.svg
new file mode 100644
index 00000000000..a764263ee9d
--- /dev/null
+++ b/public/assets/macos.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/public/assets/windows.svg b/public/assets/windows.svg
new file mode 100644
index 00000000000..b261954f55f
--- /dev/null
+++ b/public/assets/windows.svg
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src/controllers/krate/metadata.rs b/src/controllers/krate/metadata.rs
index 6764fd0be29..49b84719d02 100644
--- a/src/controllers/krate/metadata.rs
+++ b/src/controllers/krate/metadata.rs
@@ -35,7 +35,7 @@ pub fn summary(req: &mut Request) -> CargoResult {
.map(|versions| Version::max(versions.into_iter().map(|v| v.num)))
.zip(krates)
.map(|(max_version, krate)| {
- Ok(krate.minimal_encodable(&max_version, None, false, None))
+ Ok(krate.minimal_encodable(&max_version, None, false, None, None))
})
.collect()
};
@@ -152,6 +152,7 @@ pub fn show(req: &mut Request) -> CargoResult {
Some(badges),
false,
recent_downloads,
+ None,
),
versions: versions
.into_iter()
diff --git a/src/controllers/krate/publish.rs b/src/controllers/krate/publish.rs
index 00b3968fb64..7b4fa83782b 100644
--- a/src/controllers/krate/publish.rs
+++ b/src/controllers/krate/publish.rs
@@ -178,7 +178,7 @@ pub fn publish(req: &mut Request) -> CargoResult {
warnings: Warnings<'a>,
}
Ok(req.json(&R {
- krate: krate.minimal_encodable(&max_version, None, false, None),
+ krate: krate.minimal_encodable(&max_version, None, false, None, None),
warnings: warnings,
}))
})
diff --git a/src/controllers/krate/search.rs b/src/controllers/krate/search.rs
index 5fcc81af025..074fbab4533 100644
--- a/src/controllers/krate/search.rs
+++ b/src/controllers/krate/search.rs
@@ -5,7 +5,7 @@ use diesel_full_text_search::*;
use controllers::helpers::Paginate;
use controllers::prelude::*;
use views::EncodableCrate;
-use models::{Badge, Crate, OwnerKind, Version};
+use models::{Badge, BuildInfo, Crate, OwnerKind, Version};
use schema::*;
use models::krate::{canon_crate_name, ALL_COLUMNS};
@@ -178,28 +178,41 @@ pub fn search(req: &mut Request) -> CargoResult {
.load::(&*conn)?
.grouped_by(&crates)
.into_iter()
- .map(|versions| Version::max(versions.into_iter().map(|v| v.num)));
+ .map(|versions| versions.into_iter().max_by(Version::semver_cmp).unwrap())
+ .collect::>();
+
+ let build_infos = BuildInfo::belonging_to(&versions)
+ .filter(build_info::passed.eq(true))
+ .select(::version::build_info::BUILD_INFO_FIELDS)
+ .load::(&*conn)?
+ .grouped_by(&versions)
+ .into_iter()
+ .map(BuildInfo::max);
let crates = versions
+ .into_iter()
.zip(crates)
.zip(perfect_matches)
.zip(recent_downloads)
+ .zip(build_infos)
.map(
- |(((max_version, krate), perfect_match), recent_downloads)| {
+ |((((max_version, krate), perfect_match), recent_downloads), build_info)| {
+ let build_info = build_info?;
// FIXME: If we add crate_id to the Badge enum we can eliminate
// this N+1
let badges = badges::table
.filter(badges::crate_id.eq(krate.id))
.load::(&*conn)?;
Ok(krate.minimal_encodable(
- &max_version,
+ &max_version.num,
Some(badges),
perfect_match,
Some(recent_downloads),
+ Some(build_info.encode()),
))
},
)
- .collect::>()?;
+ .collect::>()?;
#[derive(Serialize)]
struct R {
diff --git a/src/lib.rs b/src/lib.rs
index 62ae19638ad..66d02ee9624 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -150,6 +150,10 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder {
"/crates/:crate_id/:version/download",
C(version::downloads::download),
);
+ api_router.put(
+ "/crates/:crate_id/:version/build_info",
+ C(version::build_info::publish_build_info),
+ );
// Routes that appear to be unused
api_router.get("/versions", C(version::deprecated::index));
@@ -174,6 +178,10 @@ pub fn middleware(app: Arc) -> MiddlewareBuilder {
"/crates/:crate_id/:version/authors",
C(version::metadata::authors),
);
+ api_router.get(
+ "/crates/:crate_id/:version/build_info",
+ C(version::metadata::build_info),
+ );
api_router.get(
"/crates/:crate_id/downloads",
C(controllers::krate::downloads::downloads),
diff --git a/src/models/krate.rs b/src/models/krate.rs
index b430e7eadf4..30dc2deef46 100644
--- a/src/models/krate.rs
+++ b/src/models/krate.rs
@@ -9,7 +9,7 @@ use url::Url;
use app::App;
use util::{human, CargoResult};
-use views::{EncodableCrate, EncodableCrateLinks};
+use views::{EncodableCrate, EncodableCrateLinks, EncodableMaxVersionBuildInfo};
use models::{Badge, Category, CrateOwner, Keyword, NewCrateOwnerInvitation, Owner, OwnerKind,
ReverseDependency, User, Version};
@@ -277,6 +277,7 @@ impl Crate {
badges: Option>,
exact_match: bool,
recent_downloads: Option,
+ max_build_info: Option,
) -> EncodableCrate {
self.encodable(
max_version,
@@ -286,6 +287,7 @@ impl Crate {
badges,
exact_match,
recent_downloads,
+ max_build_info,
)
}
@@ -299,6 +301,7 @@ impl Crate {
badges: Option>,
exact_match: bool,
recent_downloads: Option,
+ max_build_info: Option,
) -> EncodableCrate {
let Crate {
name,
@@ -319,6 +322,7 @@ impl Crate {
let category_ids = categories.map(|cats| cats.iter().map(|cat| cat.slug.clone()).collect());
let badges = badges.map(|bs| bs.into_iter().map(|b| b.encodable()).collect());
let documentation = Crate::remove_blacklisted_documentation_urls(documentation);
+ let max_build_info = max_build_info.unwrap_or_else(EncodableMaxVersionBuildInfo::default);
EncodableCrate {
id: name.clone(),
@@ -332,6 +336,9 @@ impl Crate {
categories: category_ids,
badges: badges,
max_version: max_version.to_string(),
+ max_build_info_stable: max_build_info.stable,
+ max_build_info_beta: max_build_info.beta,
+ max_build_info_nightly: max_build_info.nightly,
documentation: documentation,
homepage: homepage,
exact_match: exact_match,
diff --git a/src/models/mod.rs b/src/models/mod.rs
index 6c5a2a06b24..d9c6174d590 100644
--- a/src/models/mod.rs
+++ b/src/models/mod.rs
@@ -12,6 +12,7 @@ pub use self::team::{NewTeam, Team};
pub use user::{Email, NewUser, User};
pub use token::ApiToken;
pub use version::{NewVersion, Version};
+pub use version::build_info::BuildInfo;
pub mod helpers;
diff --git a/src/schema.rs b/src/schema.rs
index 758d64a4ca0..ec79751db76 100644
--- a/src/schema.rs
+++ b/src/schema.rs
@@ -68,6 +68,50 @@ table! {
}
}
+table! {
+ /// Representation of the `build_info` table.
+ ///
+ /// (Automatically generated by Diesel.)
+ build_info (version_id, rust_version, target) {
+ /// The `version_id` column of the `build_info` table.
+ ///
+ /// Its SQL type is `Int4`.
+ ///
+ /// (Automatically generated by Diesel.)
+ version_id -> Int4,
+ /// The `rust_version` column of the `build_info` table.
+ ///
+ /// Its SQL type is `Varchar`.
+ ///
+ /// (Automatically generated by Diesel.)
+ rust_version -> Varchar,
+ /// The `target` column of the `build_info` table.
+ ///
+ /// Its SQL type is `Varchar`.
+ ///
+ /// (Automatically generated by Diesel.)
+ target -> Varchar,
+ /// The `passed` column of the `build_info` table.
+ ///
+ /// Its SQL type is `Bool`.
+ ///
+ /// (Automatically generated by Diesel.)
+ passed -> Bool,
+ /// The `created_at` column of the `build_info` table.
+ ///
+ /// Its SQL type is `Timestamp`.
+ ///
+ /// (Automatically generated by Diesel.)
+ created_at -> Timestamp,
+ /// The `updated_at` column of the `build_info` table.
+ ///
+ /// Its SQL type is `Timestamp`.
+ ///
+ /// (Automatically generated by Diesel.)
+ updated_at -> Timestamp,
+ }
+}
+
table! {
/// Representation of the `categories` table.
///
@@ -811,6 +855,7 @@ joinable!(versions -> crates (crate_id));
allow_tables_to_appear_in_same_query!(
api_tokens,
badges,
+ build_info,
categories,
crate_downloads,
crate_owner_invitations,
diff --git a/src/tests/version.rs b/src/tests/version.rs
index 32e19897fc1..e1fc45b6ffb 100644
--- a/src/tests/version.rs
+++ b/src/tests/version.rs
@@ -8,7 +8,7 @@ use serde_json::Value;
use conduit::{Handler, Method};
use self::diesel::prelude::*;
-use views::EncodableVersion;
+use views::{EncodableVersion, EncodableVersionBuildInfo};
use schema::versions;
#[derive(Deserialize)]
@@ -109,3 +109,197 @@ fn record_rerendered_readme_time() {
version.record_readme_rendering(&conn).unwrap();
}
}
+
+#[test]
+fn publish_build_info() {
+ #[derive(Deserialize)]
+ struct O {
+ ok: bool,
+ }
+ let (_b, app, middle) = ::app();
+
+ let mut req = ::new_req(Arc::clone(&app), "publish-build-info", "1.0.0");
+
+ {
+ let conn = app.diesel_database.get().unwrap();
+ let user = ::new_user("foo").create_or_update(&conn).unwrap();
+ ::CrateBuilder::new("publish-build-info", user.id)
+ .version("1.0.0")
+ .expect_build(&conn);
+ ::sign_in_as(&mut req, &user);
+ }
+
+ let body = r#"{
+ "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":false}"#;
+
+ 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 = r#"{
+ "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 = r#"{
+ "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 = r#"{
+ "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(Deserialize)]
+ struct R {
+ build_info: EncodableVersionBuildInfo,
+ }
+
+ let json = ::json::(&mut response);
+
+ let nightly_key_string = String::from("2017-01-25");
+ assert_eq!(
+ json.build_info.ordering.get("nightly"),
+ Some(&vec![nightly_key_string.clone()])
+ );
+ assert_eq!(
+ json.build_info
+ .nightly
+ .keys()
+ .map(ToString::to_string)
+ .collect::>(),
+ vec![nightly_key_string]
+ );
+
+ let beta_key_string = String::from("2017-01-20");
+ assert_eq!(
+ json.build_info.ordering.get("beta"),
+ Some(&vec![beta_key_string.clone()])
+ );
+ assert_eq!(
+ json.build_info
+ .beta
+ .keys()
+ .map(ToString::to_string)
+ .collect::>(),
+ vec![beta_key_string]
+ );
+
+ let stable_key_string = String::from("1.13.0");
+ assert_eq!(
+ json.build_info.ordering.get("stable"),
+ Some(&vec![stable_key_string.clone()])
+ );
+ assert_eq!(
+ json.build_info
+ .stable
+ .keys()
+ .map(ToString::to_string)
+ .collect::>(),
+ vec![stable_key_string]
+ );
+}
+
+#[test]
+fn bad_rust_version_publish_build_info() {
+ let (_b, app, middle) = ::app();
+
+ let mut req = ::new_req(Arc::clone(&app), "bad-rust-vers", "1.0.0");
+
+ {
+ let conn = app.diesel_database.get().unwrap();
+ let user = ::new_user("foo").create_or_update(&conn).unwrap();
+ ::CrateBuilder::new("bad-rust-vers", user.id)
+ .version("1.0.0")
+ .expect_build(&conn);
+ ::sign_in_as(&mut req, &user);
+ }
+
+ let body = r#"{
+ "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,
+ "invalid upload request: rust_version `rustc 1.16.0-dev (df8debf6d 2017-01-25)` not \
+ recognized as nightly, beta, or stable at line 4 column 64"
+ );
+
+ let body = r#"{
+ "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,
+ "invalid upload request: rust_version `1.15.0` not recognized; expected format like `rustc X.Y.Z (SHA YYYY-MM-DD)` at line 4 column 31"
+ );
+}
diff --git a/src/version/build_info.rs b/src/version/build_info.rs
new file mode 100644
index 00000000000..8a68a5501c4
--- /dev/null
+++ b/src/version/build_info.rs
@@ -0,0 +1,129 @@
+use std::str::FromStr;
+
+use chrono::{DateTime, NaiveDate, Utc};
+use conduit::{Request, Response};
+use semver;
+use serde_json;
+
+use app::RequestApp;
+use db::RequestTransaction;
+use models::{Rights, Version};
+use user::RequestUser;
+use util::{human, CargoResult, RequestUtils};
+use version::version_and_crate;
+use views::{EncodableMaxVersionBuildInfo, EncodableVersionBuildInfoUpload,
+ ParsedRustChannelVersion};
+
+use schema::*;
+
+#[derive(Clone, Identifiable, Associations, Debug, Queryable)]
+#[belongs_to(Version)]
+#[table_name = "build_info"]
+#[primary_key(version_id, rust_version, target)]
+/// Stores information about whether this version built on the specified Rust version and target.
+pub struct BuildInfo {
+ version_id: i32,
+ pub rust_version: String,
+ pub target: String,
+ pub passed: bool,
+}
+
+/// The columns to select from the `build_info` table. The table also stores `created_at` and
+/// `updated_at` metadata for each row, but we're not displaying those anywhere so we're not
+/// bothering to select them.
+pub const BUILD_INFO_FIELDS: (
+ build_info::version_id,
+ build_info::rust_version,
+ build_info::target,
+ build_info::passed,
+) = (
+ build_info::version_id,
+ build_info::rust_version,
+ build_info::target,
+ build_info::passed,
+);
+
+#[derive(Debug)]
+/// The maximum version of Rust from each channel that a crate version successfully builds with.
+/// Used for summarizing this information in badge form on crate list pages.
+pub struct MaxBuildInfo {
+ pub stable: Option,
+ pub beta: Option,
+ pub nightly: Option,
+}
+
+impl MaxBuildInfo {
+ /// Encode stable semver number as a string and beta and nightly as times appropriate for
+ /// JSON.
+ pub fn encode(self) -> EncodableMaxVersionBuildInfo {
+ fn naive_date_to_rfc3339(date: NaiveDate) -> String {
+ DateTime::::from_utc(date.and_hms(0, 0, 0), Utc).to_rfc3339()
+ }
+
+ EncodableMaxVersionBuildInfo {
+ stable: self.stable.map(|v| v.to_string()),
+ beta: self.beta.map(naive_date_to_rfc3339),
+ nightly: self.nightly.map(naive_date_to_rfc3339),
+ }
+ }
+}
+
+impl BuildInfo {
+ /// From a set of build information data, Find the largest or latest Rust versions that we know
+ /// about for each channel. Stable uses the largest semver version number; beta and nightly use
+ /// the latest date.
+ pub fn max(build_infos: I) -> CargoResult
+ where
+ I: IntoIterator- ,
+ {
+ let build_infos = build_infos
+ .into_iter()
+ .map(|bi| ParsedRustChannelVersion::from_str(&bi.rust_version))
+ .collect::, _>>()?;
+
+ let stable = build_infos
+ .iter()
+ .filter_map(ParsedRustChannelVersion::as_stable)
+ .max();
+ let beta = build_infos
+ .iter()
+ .filter_map(ParsedRustChannelVersion::as_beta)
+ .max();
+ let nightly = build_infos
+ .iter()
+ .filter_map(ParsedRustChannelVersion::as_nightly)
+ .max();
+
+ Ok(MaxBuildInfo {
+ stable: stable.cloned(),
+ beta: beta.cloned(),
+ nightly: nightly.cloned(),
+ })
+ }
+}
+
+/// Handles the `POST /crates/:crate_id/:version/build_info` route for the
+/// `cargo publish-build-info` command to report on which versions of Rust
+/// a crate builds with.
+pub fn publish_build_info(req: &mut Request) -> CargoResult {
+ let mut body = String::new();
+ req.body().read_to_string(&mut body)?;
+ let info: EncodableVersionBuildInfoUpload = serde_json::from_str(&body)
+ .map_err(|e| human(&format_args!("invalid upload request: {}", e)))?;
+
+ let (version, krate) = version_and_crate(req)?;
+ let user = req.user()?;
+ let tx = req.db_conn()?;
+ let owners = krate.owners(&tx)?;
+ if user.rights(req.app(), &owners)? < Rights::Publish {
+ return Err(human("must already be an owner to publish build info"));
+ }
+
+ version.store_build_info(&tx, info)?;
+
+ #[derive(Serialize)]
+ struct R {
+ ok: bool,
+ }
+ Ok(req.json(&R { ok: true }))
+}
diff --git a/src/version/metadata.rs b/src/version/metadata.rs
index 5f878914d6e..2e09d2ace48 100644
--- a/src/version/metadata.rs
+++ b/src/version/metadata.rs
@@ -1,18 +1,24 @@
//! Endpoints that expose metadata about crate versions
//!
-//! These endpoints provide data that could be obtained direclty from the
+//! These endpoints provide data that could be obtained directly from the
//! index or cached metadata which was extracted (client side) from the
-//! `Cargo.toml` file.
+//! `Cargo.toml` file, as well as some information stored in crates.io's
+//! database.
-use conduit::{Request, Response};
+use std::str::FromStr;
+use chrono::NaiveDate;
+use conduit::{Request, Response};
use diesel::prelude::*;
+
use db::RequestTransaction;
use util::{CargoResult, RequestUtils};
-use views::{EncodableDependency, EncodablePublicUser};
+use views::{EncodableDependency, EncodablePublicUser, EncodableVersionBuildInfo,
+ ParsedRustChannelVersion};
use schema::*;
+use super::build_info::BuildInfo;
use super::version_and_crate;
/// Handles the `GET /crates/:crate_id/:version/dependencies` route.
@@ -64,3 +70,115 @@ pub fn authors(req: &mut Request) -> CargoResult {
meta: Meta { names: names },
}))
}
+
+/// Handles the `GET /crates/:crate_id/:version/build_info` route.
+// We do not wish the frontend to understand how to sort Rust versions
+// (semver- *or* date-based), so we return two related pieces of
+// information: the ordering of all the releases in each channel and
+// the pass/fail for each platform for each (channel, version) pair.
+//
+// {
+// "build_info": {
+// "id": 1,
+// "ordering": {
+// "nightly": ["2017-07-26"],
+// "beta": ["2017-07-18"],
+// "stable": ["1.19.0"]
+// },
+// "stable": {
+// "1.19.0": { "x86_64-apple-darwin": false }
+// "1.17.0": { "x86_64-unknown-linux-gnu": true }
+// "1.18.0": { "x86_64-pc-windows-gnu": false }
+// },
+// "beta": {
+// "2017-07-18": { "x86_64-apple-darwin": false }
+// },
+// "nightly": {
+// "2017-07-26": { "x86_64-apple-darwin": true }
+// }
+// }
+// }
+pub fn build_info(req: &mut Request) -> CargoResult {
+ use std::collections::{BTreeSet, HashMap};
+
+ let (version, _) = version_and_crate(req)?;
+
+ let conn = req.db_conn()?;
+
+ let build_infos = BuildInfo::belonging_to(&version)
+ .select(::version::build_info::BUILD_INFO_FIELDS)
+ .load(&*conn)?;
+
+ let mut encodable_build_info = EncodableVersionBuildInfo::default();
+ encodable_build_info.id = version.id;
+ let mut stables = BTreeSet::new();
+ let mut betas = BTreeSet::new();
+ let mut nightlies = BTreeSet::new();
+
+ for row in build_infos {
+ let BuildInfo {
+ rust_version,
+ target,
+ passed,
+ ..
+ } = row;
+
+ let rust_version = ParsedRustChannelVersion::from_str(&rust_version)?;
+
+ match rust_version {
+ ParsedRustChannelVersion::Stable(semver) => {
+ let key = semver.to_string();
+ stables.insert(semver);
+ encodable_build_info
+ .stable
+ .entry(key)
+ .or_insert_with(HashMap::new)
+ .insert(target, passed);
+ }
+ ParsedRustChannelVersion::Beta(date) => {
+ betas.insert(date);
+ encodable_build_info
+ .beta
+ .entry(date)
+ .or_insert_with(HashMap::new)
+ .insert(target, passed);
+ }
+ ParsedRustChannelVersion::Nightly(date) => {
+ nightlies.insert(date);
+ encodable_build_info
+ .nightly
+ .entry(date)
+ .or_insert_with(HashMap::new)
+ .insert(target, passed);
+ }
+ }
+ }
+
+ encodable_build_info.ordering.insert(
+ String::from("stable"),
+ stables.into_iter().map(|s| s.to_string()).collect(),
+ );
+
+ fn naive_date_to_string(date: NaiveDate) -> String {
+ date.format("%Y-%m-%d").to_string()
+ }
+
+ encodable_build_info.ordering.insert(
+ String::from("beta"),
+ betas.into_iter().map(naive_date_to_string).collect(),
+ );
+
+ encodable_build_info.ordering.insert(
+ String::from("nightly"),
+ nightlies.into_iter().map(naive_date_to_string).collect(),
+ );
+
+ #[derive(Serialize, Debug)]
+ struct R {
+ build_info: EncodableVersionBuildInfo,
+ }
+
+ Ok(req.json(&R {
+ build_info: encodable_build_info,
+ }))
+}
diff --git a/src/version/mod.rs b/src/version/mod.rs
index d02bfeb8956..71dfd5c026b 100644
--- a/src/version/mod.rs
+++ b/src/version/mod.rs
@@ -13,12 +13,14 @@ use db::RequestTransaction;
use util::{human, CargoResult};
use license_exprs;
+pub mod build_info;
pub mod deprecated;
pub mod downloads;
pub mod metadata;
pub mod yank;
use models::{Crate, Dependency};
+use views::EncodableVersionBuildInfoUpload;
use schema::*;
// Queryable has a custom implementation below
@@ -69,6 +71,7 @@ pub struct EncodableVersionLinks {
pub dependencies: String,
pub version_downloads: String,
pub authors: String,
+ pub build_info: String,
}
impl Version {
@@ -101,6 +104,7 @@ impl Version {
dependencies: format!("/api/v1/crates/{}/{}/dependencies", crate_name, num),
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),
},
}
}
@@ -141,6 +145,39 @@ impl Version {
.set(rendered_at.eq(now))
.execute(conn)
}
+
+ /// When we receive a `POST /crates/:crate_id/:version/build_info` API request that tells us
+ /// about whether a particular version built successfully on a particular Rust version and
+ /// target (sent via `cargo publish-build-info`), store that information in the database.
+ /// Overwrites any previous results reported for the specified `(version_id, rust_version,
+ /// target)` combination.
+ pub fn store_build_info(
+ &self,
+ conn: &PgConnection,
+ info: EncodableVersionBuildInfoUpload,
+ ) -> CargoResult<()> {
+ use schema::build_info::dsl::*;
+ use diesel::pg::upsert::excluded;
+
+ diesel::insert_into(build_info)
+ .values((
+ version_id.eq(self.id),
+ rust_version.eq(info.rust_version.to_string()),
+ target.eq(info.target),
+ passed.eq(info.passed),
+ ))
+ .on_conflict(build_info.primary_key())
+ .do_update()
+ .set(passed.eq(excluded(passed)))
+ .execute(conn)?;
+
+ Ok(())
+ }
+
+ /// Orders SemVer numbers so that "higher" version numbers appear first.
+ pub fn semver_cmp(a: &Self, b: &Self) -> ::std::cmp::Ordering {
+ a.num.cmp(&b.num)
+ }
}
impl NewVersion {
diff --git a/src/views/mod.rs b/src/views/mod.rs
index 582bfc62130..50d8c0676f2 100644
--- a/src/views/mod.rs
+++ b/src/views/mod.rs
@@ -1,5 +1,10 @@
use std::collections::HashMap;
-use chrono::NaiveDateTime;
+use std::fmt;
+use std::str::FromStr;
+
+use chrono::{NaiveDate, NaiveDateTime};
+use semver;
+use serde::{self, Deserialize, Deserializer, Serialize, Serializer};
#[derive(PartialEq, Debug, Serialize, Deserialize)]
pub struct EncodableBadge {
@@ -7,6 +12,8 @@ pub struct EncodableBadge {
pub attributes: HashMap>,
}
+use util::errors::{human, CargoError, CargoResult};
+
#[derive(Serialize, Deserialize, Debug)]
pub struct EncodableCategory {
pub id: String,
@@ -79,6 +86,9 @@ pub struct EncodableCrate {
pub repository: Option,
pub links: EncodableCrateLinks,
pub exact_match: bool,
+ pub max_build_info_stable: Option,
+ pub max_build_info_beta: Option,
+ pub max_build_info_nightly: Option,
}
#[derive(Serialize, Deserialize, Debug)]
@@ -119,6 +129,145 @@ pub mod krate_publish;
pub use self::krate_publish::CrateDependency as EncodableCrateDependency;
pub use self::krate_publish::NewCrate as EncodableCrateUpload;
+/// Information about whether this version built on the specified Rust version and target, as
+/// uploaded by the `cargo publish-build-info` command.
+#[derive(Debug, Serialize, Deserialize)]
+pub struct EncodableVersionBuildInfoUpload {
+ pub rust_version: EncodableRustChannelVersion,
+ pub target: String,
+ pub passed: bool,
+}
+
+/// Aggregated build info for a crate version grouped by Rust channel for front-end display
+/// convenience.
+#[derive(Debug, Serialize, Deserialize, Default)]
+pub struct EncodableVersionBuildInfo {
+ pub id: i32,
+ pub ordering: HashMap>,
+ pub stable: HashMap>,
+ pub beta: HashMap>,
+ pub nightly: HashMap>,
+}
+
+/// `MaxBuildInfo` in JSON form.
+#[derive(Debug, Default, Serialize, Deserialize)]
+pub struct EncodableMaxVersionBuildInfo {
+ pub stable: Option,
+ pub beta: Option,
+ pub nightly: Option,
+}
+
+/// Describes a Rust version by its channel and the released version on that channel.
+/// For use in describing what versions of Rust a particular crate version builds with.
+/// Contains the original version string for inserting into the database.
+#[derive(Debug)]
+pub struct EncodableRustChannelVersion {
+ raw: String,
+ pub parsed: ParsedRustChannelVersion,
+}
+
+/// A pretty, minimal representation of a Rust version's channel and released version on that
+/// channel. Namely, does not include the exact release hash.
+#[derive(Debug)]
+pub enum ParsedRustChannelVersion {
+ Stable(semver::Version),
+ Beta(NaiveDate),
+ Nightly(NaiveDate),
+}
+
+impl fmt::Display for EncodableRustChannelVersion {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "{}", self.raw)
+ }
+}
+
+impl Serialize for EncodableRustChannelVersion {
+ fn serialize
(&self, serializer: S) -> Result
+ where
+ S: Serializer,
+ {
+ serializer.serialize_str(&self.raw)
+ }
+}
+
+impl<'de> Deserialize<'de> for EncodableRustChannelVersion {
+ fn deserialize>(d: D) -> Result {
+ let s = String::deserialize(d)?;
+
+ Ok(EncodableRustChannelVersion {
+ raw: s.clone(),
+ parsed: ParsedRustChannelVersion::from_str(&s).map_err(serde::de::Error::custom)?,
+ })
+ }
+}
+
+impl FromStr for ParsedRustChannelVersion {
+ 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_args!(
+ "rust_version `{}` not recognized; \
+ expected format like `rustc X.Y.Z (SHA YYYY-MM-DD)`",
+ s
+ )));
+ }
+
+ if pieces[1].contains("nightly") {
+ Ok(ParsedRustChannelVersion::Nightly(
+ NaiveDate::parse_from_str(pieces[3], "%Y-%m-%d")?,
+ ))
+ } else if pieces[1].contains("beta") {
+ Ok(ParsedRustChannelVersion::Beta(NaiveDate::parse_from_str(
+ pieces[3],
+ "%Y-%m-%d",
+ )?))
+ } else {
+ let v = semver::Version::parse(pieces[1])?;
+ if v.pre.is_empty() {
+ Ok(ParsedRustChannelVersion::Stable(v))
+ } else {
+ Err(human(&format_args!(
+ "rust_version `{}` not recognized as nightly, beta, or stable",
+ s
+ )))
+ }
+ }
+ }
+}
+
+impl ParsedRustChannelVersion {
+ pub fn as_stable(&self) -> Option<&semver::Version> {
+ match *self {
+ ParsedRustChannelVersion::Stable(ref v) => Some(v),
+ _ => None,
+ }
+ }
+
+ pub fn as_beta(&self) -> Option<&NaiveDate> {
+ match *self {
+ ParsedRustChannelVersion::Beta(ref v) => Some(v),
+ _ => None,
+ }
+ }
+
+ pub fn as_nightly(&self) -> Option<&NaiveDate> {
+ match *self {
+ ParsedRustChannelVersion::Nightly(ref v) => Some(v),
+ _ => None,
+ }
+ }
+}
+
#[cfg(test)]
mod tests {
use std::collections::HashMap;
@@ -197,6 +346,7 @@ mod tests {
dependencies: "".to_string(),
version_downloads: "".to_string(),
authors: "".to_string(),
+ build_info: "".to_string(),
},
};
let json = serde_json::to_string(&ver).unwrap();
@@ -226,6 +376,9 @@ mod tests {
downloads: 0,
recent_downloads: None,
max_version: "".to_string(),
+ max_build_info_stable: None,
+ max_build_info_beta: None,
+ max_build_info_nightly: None,
description: None,
homepage: None,
documentation: None,