Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Don't rebuild daemon every time new git refs are fetched #6211

Merged
merged 5 commits into from
May 16, 2024
Merged
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
126 changes: 85 additions & 41 deletions mullvad-version/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,76 +44,120 @@ fn main() {
.unwrap();
}

/// Returns the Mullvad product version from the corresponding metadata files,
/// depending on target platform.
/// Computes the Mullvad product version using the latest release on the given platform and the git
/// hash pointed to by `HEAD`. Also triggers a rebuild of this crate when the information becomes
/// outdated.
fn get_product_version(target: Target) -> String {
let version_file_path = match target {
Target::Android => ANDROID_VERSION_FILE_PATH,
Target::Desktop => DESKTOP_VERSION_FILE_PATH,
};
println!("cargo:rerun-if-changed={version_file_path}");
let version = fs::read_to_string(version_file_path)

let release_version = fs::read_to_string(version_file_path)
.unwrap_or_else(|_| panic!("Failed to read {version_file_path}"))
.trim()
.to_owned();

if let Some(dev_suffix) = get_dev_suffix(target, &version) {
format!("{version}{dev_suffix}")
} else {
version
}
}

/// Returns the development suffix for the current build. A build has a development
/// suffix if the build is not done on a git tag named `product_version`.
/// This also returns `None` if the `git` command can't run, or the code does
/// not live in a git repository.
fn get_dev_suffix(target: Target, product_version: &str) -> Option<String> {
// Compute the expected tag name for the release named `product_version`
let release_tag = match target {
Target::Android => format!("android/{product_version}"),
Target::Desktop => product_version.to_owned(),
Target::Android => format!("android/{release_version}"),
Target::Desktop => release_version.to_owned(),
};

// Get the git commit hashes for the latest release and current HEAD
// Return `None` if unable to find the hash for HEAD.
let head_commit_hash = git_rev_parse_commit_hash("HEAD")?;
let product_version_commit_hash = git_rev_parse_commit_hash(&release_tag);
format!("{release_version}{}", get_suffix(&release_tag))
}

/// Returns the suffix for the current build. If the build is done on a git tag named
/// `product_version` or a git repository cannot be found, the suffix is empty. Otherwise,
/// `-dev-$hash` is appended to the release version.
fn get_suffix(release_tag: &str) -> String {
if !valid_git_repo() {
return String::new();
};
// Rerun this build script on changes to the git ref that affects the build version.
// NOTE: This must be kept up to date with the behavior of `git_rev_parse_commit_hash`.
rerun_if_git_ref_changed(release_tag);
let head_commit_hash =
git_rev_parse_commit_hash("HEAD").expect("Failed to run `git rev-parse HEAD^{{commit}}`");
let product_version_commit_hash = git_rev_parse_commit_hash(release_tag);

// If we are currently building the release tag, there is no dev suffix
if Some(&head_commit_hash) == product_version_commit_hash.as_ref() {
return None;
String::new()
} else {
format!("-dev-{}", &head_commit_hash[..GIT_HASH_DEV_SUFFIX_LEN])
}
Some(format!(
"-dev-{}",
&head_commit_hash[..GIT_HASH_DEV_SUFFIX_LEN]
))
}

/// Returns the commit hash for the commit that `git_ref` is pointing to.
fn valid_git_repo() -> bool {
matches!(Command::new("git").arg("status").status(), Ok(status) if status.success())
}

/// Trigger rebuild of `mullvad-version` on changing branch (`.git/HEAD`), on changes to the ref of
/// the current branch (`.git/refs/heads/$current_branch`) and on changes to the ref of the current
/// release tag (`.git/refs/tags/$current_release_tag`).
///
/// Returns `None` if executing the `git rev-parse` command fails for some reason.
fn git_rev_parse_commit_hash(git_ref: &str) -> Option<String> {
/// Returns an error if not in a git repository, or the git binary is not in `PATH`.
fn rerun_if_git_ref_changed(release_tag: &str) {
let git_dir = Path::new("..").join(".git");
// If we build our output on information about HEAD we need to re-run if HEAD moves
if git_ref == "HEAD" {
let head_path = git_dir.join("HEAD");
if head_path.exists() {
println!("cargo:rerun-if-changed={}", head_path.display());
}

// The `.git/HEAD` file contains the position of the current head. If in 'detached HEAD' state,
// this will be the ref of the current commit. If on a branch it will just point to it, e.g.
// `ref: refs/heads/main`. Tracking changes to this file will tell us if we change branch, or
// modify the current detached HEAD state (e.g. committing or rebasing).
let head_path = git_dir.join("HEAD");
if head_path.exists() {
println!("cargo:rerun-if-changed={}", head_path.display());
}
// If we build our output on information about a git reference, we need to re-run
// if it moves. Instead of trying to be smart, just re-run if any git reference moves.
let git_refs_dir = git_dir.join("refs");
if git_refs_dir.exists() {
println!("cargo:rerun-if-changed={}", git_refs_dir.display());

// The above check will not cause a rebuild when modifying commits on a currently checked out
// branch. To catch this, we need to track the `.git/refs/heads/$current_branch` file.
let output = Command::new("git")
.arg("branch")
.arg("--show-current")
.output()
.expect("Failed to execute `git branch --show-current`");

let current_branch = String::from_utf8(output.stdout).unwrap();
let current_branch = current_branch.trim();

// When in 'detached HEAD' state, the output will be empty. However, in that case we already get
// the ref from `.git/HEAD`, so we can safely skip this part.
if !current_branch.is_empty() {
let git_current_branch_ref = git_dir.join("refs").join("heads").join(current_branch);
if git_current_branch_ref.exists() {
println!(
"cargo:rerun-if-changed={}",
git_current_branch_ref.display()
);
}
}

// Since the product version depends on if the build is done on the commit with the
// corresponding release tag or not, we must track creation of/changes to said tag
let git_release_tag_ref = git_dir.join("refs").join("tags").join(release_tag);
if git_release_tag_ref.exists() {
println!("cargo:rerun-if-changed={}", git_release_tag_ref.display());
};

// NOTE: As the repository has gotten quite large, you may find the contents of the
// `.git/refs/heads` and `.git/refs/tags` empty. This happens because `git pack-refs` compresses
// and moves the information into the `.git/packed-refs` file to save storage. We do not have to
// track this file, however, as any changes to the current branch, 'detached HEAD' state
// or tags will update the corresponding `.git/refs` file we are tracking, even if it had
// previously been pruned.
}

/// Returns the commit hash for the commit that `git_ref` is pointing to.
///
/// Returns `None` if the git reference cannot be found.
fn git_rev_parse_commit_hash(git_ref: &str) -> Option<String> {
let output = Command::new("git")
.arg("rev-parse")
.arg(format!("{git_ref}^{{commit}}"))
.output()
.ok()?;
.expect("Failed to run `git rev-parse`");
if !output.status.success() {
return None;
}
Expand Down
Loading