Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5625,7 +5625,7 @@ dependencies = [

[[package]]
name = "openvmm"
version = "0.0.0"
version = "0.1.0"
dependencies = [
"crypto",
"embed-resource",
Expand All @@ -5634,6 +5634,10 @@ dependencies = [
"openvmm_resources",
]

[[package]]
name = "openvmm_build_info"
version = "0.1.0"

[[package]]
name = "openvmm_core"
version = "0.0.0"
Expand Down Expand Up @@ -5801,6 +5805,7 @@ dependencies = [
"net_tap",
"netvsp_resources",
"nvme_resources",
"openvmm_build_info",
"openvmm_defs",
"openvmm_helpers",
"openvmm_pcat_locator",
Expand Down
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ exclude = [
[workspace.package]
rust-version = "1.95"
edition = "2024"
# The canonical OpenVMM version. It remains at the most recently released
# version until a reviewed pull request selects the next release.
version = "0.1.0"

[workspace.dependencies]
xtask_fuzz = { path = "xtask/xtask_fuzz" }
Expand All @@ -77,6 +80,8 @@ xtask_fuzz = { path = "xtask/xtask_fuzz" }
opentmk_protocol = { path = "opentmk_protocol" }
opentmk_disk = { path = "opentmk/opentmk_disk" }

openvmm_build_info = { path = "openvmm/openvmm_build_info" }

flowey = { path = "flowey/flowey" }
flowey_cli = { path = "flowey/flowey_cli" }
flowey_core = { path = "flowey/flowey_core" }
Expand Down
2 changes: 2 additions & 0 deletions openvmm/openvmm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

[package]
name = "openvmm"
version.workspace = true
publish = false
edition.workspace = true
rust-version.workspace = true

Expand Down
87 changes: 84 additions & 3 deletions openvmm/openvmm/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,62 @@

#![expect(missing_docs)]

use std::path::Path;
use std::process::Command;

const RELEASE_TAG_PREFIX: &str = "openvmm-v";

fn git(repo: &Path, args: &[&str]) -> Option<String> {
let output = Command::new("git")
.arg("-C")
.arg(repo)
.args(args)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8(output.stdout).ok()?;
let stdout = stdout.trim().to_owned();
(!stdout.is_empty()).then_some(stdout)
}

fn git_repository_starts_at(repo: &Path) -> bool {
Command::new("git")
.arg("-C")
.arg(repo)
.args(["rev-parse", "--show-prefix"])
.output()
.is_ok_and(|output| {
output.status.success() && output.stdout.iter().all(u8::is_ascii_whitespace)
})
}

fn at_release_tag(repo: &Path, version: &str) -> bool {
let Some(tags) = git(repo, &["tag", "--points-at", "HEAD"]) else {
return false;
};
let release_tag = format!("{RELEASE_TAG_PREFIX}{version}");
tags.lines().any(|tag| tag.trim() == release_tag)
}

fn watch_git_identity(repo: &Path, version: &str) {
for path in ["HEAD", "packed-refs"] {
if let Some(path) = git(repo, &["rev-parse", "--git-path", path]) {
println!("cargo:rerun-if-changed={}", repo.join(path).display());
}
}
if let Some(head_ref) = git(repo, &["symbolic-ref", "HEAD"])
&& let Some(path) = git(repo, &["rev-parse", "--git-path", &head_ref])
{
println!("cargo:rerun-if-changed={}", repo.join(path).display());
}
let tag = format!("refs/tags/{RELEASE_TAG_PREFIX}{version}");
if let Some(path) = git(repo, &["rev-parse", "--git-path", &tag]) {
println!("cargo:rerun-if-changed={}", repo.join(path).display());
}
}

fn main() {
// Prevent this build script from rerunning unnecessarily.
println!("cargo:rerun-if-changed=build.rs");
Expand All @@ -18,14 +74,38 @@ fn main() {
println!("cargo:rerun-if-env-changed=OPENVMM_PATCH");
println!("cargo:rerun-if-env-changed=OPENVMM_REVISION");

// Default to the crate version so that the version Windows reports in
// the file properties and the one `openvmm --version` prints cannot
// disagree. The `OPENVMM_*` vars still win, which is how a build
// pipeline stamps its own build number in. There is no crate
// equivalent of the fourth component, so it stays 0 unless set.
let parse_u16 = |s: String| s.parse::<u16>().unwrap_or(0);
let major = std::env::var("OPENVMM_MAJOR").map(parse_u16).unwrap_or(0);
let minor = std::env::var("OPENVMM_MINOR").map(parse_u16).unwrap_or(0);
let patch = std::env::var("OPENVMM_PATCH").map(parse_u16).unwrap_or(0);
let component = |var: &str, from_crate_version: &str| {
std::env::var(var)
.or_else(|_| std::env::var(from_crate_version))
.map(parse_u16)
.unwrap_or(0)
};
let major = component("OPENVMM_MAJOR", "CARGO_PKG_VERSION_MAJOR");
let minor = component("OPENVMM_MINOR", "CARGO_PKG_VERSION_MINOR");
let patch = component("OPENVMM_PATCH", "CARGO_PKG_VERSION_PATCH");
let revision = std::env::var("OPENVMM_REVISION")
.map(parse_u16)
.unwrap_or(0);

// VS_FF_PRERELEASE. Keep Windows file metadata consistent with
// `openvmm --version`: any checkout other than the exact release tag is
// a development build, while an extracted source archive is a release.
// A build script cannot read another crate's `rustc-env`, so this small
// Git probe is intentionally duplicated from `openvmm_build_info`.
let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let checkout = git_repository_starts_at(&repo_root);
if checkout {
watch_git_identity(&repo_root, env!("CARGO_PKG_VERSION"));
}
let prerelease = checkout && !at_release_tag(&repo_root, env!("CARGO_PKG_VERSION"));
let file_flags = if prerelease { 0x2 } else { 0x0 };

let macros = [
(
"OPENVMM_VERSION",
Expand All @@ -35,6 +115,7 @@ fn main() {
"OPENVMM_VERSION_STR",
format!(r#""{major}.{minor}.{patch}.{revision}""#),
),
("OPENVMM_FILE_FLAGS", format!("{file_flags:#x}")),
];

embed_resource::compile(
Expand Down
3 changes: 3 additions & 0 deletions openvmm/openvmm/resources.rc
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
// Values are provided via build.rs using the following macros:
// - OPENVMM_VERSION (e.g., 1,2,3,4)
// - OPENVMM_VERSION_STR (e.g., "1.2.3.4")
// - OPENVMM_FILE_FLAGS (e.g., 0x2 for VS_FF_PRERELEASE)

1 VERSIONINFO
FILEVERSION OPENVMM_VERSION
PRODUCTVERSION OPENVMM_VERSION
FILEFLAGSMASK 0x3FL
FILEFLAGS OPENVMM_FILE_FLAGS
FILEOS 0x10004
FILETYPE 0x1
{
Expand Down
12 changes: 12 additions & 0 deletions openvmm/openvmm_build_info/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

[package]
name = "openvmm_build_info"
version.workspace = true
publish = false
edition.workspace = true
rust-version.workspace = true

[lints]
workspace = true
176 changes: 176 additions & 0 deletions openvmm/openvmm_build_info/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

#![expect(missing_docs)]

use std::path::Path;
use std::path::PathBuf;
use std::process::Command;

/// Prefix of the Git tag naming an OpenVMM release.
///
/// This is intentionally duplicated from the release tooling. Source consumers
/// build this crate without that tooling, so build identity cannot depend on it.
const RELEASE_TAG_PREFIX: &str = "openvmm-v";

fn git(repo: &Path, args: &[&str]) -> Option<String> {
let output = Command::new("git")
.arg("-C")
.arg(repo)
.args(args)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8(output.stdout).ok()?;
let stdout = stdout.trim().to_owned();
(!stdout.is_empty()).then_some(stdout)
}

fn git_repository_starts_at(repo: &Path) -> bool {
Command::new("git")
.arg("-C")
.arg(repo)
.args(["rev-parse", "--show-prefix"])
.output()
.is_ok_and(|output| {
output.status.success() && output.stdout.iter().all(u8::is_ascii_whitespace)
})
}

/// The commit this tree was built from, if it is a Git checkout at all.
///
/// A released source archive has no `.git`, and neither does the tree a
/// packager extracts and builds. That is the expected case rather than an
/// error, so this returns `None` instead of failing the build.
fn revision(repo: &Path) -> Option<String> {
// Git searches parent directories, so an archive extracted inside an
// unrelated checkout would otherwise silently report *that* checkout's
// HEAD. Only trust the answer if the repository we found starts exactly
// where OpenVMM does.
if !git_repository_starts_at(repo) {
return None;
}
git(repo, &["rev-parse", "HEAD"])
}

/// Whether HEAD is the exact commit tagged for `version`.
///
/// A checkout without tags answers "no", which fails safely: it may report a
/// release checkout as a development build, but never the reverse.
fn at_release_tag(repo: &Path, version: &str) -> bool {
let Some(tags) = git(repo, &["tag", "--points-at", "HEAD"]) else {
return false;
};
let release_tag = format!("{RELEASE_TAG_PREFIX}{version}");
tags.lines().any(|tag| tag.trim() == release_tag)
}

/// Notice the release tag arriving after the tree was already built.
fn watch_release_tag(repo: &Path, version: &str) {
let tag = format!("refs/tags/{RELEASE_TAG_PREFIX}{version}");
if let Some(path) = git(repo, &["rev-parse", "--git-path", &tag]) {
println!("cargo:rerun-if-changed={}", repo.join(path).display());
}
}

/// Watch just enough of `.git` to notice HEAD moving.
///
/// This deliberately does not watch tracked files. Reporting whether the tree
/// is dirty would mean emitting a `rerun-if-changed` for every file in the
/// repository, which costs a stat of the whole tree on every single build. The
/// revision on its own is worth two files; a dirty flag is not worth thousands.
fn watch_head(repo: &Path) {
for path in ["HEAD", "packed-refs"] {
if let Some(path) = git(repo, &["rev-parse", "--git-path", path]) {
println!("cargo:rerun-if-changed={}", repo.join(path).display());
}
}
// On a branch, HEAD is a pointer; the commit changes when the ref it names
// does, which is a different file.
if let Some(head_ref) = git(repo, &["symbolic-ref", "HEAD"])
&& let Some(path) = git(repo, &["rev-parse", "--git-path", &head_ref])
{
println!("cargo:rerun-if-changed={}", repo.join(path).display());
}
}

fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-env-changed=OPENVMM_PKGVERSION");

// The version is a committed fact, inherited from `[workspace.package]`.
// Git is consulted only to enrich it, never to determine it.
let product_version = env!("CARGO_PKG_VERSION");

let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
let revision = revision(&repo_root);
if revision.is_some() {
watch_head(&repo_root);
watch_release_tag(&repo_root, product_version);
}
let at_release_tag = revision.is_some() && at_release_tag(&repo_root, product_version);

// `OPENVMM_PKGVERSION` lets whoever builds the binary stamp their own build
// identity in, the way QEMU's `-Dpkgversion` and cloud-hypervisor's
// `CH_EXTRA_VERSION` do, so a bug report names the build it came from. An
// empty value is treated as unset, since build systems routinely pass an
// undefined variable through as `""`.
let pkgversion = match std::env::var("OPENVMM_PKGVERSION") {
Ok(pkgversion) if !pkgversion.is_empty() => Some(pkgversion),
_ => None,
};

let version = match &pkgversion {
Some(pkgversion) => pkgversion.clone(),
None => match &revision {
// Semver build metadata, so it orders identically to the plain
// version and a build from a checkout is never mistaken for one
// from the matching release archive.
Some(revision) if !at_release_tag => {
format!("{product_version}+g{}", &revision[..9.min(revision.len())])
}
_ => product_version.to_owned(),
},
};

// Deliberately not a boolean "official". OpenVMM ships as source that
// someone else builds, so a packager's binary is legitimately not ours and
// yet is a legitimate build of an official version. Report what is known
// and let the consumer judge.
let (kind, kind_description) = if pkgversion.is_some() {
("custom", "custom (built with OPENVMM_PKGVERSION)")
} else if revision.is_none() || at_release_tag {
("release", "release")
} else {
("development", "development (not an official release)")
};

let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".into());
let long_version = format!(
"{version}\n\
build: {kind_description}\n\
version: {product_version}\n\
commit: {}\n\
host: {target}",
revision.as_deref().unwrap_or("(not built from a checkout)"),
);
Comment on lines +150 to +158

println!("cargo:rustc-env=OPENVMM_VERSION={version}");
println!("cargo:rustc-env=OPENVMM_PRODUCT_VERSION={product_version}");
println!("cargo:rustc-env=OPENVMM_BUILD_KIND={kind}");
println!("cargo:rustc-env=OPENVMM_TARGET={target}");
// Written to a file rather than emitted as `rustc-env`, because cargo parses
// build script output a line at a time and would silently keep only the
// first line of a multi-line value. Pre-formatted here because a build
// script can compose it from optional parts, where `concat!` in the library
// could not.
let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").expect("cargo sets OUT_DIR"));
std::fs::write(out_dir.join("long_version.txt"), &long_version)
.expect("failed to write long version");
println!(
"cargo:rustc-env=OPENVMM_REVISION={}",
revision.unwrap_or_default()
);
}
Loading
Loading