如何在编译到Rust二进制文件期间编译在命令行上提供的字符串?

时间:2018-01-10 07:59:03

标签: rust rust-cargo

我想这样做,以便在程序启动时打印到stderr:

This is Program X v. 0.1.0 compiled on 20180110. Now listening on stdin,
quit with SIGINT (^C). EOF is ignored. For licensing  information,  read 
LICENSE. To suppress this message, supply --quiet or --suppress-greeting

在C / C ++中,我会用Makefile实现这一点,例如:

VERSION = 0.1.0
FLAGS = -Wall -pipe -O3 -funroll-loops -Wall -DVERSION="\"$(VERSION)\"" -DCOMPILED_AT="\"`date +%Y%m%d`\""

然后,在源代码中,我会喜欢使用这些常量,也许是在调用fprintf时。当然,检查它们是否确实存在#ifdef

如何在Rust中实现这一目标?我需要使用程序宏吗?我能以某种方式使用cargo吗?

我知道env!("CARGO_PKG_VERSION")可以用作VERSION的替代品,但是COMPILED_AT呢?

2 个答案:

答案 0 :(得分:5)

有两种方法可以做到这一点。

使用build.rs脚本

使用build.rs脚本的好处是,编译程序的其他用户不必以特殊方式调用cargo或设置其环境。这是一个如何做到这一点的最小例子。

build.rs

use std::process::{Command, exit};
use std::str;

static CARGOENV: &str = "cargo:rustc-env=";

fn main() {
    let time_c = Command::new("date").args(&["+%Y%m%d"]).output();

    match time_c {
        Ok(t) => {
            let time;
            unsafe {
                time = str::from_utf8_unchecked( &t.stdout );
            }   
            println!("{}COMPILED_AT={}", CARGOENV, time);
        }   
        Err(_) => exit(1)
    }   
}

src/main.rs

fn main() {
    println!("This is Example Program {} compiled at {}", env!("CARGO_PKG_VERSION"), env!("COMPILED_AT"));
}

Cargo.toml

[package]
name = "compiled_at"
version = "0.1.0"
authors = ["Fredrick Brennan <copypaste@kittens.ph>"]
build = "build.rs"

[dependencies]

显然,可以调整它以使其在没有/bin/date的其他平台上工作,或者在其他东西(例如Git版本号)中编译。此脚本基于Jmb提供的示例,该示例演示了如何在编译时将Mercurial信息添加到程序中。

这可以保证COMPILED_AT将被设置或构建失败。这样,其他Rust程序员可以通过执行cargo build来构建。

[osboxes@osboxes compiled_at]$ cargo run
   Compiling compiled_at v0.1.0 (file:///home/osboxes/Workspace/rust/compiled_at)
    Finished dev [unoptimized + debuginfo] target(s) in 1.27 secs
     Running `target/debug/compiled_at`
This is Example Program 0.1.0 compiled at 20180202

使用env!(),然后要求用户在构建

之前设置其环境

在我提出此问题后尝试以下内容时,我发现它确实有用(vim正在使用中,因此转义的% s),cargo确实通过了我的问题环境低至rustc

COMPILED_AT=`date +\%Y\%m\%d` cargo run

然后,在Rust:

fn main() {
    eprintln!("{}", env!("COMPILED_AT"));
}

如果我不提供环境变量,Rust编译器拒绝编译代码,这是一个不错的选择。

error: environment variable `COMPILED_AT` not defined
   --> src/main.rs:147:21
    |
147 |     eprintln!("{}", env!("COMPILED_AT"));
    |                     ^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

这种方式非常hacky并且保证会惹恼其他希望使用cargo build构建的Rust程序员,但它确实有效。如果可以,建议改为使用build.rs

答案 1 :(得分:1)

您可以使用build.rs脚本将环境变量添加到货物环境中,或者创建包含所需信息的额外源文件。这样,您可以添加有关构建环境的大量信息。这是一个full example,它创建一个build_info.rs源文件,其中包含:

  • 根据Mercurial标记,版本哈希和状态构建的版本信息(包括源文件夹与Mercurial文件夹不同的日期)。
  • 工具链信息,包括编译器版本和构建配置文件(例如debugrelease),感谢rustc_version包。
  • 另外,它从货物中提取一些信息(如包名称),因此不必在Cargo.toml和源代码之间重复。
#[macro_use] extern crate map_for;
extern crate rustc_version;
extern crate time;

use rustc_version::{ Channel, version_meta };
use std::collections::HashMap;
use std::env;
use std::ffi::OsStr;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::process::Command;

/// Run mercurial with the given arguments and return the output.
fn run_hg<S: AsRef<OsStr>> (args: &[S]) -> Option<String> {
   Command::new ("hg")
      .env ("HGRCPATH", "")
      .env ("LANG", "C")
      .args (args)
      .output()
      .ok()
      .and_then (|output|
                 String::from_utf8 (output.stdout)
                 .ok())
}

/// Get the version from a mercurial repository.
///
/// Version  numbers  follow the  Python  PEP440  conventions. If  the
/// current folder corresponds to a version tag, then return that tag.
/// Otherwise, identify  the closest  tag and return  a string  of the
/// form _tag_.dev_N_+_hash_. In both cases, if the current folder has
/// been  modified, then  add the  current date  as `YYYYMMDD`  to the
/// local version label.
fn get_mercurial_version_tag() -> Option<String> {
   let output = run_hg (&[ "id", "-i", "-t" ]);
   let mut iter = output.iter().flat_map (|s| s.split_whitespace()).fuse();
   let hash = match iter.next() {
      Some (hash) => hash,
      _ => { return None },
   };

   let clean = !hash.ends_with ("+");

   fn mkdate() -> String { time::strftime ("%Y%m%d", &time::now()).unwrap() }

   map_for!(
      version <- iter.find (|s| s.chars().next()
                            .map (|c| ('0' <= c) && (c <= '9'))
                            .unwrap_or (false));
         // The current folder corresponds to a version tag (i.e. a
         // tag that starts with a digit).
         => (if clean { version.into() }
             else { format!("{}+{}", version, mkdate()) }))
      .or_else (|| {
         // The current folder does not correspond to a version tag.
         // Find the closest tag and build the version from that. Note
         // that this may return a wrong version number if the closest
         // tag is not a version tag.
         let version = run_hg (
            &[ "parents",
                "--template",
                "{latesttag}.dev{latesttagdistance}+{node|short}" ]);
         if clean { version }
         else { version.map (|s| format!("{}.{}", s, mkdate())) }
      })
}

/// Get the version from Mercurial archive information.
///
/// The   Mercurial   `archive`   command   creates   a   file   named
/// `.hg_archival.txt`  that contains  information about  the archived
/// version. This function  tries to use this information  to create a
/// version string  similar to what  `get_mercurial_version_tag` would
/// have created for this version.
fn get_mercurial_archived_version_tag() -> Option<String> {
   use map_for::FlatMap;

   // Parse the contents of `.hg_archival.txt` into a hash map.
   let info = &File::open (".hg_archival.txt")
      .iter()
      .flat_map (|f| BufReader::new (f).lines())
      .filter_map (|l| l.ok())
      .map (|l| l.splitn (2, ':')
            .map (String::from)
            .collect::<Vec<_>>())
      .filter_map (
         |v| if v.len() == 2
         { Some ((String::from (v[0].trim()),
                  String::from (v[1].trim()))) }
         else { None })
      .collect::<HashMap<_,_>>();
   // Extract version information from the hash map.
   map_for!(
      tag <- info.get ("tag");
      => format!("{}+archive.{}", tag, time::strftime ("%Y%m%d", &time::now()).unwrap()))
      .or_else (|| map_for!{
         tag      <- info.get ("latesttag");
         distance <- info.get ("latesttagdistance");
         node     <- info.get ("node");
         => format!("{}.dev{}+archive.{:.12}.{}",
                    tag, distance, node,
                    time::strftime ("%Y%m%d", &time::now()).unwrap()) })
      .map (String::from)
}

/// Get the version information.
///
/// This function will  first try to get the version  from a Mercurial
/// repository. If that  fails, it will try to get  the version from a
/// `.hg_archival.txt` file. If both fail, it will return a version of
/// the form: "unknown-date".
fn get_version() -> String {
   get_mercurial_version_tag()
      .or_else (get_mercurial_archived_version_tag)
      .unwrap_or_else (
         || format!("{}+cargo.{}",
                    env::var ("CARGO_PKG_VERSION").unwrap(),
                    time::strftime ("%Y%m%d", &time::now()).unwrap())
            .into())
}

fn main()
{
   let mut f = File::create ("src/build_info.rs").unwrap();

   let version = version_meta().unwrap();
   writeln!(f, "pub const RUST_VERSION: &'static str = \"{} {} v{}\";",
            env::var ("RUSTC").unwrap_or ("rustc".into()),
            match version.channel {
               Channel::Dev => "dev",
               Channel::Nightly => "nightly",
               Channel::Beta => "beta",
               Channel::Stable => "stable",
            },
            version.semver).unwrap();
   writeln!(f, "pub const PROFILE: &'static str = \"{}\";",
            env::var ("PROFILE").unwrap_or ("unknown".into()))
      .unwrap();
   writeln!(f, "pub const TARGET: &'static str = \"{}\";",
            env::var ("TARGET").unwrap_or ("unknown".into()))
      .unwrap();
   writeln!(f, "pub const PKG_NAME: &'static str = \"{} {} {}\";",
            env::var ("CARGO_PKG_NAME").unwrap(),
            get_version(),
            env::var ("PROFILE").unwrap_or ("".into()))
      .unwrap();
   writeln!(f, "pub const PKG_VERSION: &'static str = \"{}\";",
            get_version())
      .unwrap();
}