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

How can I compile a string supplied on the command line during compilation into my Rust binary?

我想让它在我的程序启动时打印到 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呢?

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

使用 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代替。

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

  • 从 Mercurial 标签、修订散列和状态构建的版本信息(如果源文件夹与 Mercurial 文件夹不同,则包括日期)。
  • 工具链信息,包括编译器版本和构建配置文件(例如 debugrelease)感谢 rustc_version crate。
  • 此外,它还从货物中提取了一些信息(如包裹名称),因此不必在 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();
}