How can I pass version information from Vergen to StructOpt? - rust

I'm trying to setup a simple CLI program in Rust, but I want it to display the version based on the info in git describe rather than whatever is it the Cargo.toml file.
I setup some basic dependencies:
[dependencies]
structopt = "0.3.13"
[build-dependencies]
vergen = "3.1.0"
And a build.rs file does the lifting with vergen:
extern crate vergen;
use vergen::{ConstantsFlags, generate_cargo_keys};
fn main() {
// Setup the flags, toggling off the 'SEMVER_FROM_CARGO_PKG' flag
let mut flags = ConstantsFlags::all();
flags.toggle(ConstantsFlags::SEMVER_FROM_CARGO_PKG);
// Generate the 'cargo:' key output
generate_cargo_keys(flags).expect("Unable to generate the cargo keys!");
}
The result is a set of env variables usable from my main.rs:
fn main() {
println!("Build SHA: {}", env!("VERGEN_SHA_SHORT"));
}
This prints the correct git SHA I built from. The trouble is I can't figure out how to make StruckOpt use the generated version. I would have expected something like this to work:
use structopt::StructOpt;
/// A thing
#[derive(StructOpt)]
struct Cli {
version: [ version = env!("VERGEN_SEMVER") ],
}
fn main() {
let args = Cli::from_args();
println!("Build SHA: {}", env!("VERGEN_SHA_SHORT"));
}
But alas! In spite of this syntax being documented, it throws a parse error trying to build:
error: expected one of `!`, `(`, `+`, `::`, `;`, `<`, or `]`, found `=`
--> src/main.rs:6:24
|
6 | version: [ version = env!("VERGEN_SEMVER") ],
| ^ expected one of 7 possible tokens
What gives? How do I setup my CLI using StructOpt such that --version will report the version detected by vergen?

The [version = "version"] notation in the manual means that we need to write #[structopt(version = "version")] to specify a string of the version number:
$ cat src/main.rs
use structopt::StructOpt;
#[derive(StructOpt)]
#[structopt(version = env!("VERGEN_SEMVER"))]
struct Cli {}
fn main() {
let _cli = Cli::from_args();
}
$ env VERGEN_SEMVER=v1.2.3 cargo build
...
$ cargo run -- --version
Finished dev [unoptimized + debuginfo] target(s) in 0.02s
Running `target/debug/foo --version`
foo v1.2.3

Related

How do I indicate that an argument must be passed?

I'm writing a CLI tool using Clap. Usage should look like this:
$ my_tool command argument
The argument is necessary for the command to work. This is the code I have at the moment:
use::clap::{Parser, Subcommand};
#[derive(Parser)]
#[clap(author, version, about)]
struct Cli {
#[clap(subcommand)]
subcommand: Subcommands,
}
#[derive(Subcommand)]
enum Subcommands {
Command {
#[clap(value_parser)]
argument: Vec<String>,
}
// --snip--
}
fn main() {
let _cli = Cli::parse();
// --snip--
}
I get this when trying to use the tool:
~/my_tool$ cargo run -- command
Compiling my_tool v0.1.0 (/home/derch/my_tool)
Finished dev [unoptimized + debuginfo] target(s) in 0.78s
Running `target/debug/my_tool command`
~/my_tool$
I was expecting an error message because I didn't provide an argument after the command. How do I make the program to require the argument?
Because this is a Vec, by default it allows zero values. You can fix that by making it explicitly required:
#[derive(Subcommand)]
enum Subcommands {
Command {
#[clap(value_parser, required = true)]
argument: Vec<String>,
}
// --snip--
}

structopt unable to find its Args parser when defined in a separate file

I'm starting on a command-line tool in Rust, and hitting a wall right from the get-go. I can parse command-line arguments using StructOpt if the Opt struct is defined in main.rs, but since I want to be able to pass the Opt struct into the library code, I'm defining it in its own file so that other parts of the library know what it's all about.
Here's the version of the code that's dumping the fewest errors, although I concede that it's largely cobbled together by trying things suggested by the compiler and some random SO suggestions, so it may be completely wrong.
The build error I'm getting is:
$ cargo run
Compiling basic v0.1.0 (/home/mpalmer/src/action-validator/blobble)
error[E0433]: failed to resolve: maybe a missing crate `structopt`?
--> src/opt.rs:8:5
|
8 | /// Activate debug mode
| ^^^^^^^^^^^^^^^^^^^^^^^ not found in `structopt::clap`
|
help: consider importing this struct
|
3 | use opt::structopt::clap::Arg;
|
For more information about this error, try `rustc --explain E0433`.
error: could not compile `basic` due to previous error
$ cargo --version
cargo 1.56.0 (4ed5d137b 2021-10-04)
$ rustc --version
rustc 1.56.0 (09c42c458 2021-10-18)
(Yes, I have tried adding use opt::structopt::clap::Arg;, just in case, but the error doesn't go away and I get a warning about an unused import -- while still being told to try adding the same use that is unused, which is amusing)
Cargo.toml
[package]
name = "basic"
version = "0.1.0"
authors = ["User"]
[dependencies]
structopt = "0.3"
src/main.rs
extern crate basic;
extern crate structopt;
use basic::Opt;
use structopt::StructOpt;
fn main() {
let opt = Opt::from_args();
println!("{:#?}", opt)
}
src/lib.rs
mod opt;
pub use crate::opt::Opt;
src/opt.ts
extern crate structopt;
use self::structopt::StructOpt;
#[derive(StructOpt, Debug)]
#[structopt(name = "basic")]
pub struct Opt {
/// Activate debug mode
#[structopt(short,long)]
debug: bool,
}
Suggestions gratefully appreciated.
A working version is
Cargo.toml
[package]
name = "basic"
version = "0.1.0"
authors = ["User"]
edition = "2018"
[dependencies]
structopt = "0.3"
lib.rs
#[macro_use] extern crate structopt;
use structopt::StructOpt;
pub mod opt;
opt.rs
#[derive(StructOpt, Debug)]
#[structopt(name = "basic")]
pub struct Opt {
/// Activate debug mode
#[structopt(short,long)]
debug: bool,
}
main.rs
#[macro_use] extern crate structopt;
use structopt::StructOpt;
fn main() {
let opt = basic::opt::Opt::from_args();
println!("{:#?}", opt);
}
You need to declare use structopt::StructOpt because from_args trait must be in the scope.

Compilation error: can't find crate for `core`

I'm using Rust 1.35.0 to try out some Rust examples and I could not get it to compile, as I keep getting the following message:
error[E0463]: can't find crate for `core`
I ran rustc --explain E0463 and I see the following message:
You need to link your code to the relevant crate in order to be able to use it
(through Cargo or the `-L` option of rustc example). Plugins are crates as
well, and you link to them the same way.
Here is my Cargo.toml:
[package]
name = "sensor-node"
version = "0.1.0"
authors = ["joesan <email#gmail.com>"]
edition = "2018"
[dependencies]
dwm1001 = "0.1.0"
panic-halt = "0.2.0"
nb = "0.1.1"
Here is my main.rs:
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function,
// but i32 is Copy, so it’s okay to still
// use x afterward
} // Here, x goes out of scope, then s. But because s's value was moved, nothing
// special happens.
fn takes_ownership(some_string: String) { // some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) { // some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.
Your code works fine on the Rust playground, so I recommend checking your Rust installation and environment settings.
You may want to use the preconfigured Rust Docker image to run your app. Have Docker installed, then:
docker pull rust
Go to your project folder and run:
docker run --rm --user "$(id -u)":"$(id -g)" -v "$PWD":/usr/src/myapp -w /usr/src/myapp rust cargo run
Output:
hello
5
For your simple example on a PC, you don't need any of these dependencies:
[dependencies]
dwm1001 = "0.1.0"
panic-halt = "0.2.0"
nb = "0.1.1"
Here are my steps to test your sample on Linux:
cargo new hello
cd hello
code .
Open main.rs and paste your sample main.rs and save:
fn main() {
let s = String::from("hello"); // s comes into scope
takes_ownership(s); // s's value moves into the function...
// ... and so is no longer valid here
let x = 5; // x comes into scope
makes_copy(x); // x would move into the function,
// but i32 is Copy, so it’s okay to still
// use x afterward
} // Here, x goes out of scope, then s. But because s's value was moved, nothing
// special happens.
fn takes_ownership(some_string: String) {
// some_string comes into scope
println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
// memory is freed.
fn makes_copy(some_integer: i32) {
// some_integer comes into scope
println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.
In a terminal inside the hello folder, run:
cargo run
And the output is good:
hello
5
This may help:
Shell command
rustup component list --installed
Output:
cargo-x86_64-unknown-linux-gnu
clippy-x86_64-unknown-linux-gnu
rls-x86_64-unknown-linux-gnu
rust-analysis-x86_64-unknown-linux-gnu
rust-docs-x86_64-unknown-linux-gnu
rust-src
rust-std-x86_64-unknown-linux-gnu
rustc-x86_64-unknown-linux-gnu
rustfmt-x86_64-unknown-linux-gnu
Shell command:
rustup show
Output:
Default host: x86_64-unknown-linux-gnu
installed toolchains
--------------------
stable-x86_64-unknown-linux-gnu (default)
nightly-x86_64-unknown-linux-gnu
active toolchain
----------------
stable-x86_64-unknown-linux-gnu (default)
rustc 1.35.0 (3c235d560 2019-05-20)

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?
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 <copypaste#kittens.ph>"]
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.
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();
}

How do I specify the linker path in Rust?

I'm trying to link a Rust program with libsoundio. I'm using Windows and there's a GCC binary download available. I can link it like this if I put it in the same folder as my project:
#[link(name = ":libsoundio-1.1.0/i686/libsoundio.a")]
#[link(name = "ole32")]
extern {
fn soundio_version_string() -> *const c_char;
}
But I really want to specify #[link(name = "libsoundio")] or even #[link(name = "soundio")], and then provide a linker path somewhere else.
Where can I specify that path?
I tried the rustc-link-search suggestion as follows:
#[link(name = "libsoundio")]
#[link(name = "ole32")]
extern {
fn soundio_version_string() -> *const c_char;
}
And in .cargo/config:
[target.i686-pc-windows-gnu.libsoundio]
rustc-link-search = ["libsoundio-1.1.0/i686"]
rustc-link-lib = ["libsoundio.a"]
[target.x86_64-pc-windows-gnu.libsoundio]
rustc-link-search = ["libsoundio-1.1.0/x86_64"]
rustc-link-lib = ["libsoundio.a"]
But it still only passes "-l" "libsoundio" to gcc and fails with the same ld: cannot find -llibsoundio. Am I missing something really obvious? The docs seem to suggest this should work.
As stated in the documentation for a build script:
All the lines printed to stdout by a build script [... starting] with cargo: is interpreted directly by Cargo [...] rustc-link-search indicates the specified value should be passed to the compiler as a -L flag.
In your Cargo.toml:
[package]
name = "link-example"
version = "0.1.0"
authors = ["An Devloper <an.devloper#example.com>"]
build = "build.rs"
And your build.rs:
fn main() {
println!(r"cargo:rustc-link-search=C:\Rust\linka\libsoundio-1.1.0\i686");
}
Note that your build script can use all the power of Rust and can output different values depending on target platform (e.g. 32- and 64-bit).
Finally, your code:
extern crate libc;
use libc::c_char;
use std::ffi::CStr;
#[link(name = "soundio")]
extern {
fn soundio_version_string() -> *const c_char;
}
fn main() {
let v = unsafe { CStr::from_ptr(soundio_version_string()) };
println!("{:?}", v);
}
The proof is in the pudding:
$ cargo run
Finished debug [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target\debug\linka.exe`
"1.0.3"
Ideally, you will create a soundio-sys package, using the convention for *-sys packages. That simply has a build script that links to the appropriate libraries and exposes the C methods. It will use the Cargo links key to uniquely identify the native library and prevent linking to it multiple times. Other libraries can then include this new crate and not worry about those linking details.
Another possible way is setting the RUSTFLAGS like:
RUSTFLAGS='-L my/lib/location' cargo build # or cargo run
I don't know if this is the most organized and recommended approach, but it worked for my simple project.
I found something that works OK: you can specify links in your Cargo.toml:
[package]
links = "libsoundio"
build = "build.rs"
This specifies that the project links to libsoundio. Now you can specify the search path and library name in the .cargo/config file:
[target.i686-pc-windows-gnu.libsoundio]
rustc-link-search = ["libsoundio-1.1.0/i686"]
rustc-link-lib = [":libsoundio.a"]
[target.x86_64-pc-windows-gnu.libsoundio]
rustc-link-search = ["libsoundio-1.1.0/x86_64"]
rustc-link-lib = [":libsoundio.a"]
(The : prefix tells GCC to use the actual filename and not to do all its idiotic lib-prepending and extension magic.)
You also need to create an empty build.rs:
fn main() {}
This file is never run, because the values in .cargo/config override its output, but for some reason Cargo still requires it - any time you use links = you have to have build =, even if it isn't used.
Finally in main.rs:
#[link(name = "libsoundio")]
#[link(name = "ole32")]
extern {
fn soundio_version_string() -> *const c_char;
}

Resources