// MIT License
//
// Copyright (c) 2017-2020 Lukas Lueg
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
#![allow(clippy::needless_doctest_main)]
//! Provides a crate with information from the time it was built.
//!
//! `built` is used as a build-time dependency to collect various information
//! about the build environment, serialize it into Rust-code and compile
//! it into the final crate. The information collected by `built` include:
//!
//! * Various metadata like version, authors, homepage etc. as set by `Cargo.toml`
//! * The tag or commit id if the crate was being compiled from within a git repo.
//! * The values of `CARGO_CFG_*` build script env variables, like `CARGO_CFG_TARGET_OS` and
//! `CARGO_CFG_TARGET_ARCH`.
//! * The features the crate was compiled with.
//! * The various dependencies, dependencies of dependencies and their versions
//! cargo ultimately chose to compile.
//! * The presence of a CI-platform like `Travis CI` and `AppVeyor`.
//! * The used compiler and it's version; the used documentation generator and
//! it's version.
//!
//! See the [`Options`][options]-type regarding what `built` can serialize.
//!
//! `built` does not add any further dependencies to a crate; all information
//! is serialized as types from `stdlib`. One can include `built` as a
//! runtime-dependency and use it's convenience functions.
//!
//! To add `built` to a crate, add it as a build-time dependency, use a build
//! script that collects and serializes the build-time information and `include!`
//! that as code.
//!
//! Add this to `Cargo.toml`:
//!
//! ```toml
//! [package]
//! build = "build.rs"
//!
//! [build-dependencies]
//! built = "0.5"
//! ```
//!
//! Add or modify a build script. In `build.rs`:
//!
//! ```rust,no_run
//! fn main() {
//! built::write_built_file().expect("Failed to acquire build-time information");
//! }
//! ```
//!
//! The build-script will by default write a file named `built.rs` into Cargo's output
//! directory. It can be picked up in `main.rs` (or anywhere else) like this:
//!
//! ```rust,ignore
//! // Use of a mod or pub mod is not actually necessary.
//! pub mod built_info {
//! // The file has been placed there by the build script.
//! include!(concat!(env!("OUT_DIR"), "/built.rs"));
//! }
//! ```
//!
//! And then used somewhere in the crate's code:
//!
//! ```rust
//! # mod built_info {
//! # pub const PKG_VERSION_PRE: &str = "";
//! # pub const CI_PLATFORM: Option<&str> = None;
//! # pub const GIT_VERSION: Option<&str> = None;
//! # pub const DEPENDENCIES: [(&str, &str); 0] = [];
//! # pub const BUILT_TIME_UTC: &str = "Tue, 14 Feb 2017 05:21:41 GMT";
//! # }
//! #
//! # enum LogLevel { TRACE, ERROR };
//! /// Determine if current version is a pre-release or was built from a git-repo
//! fn release_is_unstable() -> bool {
//! return !built_info::PKG_VERSION_PRE.is_empty() || built_info::GIT_VERSION.is_some()
//! }
//!
//! /// Default log-level, enhanced on CI
//! fn default_log_level() -> LogLevel {
//! if built_info::CI_PLATFORM.is_some() {
//! LogLevel::TRACE
//! } else {
//! LogLevel::ERROR
//! }
//! }
//!
//! /// The time this crate was built
//! #[cfg(feature = "chrono")]
//! fn built_time() -> built::chrono::DateTime<built::chrono::Local> {
//! built::util::strptime(built_info::BUILT_TIME_UTC)
//! .with_timezone(&built::chrono::offset::Local)
//! }
//!
//! /// If another crate pulls in a dependency we don't like, print a warning
//! #[cfg(feature = "semver")]
//! fn check_sane_dependencies() {
//! if built::util::parse_versions(&built_info::DEPENDENCIES)
//! .any(|(name, ver)| name == "DeleteAllMyFiles"
//! && ver < built::semver::Version::parse("1.1.4").unwrap()) {
//! eprintln!("DeleteAllMyFiles < 1.1.4 may not delete all your files. Beware!");
//! }
//! }
//! ```
//!
//! A full `built.rs` will look something like:
//! ```
//! /// The Continuous Integration platform detected during compilation.
//! pub const CI_PLATFORM: Option<&str> = None;
//! #[doc="The full version."]
//! pub const PKG_VERSION: &str = "0.1.0";
//! #[doc="The major version."]
//! pub const PKG_VERSION_MAJOR: &str = "0";
//! #[doc="The minor version."]
//! pub const PKG_VERSION_MINOR: &str = "1";
//! #[doc="The patch version."]
//! pub const PKG_VERSION_PATCH: &str = "0";
//! #[doc="The pre-release version."]
//! pub const PKG_VERSION_PRE: &str = "";
//! #[doc="A colon-separated list of authors."]
//! pub const PKG_AUTHORS: &str = "Lukas Lueg <lukas.lueg@gmail.com>";
//! #[doc="The name of the package."]
//! pub const PKG_NAME: &str = "example_project";
//! #[doc="The description."]
//! pub const PKG_DESCRIPTION: &str = "";
//! #[doc="The homepage."]
//! pub const PKG_HOMEPAGE: &str = "";
//! #[doc="The license."]
//! pub const PKG_LICENSE: &str = "MIT";
//! #[doc="The source repository as advertised in Cargo.toml."]
//! pub const PKG_REPOSITORY: &str = "";
//! #[doc="The target triple that was being compiled for."]
//! pub const TARGET: &str = "x86_64-unknown-linux-gnu";
//! #[doc="The host triple of the rust compiler."]
//! pub const HOST: &str = "x86_64-unknown-linux-gnu";
//! #[doc="`release` for release builds, `debug` for other builds."]
//! pub const PROFILE: &str = "debug";
//! #[doc="The compiler that cargo resolved to use."]
//! pub const RUSTC: &str = "rustc";
//! #[doc="The documentation generator that cargo resolved to use."]
//! pub const RUSTDOC: &str = "rustdoc";
//! #[doc="Value of OPT_LEVEL for the profile used during compilation."]
//! pub const OPT_LEVEL: &str = "0";
//! #[doc="The parallelism that was specified during compilation."]
//! pub const NUM_JOBS: u32 = 8;
//! #[doc="Value of DEBUG for the profile used during compilation."]
//! pub const DEBUG: bool = true;
//! /// The features that were enabled during compilation.
//! pub const FEATURES: [&str; 0] = [];
//! /// The features as a comma-separated string.
//! pub const FEATURES_STR: &str = "";
//! /// The output of `rustc -V`
//! pub const RUSTC_VERSION: &str = "rustc 1.43.1 (8d69840ab 2020-05-04)";
//! /// The output of `rustdoc -V`
//! pub const RUSTDOC_VERSION: &str = "rustdoc 1.43.1 (8d69840ab 2020-05-04)";
//! /// If the crate was compiled from within a git-repository, `GIT_VERSION` contains HEAD's tag. The short commit id is used if HEAD is not tagged.
//! pub const GIT_VERSION: Option<&str> = Some("0.4.1-10-gca2af4f");
//! /// If the repository had dirty/staged files.
//! pub const GIT_DIRTY: Option<bool> = Some(true);
//! /// If the crate was compiled from within a git-repository, `GIT_HEAD_REF` contains full name to the reference pointed to by HEAD (e.g.: `refs/heads/master`). If HEAD is detached or the branch name is not valid UTF-8 `None` will be stored.
//! pub const GIT_HEAD_REF: Option<&str> = Some("refs/heads/master");
//! /// If the crate was compiled from within a git-repository, `GIT_COMMIT_HASH` contains HEAD's full commit SHA-1 hash.
//! pub const GIT_COMMIT_HASH: Option<&str> = Some("ca2af4f11bb8f4f6421c4cccf428bf4862573daf");
//! /// An array of effective dependencies as documented by `Cargo.lock`.
//! pub const DEPENDENCIES: [(&str, &str); 39] = [("autocfg", "1.0.0"), ("bitflags", "1.2.1"), ("built", "0.4.1"), ("cargo-lock", "4.0.1"), ("cc", "1.0.54"), ("cfg-if", "0.1.10"), ("chrono", "0.4.11"), ("example_project", "0.1.0"), ("git2", "0.13.6"), ("idna", "0.2.0"), ("jobserver", "0.1.21"), ("libc", "0.2.71"), ("libgit2-sys", "0.12.6+1.0.0"), ("libz-sys", "1.0.25"), ("log", "0.4.8"), ("matches", "0.1.8"), ("num-integer", "0.1.42"), ("num-traits", "0.2.11"), ("percent-encoding", "2.1.0"), ("pkg-config", "0.3.17"), ("proc-macro2", "1.0.17"), ("quote", "1.0.6"), ("semver", "0.10.0"), ("semver", "0.9.0"), ("semver-parser", "0.7.0"), ("serde", "1.0.110"), ("serde_derive", "1.0.110"), ("smallvec", "1.4.0"), ("syn", "1.0.25"), ("time", "0.1.43"), ("toml", "0.5.6"), ("unicode-bidi", "0.3.4"), ("unicode-normalization", "0.1.12"), ("unicode-xid", "0.2.0"), ("url", "2.1.1"), ("vcpkg", "0.2.8"), ("winapi", "0.3.8"), ("winapi-i686-pc-windows-gnu", "0.4.0"), ("winapi-x86_64-pc-windows-gnu", "0.4.0")];
//! /// The effective dependencies as a comma-separated string.
//! pub const DEPENDENCIES_STR: &str = "autocfg 1.0.0, bitflags 1.2.1, built 0.4.1, cargo-lock 4.0.1, cc 1.0.54, cfg-if 0.1.10, chrono 0.4.11, example_project 0.1.0, git2 0.13.6, idna 0.2.0, jobserver 0.1.21, libc 0.2.71, libgit2-sys 0.12.6+1.0.0, libz-sys 1.0.25, log 0.4.8, matches 0.1.8, num-integer 0.1.42, num-traits 0.2.11, percent-encoding 2.1.0, pkg-config 0.3.17, proc-macro2 1.0.17, quote 1.0.6, semver 0.10.0, semver 0.9.0, semver-parser 0.7.0, serde 1.0.110, serde_derive 1.0.110, smallvec 1.4.0, syn 1.0.25, time 0.1.43, toml 0.5.6, unicode-bidi 0.3.4, unicode-normalization 0.1.12, unicode-xid 0.2.0, url 2.1.1, vcpkg 0.2.8, winapi 0.3.8, winapi-i686-pc-windows-gnu 0.4.0, winapi-x86_64-pc-windows-gnu 0.4.0";
//! /// The built-time in RFC2822, UTC
//! pub const BUILT_TIME_UTC: &str = "Wed, 27 May 2020 18:12:39 +0000";
//! /// The target architecture, given by `CARGO_CFG_TARGET_ARCH`.
//! pub const CFG_TARGET_ARCH: &str = "x86_64";
//! /// The endianness, given by `CARGO_CFG_TARGET_ENDIAN`.
//! pub const CFG_ENDIAN: &str = "little";
//! /// The toolchain-environment, given by `CARGO_CFG_TARGET_ENV`.
//! pub const CFG_ENV: &str = "gnu";
//! /// The OS-family, given by `CARGO_CFG_TARGET_FAMILY`.
//! pub const CFG_FAMILY: &str = "unix";
//! /// The operating system, given by `CARGO_CFG_TARGET_OS`.
//! pub const CFG_OS: &str = "linux";
//! /// The pointer width, given by `CARGO_CFG_TARGET_POINTER_WIDTH`.
//! pub const CFG_POINTER_WIDTH: &str = "64";
//! ```
//! [options]: struct.Options.html
#![cfg_attr(feature = "nightly", feature(external_doc))]
pub mod util;
use std::{
collections, env, ffi, fmt, fs, io,
io::{Read, Write},
path, process,
};
#[cfg(feature = "semver")]
pub use semver;
#[cfg(feature = "chrono")]
pub use chrono;
#[cfg(feature = "nightly")]
#[doc(include = "../README.md")]
#[allow(dead_code)]
type _READMETEST = ();
macro_rules! write_variable {
($writer:expr, $name:expr, $datatype:expr, $value:expr, $doc:expr) => {
writeln!(
$writer,
"#[doc=r#\"{}\"#]\n#[allow(dead_code)]\npub const {}: {} = {};",
$doc, $name, $datatype, $value
)?;
};
}
macro_rules! write_str_variable {
($writer:expr, $name:expr, $value:expr, $doc:expr) => {
write_variable!($writer, $name, "&str", format!("r\"{}\"", $value), $doc);
};
}
/// Various Continuous Integration platforms whose presence can be detected.
pub enum CIPlatform {
/// https://travis-ci.org
Travis,
/// https://circleci.com
Circle,
/// https://about.gitlab.com/gitlab-ci/
GitLab,
/// https://www.appveyor.com/
AppVeyor,
/// https://codeship.com/
Codeship,
/// https://github.com/drone/drone
Drone,
/// https://magnum-ci.com/
Magnum,
/// https://semaphoreci.com/
Semaphore,
/// https://jenkins.io/
Jenkins,
/// https://www.atlassian.com/software/bamboo
Bamboo,
/// https://www.visualstudio.com/de/tfs/
TFS,
/// https://www.jetbrains.com/teamcity/
TeamCity,
/// https://buildkite.com/
Buildkite,
/// http://hudson-ci.org/
Hudson,
/// https://github.com/taskcluster
TaskCluster,
/// https://www.gocd.io/
GoCD,
/// https://bitbucket.org
BitBucket,
/// Unspecific
Generic,
}
impl fmt::Display for CIPlatform {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match *self {
CIPlatform::Travis => "Travis CI",
CIPlatform::Circle => "CircleCI",
CIPlatform::GitLab => "GitLab",
CIPlatform::AppVeyor => "AppVeyor",
CIPlatform::Codeship => "CodeShip",
CIPlatform::Drone => "Drone",
CIPlatform::Magnum => "Magnum",
CIPlatform::Semaphore => "Semaphore",
CIPlatform::Jenkins => "Jenkins",
CIPlatform::Bamboo => "Bamboo",
CIPlatform::TFS => "Team Foundation Server",
CIPlatform::TeamCity => "TeamCity",
CIPlatform::Buildkite => "Buildkite",
CIPlatform::Hudson => "Hudson",
CIPlatform::TaskCluster => "TaskCluster",
CIPlatform::GoCD => "GoCD",
CIPlatform::BitBucket => "BitBucket",
CIPlatform::Generic => "Generic CI",
})
}
}
type EnvironmentMap = collections::HashMap<String, String>;
fn get_environment() -> EnvironmentMap {
let mut envmap = EnvironmentMap::new();
for (k, v) in env::vars_os() {
let k = k.into_string();
let v = v.into_string();
if let (Ok(k), Ok(v)) = (k, v) {
envmap.insert(k, v);
}
}
envmap
}
impl CIPlatform {
fn detect_from_envmap(envmap: &EnvironmentMap) -> Option<CIPlatform> {
macro_rules! detect {
($(($k:expr, $v:expr, $i:ident)),*) => {$(
if envmap.get($k).map_or(false, |v| v == $v) {
return Some(CIPlatform::$i);
}
)*};
($(($k:expr, $i:ident)),*) => {$(
if envmap.contains_key($k) {
return Some(CIPlatform::$i);
}
)*};
($($k:expr),*) => {$(
if envmap.contains_key($k) {
return Some(CIPlatform::Generic);
}
)*};
}
// Variable names collected by watson/ci-info
detect!(
("TRAVIS", Travis),
("CIRCLECI", Circle),
("GITLAB_CI", GitLab),
("APPVEYOR", AppVeyor),
("DRONE", Drone),
("MAGNUM", Magnum),
("SEMAPHORE", Semaphore),
("JENKINS_URL", Jenkins),
("bamboo_planKey", Bamboo),
("TF_BUILD", TFS),
("TEAMCITY_VERSION", TeamCity),
("BUILDKITE", Buildkite),
("HUDSON_URL", Hudson),
("GO_PIPELINE_LABEL", GoCD),
("BITBUCKET_COMMIT", BitBucket)
);
if envmap.contains_key("TASK_ID") && envmap.contains_key("RUN_ID") {
return Some(CIPlatform::TaskCluster);
}
detect!(("CI_NAME", "codeship", Codeship));
detect!(
"CI", // Could be Travis, Circle, GitLab, AppVeyor or CodeShip
"CONTINUOUS_INTEGRATION", // Probably Travis
"BUILD_NUMBER" // Jenkins, TeamCity
);
None
}
}
fn get_build_deps(manifest_location: &path::Path) -> io::Result<Vec<(String, String)>> {
let mut lock_buf = String::new();
fs::File::open(manifest_location.join("Cargo.lock"))?.read_to_string(&mut lock_buf)?;
Ok(parse_dependencies(&lock_buf))
}
fn parse_dependencies(lock_toml_buf: &str) -> Vec<(String, String)> {
let lockfile: cargo_lock::Lockfile = lock_toml_buf.parse().expect("Failed to parse lockfile");
let mut deps = Vec::new();
for package in lockfile.packages {
deps.push((package.name.to_string(), package.version.to_string()));
}
deps.sort_unstable();
deps
}
fn get_version_from_cmd(executable: &ffi::OsStr) -> io::Result<String> {
let output = process::Command::new(executable).arg("-V").output()?;
let mut v = String::from_utf8(output.stdout).unwrap();
v.pop(); // remove newline
Ok(v)
}
fn write_compiler_version(
rustc: &ffi::OsStr,
rustdoc: &ffi::OsStr,
w: &mut fs::File,
) -> io::Result<()> {
let rustc_version = get_version_from_cmd(&rustc)?;
let rustdoc_version = get_version_from_cmd(&rustdoc)?;
let doc = format!("The output of `{} -V`", rustc.to_string_lossy());
write_str_variable!(w, "RUSTC_VERSION", rustc_version, doc);
let doc = format!("The output of `{} -V`", rustdoc.to_string_lossy());
write_str_variable!(w, "RUSTDOC_VERSION", rustdoc_version, doc);
Ok(())
}
fn fmt_option_str<S: fmt::Display>(o: Option<S>) -> String {
match o {
Some(s) => format!("Some(\"{}\")", s),
None => "None".to_owned(),
}
}
#[cfg(feature = "git2")]
fn write_git_version(manifest_location: &path::Path, w: &mut fs::File) -> io::Result<()> {
// CIs will do shallow clones of repositories, causing libgit2 to error
// out. We try to detect if we are running on a CI and ignore the
// error.
let (tag, dirty) = match util::get_repo_description(&manifest_location) {
Ok(Some((tag, dirty))) => (Some(tag), Some(dirty)),
_ => (None, None),
};
write_variable!(
w,
"GIT_VERSION",
"Option<&str>",
fmt_option_str(tag),
"If the crate was compiled from within a git-repository, \
`GIT_VERSION` contains HEAD's tag. The short commit id is used if HEAD is not tagged."
);
write_variable!(
w,
"GIT_DIRTY",
"Option<bool>",
match dirty {
Some(true) => "Some(true)",
Some(false) => "Some(false)",
None => "None",
},
"If the repository had dirty/staged files."
);
let (branch, commit) = match util::get_repo_head(&manifest_location) {
Ok(Some((b, c))) => (b, Some(c)),
_ => (None, None),
};
let doc = "If the crate was compiled from within a git-repository, `GIT_HEAD_REF` \
contains full name to the reference pointed to by HEAD \
(e.g.: `refs/heads/master`). If HEAD is detached or the branch name is not \
valid UTF-8 `None` will be stored.\n";
write_variable!(
w,
"GIT_HEAD_REF",
"Option<&str>",
fmt_option_str(branch),
doc
);
write_variable!(
w,
"GIT_COMMIT_HASH",
"Option<&str>",
fmt_option_str(commit),
"If the crate was compiled from within a git-repository, `GIT_COMMIT_HASH` \
contains HEAD's full commit SHA-1 hash."
);
Ok(())
}
fn write_ci(envmap: &EnvironmentMap, w: &mut fs::File) -> io::Result<()> {
write_variable!(
w,
"CI_PLATFORM",
"Option<&str>",
fmt_option_str(CIPlatform::detect_from_envmap(envmap)),
"The Continuous Integration platform detected during compilation."
);
Ok(())
}
fn write_features(envmap: &EnvironmentMap, w: &mut fs::File) -> io::Result<()> {
let prefix = "CARGO_FEATURE_";
let mut features = Vec::new();
for name in envmap.keys() {
if name.starts_with(&prefix) {
features.push(name[prefix.len()..].to_owned());
}
}
features.sort();
write_variable!(
w,
"FEATURES",
format!("[&str; {}]", features.len()),
format!("{:?}", features),
"The features that were enabled during compilation."
);
let features_str = features.join(", ");
write_str_variable!(
w,
"FEATURES_STR",
features_str,
"The features as a comma-separated string."
);
Ok(())
}
fn write_env(envmap: &EnvironmentMap, w: &mut fs::File) -> io::Result<()> {
macro_rules! write_env_str {
($(($name:ident, $env_name:expr,$doc:expr)),*) => {$(
write_str_variable!(
w,
stringify!($name),
envmap.get($env_name)
.expect(stringify!(Missing expected environment variable $env_name)),
$doc
);
)*}
}
write_env_str!(
(PKG_VERSION, "CARGO_PKG_VERSION", "The full version."),
(
PKG_VERSION_MAJOR,
"CARGO_PKG_VERSION_MAJOR",
"The major version."
),
(
PKG_VERSION_MINOR,
"CARGO_PKG_VERSION_MINOR",
"The minor version."
),
(
PKG_VERSION_PATCH,
"CARGO_PKG_VERSION_PATCH",
"The patch version."
),
(
PKG_VERSION_PRE,
"CARGO_PKG_VERSION_PRE",
"The pre-release version."
),
(
PKG_AUTHORS,
"CARGO_PKG_AUTHORS",
"A colon-separated list of authors."
),
(PKG_NAME, "CARGO_PKG_NAME", "The name of the package."),
(PKG_DESCRIPTION, "CARGO_PKG_DESCRIPTION", "The description."),
(PKG_HOMEPAGE, "CARGO_PKG_HOMEPAGE", "The homepage."),
(PKG_LICENSE, "CARGO_PKG_LICENSE", "The license."),
(
PKG_REPOSITORY,
"CARGO_PKG_REPOSITORY",
"The source repository as advertised in Cargo.toml."
),
(
TARGET,
"TARGET",
"The target triple that was being compiled for."
),
(HOST, "HOST", "The host triple of the rust compiler."),
(
PROFILE,
"PROFILE",
"`release` for release builds, `debug` for other builds."
),
(RUSTC, "RUSTC", "The compiler that cargo resolved to use."),
(
RUSTDOC,
"RUSTDOC",
"The documentation generator that cargo resolved to use."
)
);
write_str_variable!(
w,
"OPT_LEVEL",
env::var("OPT_LEVEL").unwrap(),
"Value of OPT_LEVEL for the profile used during compilation."
);
write_variable!(
w,
"NUM_JOBS",
"u32",
env::var("NUM_JOBS").unwrap(),
"The parallelism that was specified during compilation."
);
write_variable!(
w,
"DEBUG",
"bool",
env::var("DEBUG").unwrap() == "true",
"Value of DEBUG for the profile used during compilation."
);
Ok(())
}
fn write_dependencies(manifest_location: &path::Path, w: &mut fs::File) -> io::Result<()> {
let deps = get_build_deps(&manifest_location)?;
write_variable!(
w,
"DEPENDENCIES",
format!("[(&str, &str); {}]", deps.len()),
format!("{:?}", deps),
"An array of effective dependencies as documented by `Cargo.lock`."
);
write_str_variable!(
w,
"DEPENDENCIES_STR",
deps.iter()
.map(|&(ref n, ref v)| format!("{} {}", n, v))
.collect::<Vec<_>>()
.join(", "),
"The effective dependencies as a comma-separated string."
);
Ok(())
}
#[cfg(feature = "chrono")]
fn write_time(w: &mut fs::File) -> io::Result<()> {
let now = chrono::offset::Utc::now();
write_str_variable!(
w,
"BUILT_TIME_UTC",
now.to_rfc2822(),
"The build time in RFC2822, UTC."
);
Ok(())
}
fn write_cfg(w: &mut fs::File) -> io::Result<()> {
fn get_env(name: &str) -> String {
env::var(name).unwrap_or_default()
}
let target_arch = get_env("CARGO_CFG_TARGET_ARCH");
let target_endian = get_env("CARGO_CFG_TARGET_ENDIAN");
let target_env = get_env("CARGO_CFG_TARGET_ENV");
let target_family = get_env("CARGO_CFG_TARGET_FAMILY");
let target_os = get_env("CARGO_CFG_TARGET_OS");
let target_pointer_width = get_env("CARGO_CFG_TARGET_POINTER_WIDTH");
write_str_variable!(
w,
"CFG_TARGET_ARCH",
target_arch,
"The target architecture, given by `CARGO_CFG_TARGET_ARCH`."
);
write_str_variable!(
w,
"CFG_ENDIAN",
target_endian,
"The endianness, given by `CARGO_CFG_TARGET_ENDIAN`."
);
write_str_variable!(
w,
"CFG_ENV",
target_env,
"The toolchain-environment, given by `CARGO_CFG_TARGET_ENV`."
);
write_str_variable!(
w,
"CFG_FAMILY",
target_family,
"The OS-family, given by `CARGO_CFG_TARGET_FAMILY`."
);
write_str_variable!(
w,
"CFG_OS",
target_os,
"The operating system, given by `CARGO_CFG_TARGET_OS`."
);
write_str_variable!(
w,
"CFG_POINTER_WIDTH",
target_pointer_width,
"The pointer width, given by `CARGO_CFG_TARGET_POINTER_WIDTH`."
);
Ok(())
}
/// Selects which information `built` should retrieve and write as Rust code.
/// Used in conjunction with [`write_built_file_with_opts`][wrt].
///
/// [wrt]: fn.write_built_file_with_opts.html
#[allow(unused)]
pub struct Options {
compiler: bool,
git: bool,
ci: bool,
env: bool,
deps: bool,
features: bool,
time: bool,
cfg: bool,
}
impl Default for Options {
/// A new struct with almost all options enabled.
///
/// Parsing and writing information about dependencies is disabled by default
/// because this information is not available for crates compiled as dependencies;
/// only the top-level crate gets to do this. See `set_dependencies()` for
/// further information.
///
fn default() -> Options {
Options {
compiler: true,
git: true,
ci: true,
env: true,
deps: false,
features: true,
time: true,
cfg: true,
}
}
}
impl Options {
/// Detecting and writing the version of `RUSTC` and `RUSTDOC`.
///
/// Call the values of `RUSTC` and `RUSTDOC` as provided by Cargo to get a version string. The
/// result will something like
///
/// ```rust,no_run
/// pub const RUSTC_VERSION: &str = "rustc 1.15.0";
/// pub const RUSTDOC_VERSION: &str = "rustdoc 1.15.0";
/// ```
pub fn set_compiler(&mut self, enabled: bool) -> &mut Self {
self.compiler = enabled;
self
}
/// Detecting and writing the tag or commit-id and a flag to indicate
/// a dirty working directory of the crate's git repository (if any).
///
/// This option is only available if `built` was compiled with the
/// `git2` feature.
///
/// Try to open the git-repository at `manifest_location` and retrieve `HEAD`
/// tag or commit id. The result will be something like
///
/// ```rust,no_run
/// pub const GIT_VERSION: Option<&str> = Some("0.1");
/// pub const GIT_DIRTY: Option<bool> = Some(false);
/// pub const GIT_COMMIT_HASH: Option<&str> = Some("18b2eabfb47998c296f9d5183f617f1b1cc2d321");
/// pub const GIT_HEAD_REF: Option<&str> = Some("refs/heads/master");
/// ```
///
/// Notice that `GIT_HEAD_REF` is `None` if `HEAD` is detached or not valid UTF-8.
///
/// Continuous Integration platforms like `Travis` and `AppVeyor` will
/// do shallow clones, causing `libgit2` to be unable to get a meaningful
/// result. `GIT_VERSION` and `GIT_DIRTY` will therefor always be `None` if
/// a CI-platform is detected.
///
#[cfg(feature = "git2")]
pub fn set_git(&mut self, enabled: bool) -> &mut Self {
self.git = enabled;
self
}
/// Detecting and writing the Continuous Integration Platforms we are running on.
///
/// Detect various CI-platforms (named or not) and write something like
///
/// ```rust
/// pub const CI_PLATFORM: Option<&str> = Some("AppVeyor");
/// ```
pub fn set_ci(&mut self, enabled: bool) -> &mut Self {
self.ci = enabled;
self
}
/// Detecting various information provided through the environment, especially Cargo.
///
/// `built` writes something like
///
/// ```rust,no_run
/// #[doc="The full version."]
/// pub const PKG_VERSION: &str = "1.2.3-rc1";
/// #[doc="The major version."]
/// pub const PKG_VERSION_MAJOR: &str = "1";
/// #[doc="The minor version."]
/// pub const PKG_VERSION_MINOR: &str = "2";
/// #[doc="The patch version."]
/// pub const PKG_VERSION_PATCH: &str = "3";
/// #[doc="The pre-release version."]
/// pub const PKG_VERSION_PRE: &str = "rc1";
/// #[doc="A colon-separated list of authors."]
/// pub const PKG_AUTHORS: &str = "Joe:Bob:Harry:Potter";
/// #[doc="The name of the package."]
/// pub const PKG_NAME: &str = "testbox";
/// #[doc="The description."]
/// pub const PKG_DESCRIPTION: &str = "xobtset";
/// #[doc="The home page."]
/// pub const PKG_HOMEPAGE: &str = "localhost";
/// #[doc="The target triple that was being compiled for."]
/// pub const TARGET: &str = "x86_64-apple-darwin";
/// #[doc="The host triple of the rust compiler."]
/// pub const HOST: &str = "x86_64-apple-darwin";
/// #[doc="`release` for release builds, `debug` for other builds."]
/// pub const PROFILE: &str = "debug";
/// #[doc="The compiler that cargo resolved to use."]
/// pub const RUSTC: &str = "rustc";
/// #[doc="The documentation generator that cargo resolved to use."]
/// pub const RUSTDOC: &str = "rustdoc";
/// #[doc="Value of OPT_LEVEL for the profile used during compilation."]
/// pub const OPT_LEVEL: &str = "0";
/// #[doc="The parallelism that was specified during compilation."]
/// pub const NUM_JOBS: u32 = 8;
/// #[doc="Value of DEBUG for the profile used during compilation."]
/// pub const DEBUG: bool = true;
/// ```
///
pub fn set_env(&mut self, enabled: bool) -> &mut Self {
self.env = enabled;
self
}
/// Parsing `Cargo.lock`and writing lists of dependencies and their versions.
///
/// For this to work, `Cargo.lock` needs to actually be there; this is (usually)
/// only true for executables and not for libraries. Cargo will only create a
/// `Cargo.lock` for the top-level crate in a dependency-tree. In case
/// of a library, the top-level crate will decide which crate/version
/// combination to compile and there will be no `Cargo.lock` while the library
/// gets compiled as a dependency.
///
/// Parsing `Cargo.lock` instead of `Cargo.toml` allows us to serialize the
/// precise versions Cargo chose to compile. One can't, however, distinguish
/// `build-dependencies`, `dev-dependencies` and `dependencies`. Furthermore,
/// some dependencies never show up if Cargo had not been forced to
/// actually use them (e.g. `dev-dependencies` with `cargo test` never
/// having been executed).
///
/// ```rust,no_run
/// /// An array of effective dependencies as documented by `Cargo.lock`
/// pub const DEPENDENCIES: [(&str, &str); 2] = [("built", "0.1.0"), ("time", "0.1.36")];
/// /// The effective dependencies as a comma-separated string.
/// pub const DEPENDENCIES_STR: &str = "built 0.1.0, time 0.1.36";
/// ```
pub fn set_dependencies(&mut self, enabled: bool) -> &mut Self {
self.deps = enabled;
self
}
/// Writing features enabled during build.
///
/// One should not rely on this besides convenient debug output. If the runtime
/// depends on enabled features, use `#[cfg(feature = "foo")]` instead.
///
/// ```rust,no_run
/// /// The features that were enabled during compilation.
/// pub const FEATURES: [&str; 2] = ["DEFAULT", "WAYLAND"];
/// /// The features as a comma-separated string.
/// pub const FEATURES_STR: &str = "DEFAULT, WAYLAND";
/// ```
pub fn set_features(&mut self, enabled: bool) -> &mut Self {
self.features = enabled;
self
}
/// Writing the current timestamp.
///
/// This option is only available if `built` is compiled with the
/// `chrono` feature.
///
/// If `built` is included as a runtime-dependency, it can parse the
/// string-representation into a `time:Tm` with the help
/// of `built::util::strptime()`.
///
/// ```rust,no_run
/// /// The built-time in RFC822, UTC
/// pub const BUILT_TIME_UTC: &str = "Tue, 14 Feb 2017 01:12:35 GMT";
/// ```
#[cfg(feature = "chrono")]
pub fn set_time(&mut self, enabled: bool) -> &mut Self {
self.time = enabled;
self
}
/// Writing the configuration attributes.
///
/// `built` writes something like
///
/// ```rust,no_run
/// /// The target architecture, given by `CARGO_CFG_TARGET_ARCH`.
/// pub const CFG_TARGET_ARCH: &str = "x86_64";
/// /// The endianness, given by `CARGO_CFG_TARGET_ENDIAN`.
/// pub const CFG_ENDIAN: &str = "little";
/// /// The toolchain-environment, given by `CARGO_CFG_TARGET_ENV`.
/// pub const CFG_ENV: &str = "gnu";
/// /// The OS-family, given by `CARGO_CFG_TARGET_FAMILY`.
/// pub const CFG_FAMILY: &str = "unix";
/// /// The operating system, given by `CARGO_CFG_TARGET_OS`.
/// pub const CFG_OS: &str = "linux";
/// /// The pointer width, given by `CARGO_CFG_TARGET_POINTER_WIDTH`.
/// pub const CFG_POINTER_WIDTH: &str = "64";
/// ```
pub fn set_cfg(&mut self, enabled: bool) -> &mut Self {
self.cfg = enabled;
self
}
}
/// Writes rust-code describing the crate at `manifest_location` to a new file named `dst`.
///
/// # Errors
/// The function returns an error if the file at `dst` already exists or can't
/// be written to. This should not be a concern if the filename points to
/// `OUR_DIR`.
pub fn write_built_file_with_opts(
options: &Options,
manifest_location: &path::Path,
dst: &path::Path,
) -> io::Result<()> {
let mut built_file = fs::File::create(&dst)?;
built_file.write_all(
r#"//
// EVERYTHING BELOW THIS POINT WAS AUTO-GENERATED DURING COMPILATION. DO NOT MODIFY.
//
"#
.as_ref(),
)?;
macro_rules! o {
($i:ident, $b:stmt) => {
if options.$i {
$b
}
};
}
if options.ci || options.env || options.features || options.compiler {
let envmap = get_environment();
o!(ci, write_ci(&envmap, &mut built_file)?);
o!(env, write_env(&envmap, &mut built_file)?);
o!(features, write_features(&envmap, &mut built_file)?);
o!(
compiler,
write_compiler_version(
&envmap["RUSTC"].as_ref(),
&envmap["RUSTDOC"].as_ref(),
&mut built_file
)?
);
#[cfg(feature = "git2")]
{
o!(git, write_git_version(&manifest_location, &mut built_file)?);
}
}
o!(
deps,
write_dependencies(&manifest_location, &mut built_file)?
);
#[cfg(feature = "chrono")]
{
o!(time, write_time(&mut built_file)?);
}
o!(cfg, write_cfg(&mut built_file)?);
built_file.write_all(
r#"//
// EVERYTHING ABOVE THIS POINT WAS AUTO-GENERATED DURING COMPILATION. DO NOT MODIFY.
//
"#
.as_ref(),
)?;
Ok(())
}
/// A shorthand for calling `write_built_file()` with `CARGO_MANIFEST_DIR` and
/// `[OUT_DIR]/built.rs`.
///
/// # Errors
/// Same as `write_built_file_with_opts()`.
///
/// # Panics
/// If `CARGO_MANIFEST_DIR` or `OUT_DIR` are not set.
pub fn write_built_file() -> io::Result<()> {
let src = env::var("CARGO_MANIFEST_DIR").unwrap();
let dst = path::Path::new(&env::var("OUT_DIR").unwrap()).join("built.rs");
write_built_file_with_opts(&Options::default(), src.as_ref(), &dst)?;
Ok(())
}
#[cfg(test)]
mod tests {
#[test]
#[cfg(feature = "git2")]
fn parse_git_repo() {
use super::util;
use std::fs;
use std::path;
let repo_root = tempfile::tempdir().unwrap();
assert_eq!(util::get_repo_description(repo_root.as_ref()), Ok(None));
let repo = git2::Repository::init_opts(
&repo_root,
git2::RepositoryInitOptions::new()
.external_template(false)
.mkdir(false)
.no_reinit(true)
.mkpath(false),
)
.unwrap();
let cruft_file = repo_root.path().join("cruftfile");
std::fs::write(&cruft_file, "Who? Me?").unwrap();
let project_root = repo_root.path().join("project_root");
fs::create_dir(&project_root).unwrap();
let sig = git2::Signature::now("foo", "bar").unwrap();
let mut idx = repo.index().unwrap();
idx.add_path(path::Path::new("cruftfile")).unwrap();
idx.write().unwrap();
let commit_oid = repo
.commit(
Some("HEAD"),
&sig,
&sig,
"Testing testing 1 2 3",
&repo.find_tree(idx.write_tree().unwrap()).unwrap(),
&[],
)
.unwrap();
let commit_hash = format!("{}", commit_oid);
// The the commit, the commit-id is something and the repo is not dirty
let (tag, dirty) = util::get_repo_description(&project_root).unwrap().unwrap();
assert!(!tag.is_empty());
assert!(!dirty);
// Tag the commit, it should be retrieved
repo.tag(
"foobar",
&repo
.find_object(commit_oid, Some(git2::ObjectType::Commit))
.unwrap(),
&sig,
"Tagged foobar",
false,
)
.unwrap();
let (tag, dirty) = util::get_repo_description(&project_root).unwrap().unwrap();
assert_eq!(tag, "foobar");
assert!(!dirty);
// Make some dirt
std::fs::write(cruft_file, "now dirty").unwrap();
let (tag, dirty) = util::get_repo_description(&project_root).unwrap().unwrap();
assert_eq!(tag, "foobar");
assert!(dirty);
let branch_short_name = "baz";
let branch_name = "refs/heads/baz";
let commit = repo.find_commit(commit_oid).unwrap();
repo.branch(branch_short_name, &commit, true).unwrap();
repo.set_head(branch_name).unwrap();
assert_eq!(
util::get_repo_head(&project_root),
Ok(Some((Some(branch_name.to_owned()), commit_hash)))
);
}
#[test]
#[cfg(feature = "git2")]
fn detached_head_repo() {
let repo_root = tempfile::tempdir().unwrap();
let repo = git2::Repository::init_opts(
&repo_root,
git2::RepositoryInitOptions::new()
.external_template(false)
.mkdir(false)
.no_reinit(true)
.mkpath(false),
)
.unwrap();
let sig = git2::Signature::now("foo", "bar").unwrap();
let commit_oid = repo
.commit(
Some("HEAD"),
&sig,
&sig,
"Testing",
&repo
.find_tree(repo.index().unwrap().write_tree().unwrap())
.unwrap(),
&[],
)
.unwrap();
let commit_hash = format!("{}", commit_oid);
repo.set_head_detached(commit_oid).unwrap();
assert_eq!(
super::util::get_repo_head(repo_root.as_ref()),
Ok(Some((None, commit_hash)))
);
}
#[test]
fn parse_deps() {
let lock_toml_buf = r#"
[root]
name = "foobar"
version = "1.0.0"
dependencies = [
"normal_dep 1.2.3",
"local_dep 4.5.6",
]
[[package]]
name = "normal_dep"
version = "1.2.3"
dependencies = [
"dep_of_dep 7.8.9",
]
[[package]]
name = "local_dep"
version = "4.5.6"
[[package]]
name = "dep_of_dep"
version = "7.8.9""#;
let deps = super::parse_dependencies(&lock_toml_buf);
assert_eq!(
deps,
[
("dep_of_dep".to_owned(), "7.8.9".to_owned()),
("local_dep".to_owned(), "4.5.6".to_owned()),
("normal_dep".to_owned(), "1.2.3".to_owned()),
]
);
}
}