Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

I want to make it so that when my program starts it prints to 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

In C/C++, I would achieve this with a Makefile, e.g.:

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

Then, in the source code, I would use those constants as I pleased, perhaps in a call to fprintf. After checking if they actually existed with #ifdef, of course.

How can this be achieved in Rust? Do I need to use a procedural macro? Can I use cargo somehow?

I know that env!("CARGO_PKG_VERSION") can be used as a replacement for VERSION, but what about COMPILED_AT?

like image 826
Fredrick Brennan Avatar asked Dec 23 '22 11:12

Fredrick Brennan


2 Answers

There are two ways to do this.

Use a build.rs script

The benefit of using a build.rs script is that other users compiling your program will not have to invoke cargo in a special way or set up their environment. Here is a minimal example of how to do that.

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 <[email protected]>"]
build = "build.rs"

[dependencies]

Obviously this can be tweaked to get it to work on other platforms which don't have a /bin/date or to compile in other things such as the Git version number. This script was based on the example provided by Jmb, which shows how to add Mercurial information into your program at compile time.

This guarantees that COMPILED_AT will either be set or the build will fail. This way other Rust programmers can build just by doing 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

Use env!(), then require users to set their environment prior to building

It occurred to me after I asked this question to try the following, and it does work (vim is in use, thus the escaped %s), cargo does pass my environment down to rustc:

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

Then, in Rust:

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

The Rust compiler refuses to compile the code if I don't supply the environment variable, which is a nice touch.

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

error: aborting due to previous error

This way is extremely hacky and is guaranteed to annoy other Rust programmers who just expect to build with cargo build, but it does work. If you can, it is recommended to use the build.rs instead.

like image 67
Fredrick Brennan Avatar answered Jan 04 '23 22:01

Fredrick Brennan


You can use the build.rs script to add an environment variable to the cargo environment or to create an extra source file that contains the information you need. That way you can add a lot of information about the build environment. Here is a full example that creates a build_info.rs source file containing:

  • Version information built from the Mercurial tag, revision hash and status (including the date if the source folder is different from the Mercurial folder).
  • Toolchain information, including the compiler version and the build profile (eg. debug or release) thanks to the rustc_version crate.
  • Plus it extracts some information from cargo (like the package name) so it doesn't have to be duplicated between the Cargo.toml and the source code).

#[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();
}
like image 24
Jmb Avatar answered Jan 04 '23 22:01

Jmb