372 lines
13 KiB
Rust
372 lines
13 KiB
Rust
|
|
use std::net::{SocketAddr, TcpStream};
|
||
|
|
use std::process::{Command, Stdio, exit};
|
||
|
|
use std::time::Duration;
|
||
|
|
use std::{env, fs, process, thread};
|
||
|
|
|
||
|
|
const JOSH_PORT: u16 = 42042;
|
||
|
|
const DEFAULT_PR_BRANCH: &str = "update-builtins";
|
||
|
|
|
||
|
|
pub struct GitSync {
|
||
|
|
upstream_repo: String,
|
||
|
|
upstream_ref: String,
|
||
|
|
upstream_url: String,
|
||
|
|
josh_filter: String,
|
||
|
|
josh_url_base: String,
|
||
|
|
}
|
||
|
|
|
||
|
|
/// This code was adapted from the miri repository, via the rustc-dev-guide
|
||
|
|
/// (<https://github.com/rust-lang/rustc-dev-guide/tree/c51adbd12d/josh-sync>)
|
||
|
|
impl GitSync {
|
||
|
|
pub fn from_current_dir() -> Self {
|
||
|
|
let upstream_repo =
|
||
|
|
env::var("UPSTREAM_ORG").unwrap_or_else(|_| "rust-lang".to_owned()) + "/rust";
|
||
|
|
|
||
|
|
Self {
|
||
|
|
upstream_url: format!("https://github.com/{upstream_repo}"),
|
||
|
|
upstream_repo,
|
||
|
|
upstream_ref: env::var("UPSTREAM_REF").unwrap_or_else(|_| "HEAD".to_owned()),
|
||
|
|
josh_filter: ":/library/compiler-builtins".to_owned(),
|
||
|
|
josh_url_base: format!("http://localhost:{JOSH_PORT}"),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Pull from rust-lang/rust to compiler-builtins.
|
||
|
|
pub fn rustc_pull(&self, commit: Option<String>) {
|
||
|
|
let Self {
|
||
|
|
upstream_ref,
|
||
|
|
upstream_url,
|
||
|
|
upstream_repo,
|
||
|
|
..
|
||
|
|
} = self;
|
||
|
|
|
||
|
|
let new_upstream_base = commit.unwrap_or_else(|| {
|
||
|
|
let out = check_output(["git", "ls-remote", upstream_url, upstream_ref]);
|
||
|
|
out.split_whitespace()
|
||
|
|
.next()
|
||
|
|
.unwrap_or_else(|| panic!("could not split output: '{out}'"))
|
||
|
|
.to_owned()
|
||
|
|
});
|
||
|
|
|
||
|
|
ensure_clean();
|
||
|
|
|
||
|
|
// Make sure josh is running.
|
||
|
|
let _josh = Josh::start();
|
||
|
|
let josh_url_filtered = self.josh_url(
|
||
|
|
&self.upstream_repo,
|
||
|
|
Some(&new_upstream_base),
|
||
|
|
Some(&self.josh_filter),
|
||
|
|
);
|
||
|
|
|
||
|
|
let previous_upstream_base = fs::read_to_string("rust-version")
|
||
|
|
.expect("failed to read `rust-version`")
|
||
|
|
.trim()
|
||
|
|
.to_string();
|
||
|
|
assert_ne!(previous_upstream_base, new_upstream_base, "nothing to pull");
|
||
|
|
|
||
|
|
let orig_head = check_output(["git", "rev-parse", "HEAD"]);
|
||
|
|
println!("original upstream base: {previous_upstream_base}");
|
||
|
|
println!("new upstream base: {new_upstream_base}");
|
||
|
|
println!("original HEAD: {orig_head}");
|
||
|
|
|
||
|
|
// Fetch the latest upstream HEAD so we can get a summary. Use the Josh URL for caching.
|
||
|
|
run([
|
||
|
|
"git",
|
||
|
|
"fetch",
|
||
|
|
&self.josh_url(&self.upstream_repo, Some(&new_upstream_base), Some(":/")),
|
||
|
|
&new_upstream_base,
|
||
|
|
"--depth=1",
|
||
|
|
]);
|
||
|
|
let new_summary = check_output(["git", "log", "-1", "--format=%h %s", &new_upstream_base]);
|
||
|
|
|
||
|
|
// Update rust-version file. As a separate commit, since making it part of
|
||
|
|
// the merge has confused the heck out of josh in the past.
|
||
|
|
// We pass `--no-verify` to avoid running git hooks.
|
||
|
|
// We do this before the merge so that if there are merge conflicts, we have
|
||
|
|
// the right rust-version file while resolving them.
|
||
|
|
fs::write("rust-version", format!("{new_upstream_base}\n"))
|
||
|
|
.expect("failed to write rust-version");
|
||
|
|
|
||
|
|
let prep_message = format!(
|
||
|
|
"Update the upstream Rust version\n\n\
|
||
|
|
To prepare for merging from {upstream_repo}, set the version file to:\n\n \
|
||
|
|
{new_summary}\n\
|
||
|
|
",
|
||
|
|
);
|
||
|
|
run([
|
||
|
|
"git",
|
||
|
|
"commit",
|
||
|
|
"rust-version",
|
||
|
|
"--no-verify",
|
||
|
|
"-m",
|
||
|
|
&prep_message,
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Fetch given rustc commit.
|
||
|
|
run(["git", "fetch", &josh_url_filtered]);
|
||
|
|
let incoming_ref = check_output(["git", "rev-parse", "FETCH_HEAD"]);
|
||
|
|
println!("incoming ref: {incoming_ref}");
|
||
|
|
|
||
|
|
let merge_message = format!(
|
||
|
|
"Merge ref '{upstream_head_short}{filter}' from {upstream_url}\n\n\
|
||
|
|
Pull recent changes from {upstream_repo} via Josh.\n\n\
|
||
|
|
Upstream ref: {new_upstream_base}\n\
|
||
|
|
Filtered ref: {incoming_ref}\n\
|
||
|
|
",
|
||
|
|
upstream_head_short = &new_upstream_base[..12],
|
||
|
|
filter = self.josh_filter
|
||
|
|
);
|
||
|
|
|
||
|
|
// This should not add any new root commits. So count those before and after merging.
|
||
|
|
let num_roots = || -> u32 {
|
||
|
|
let out = check_output(["git", "rev-list", "HEAD", "--max-parents=0", "--count"]);
|
||
|
|
out.trim()
|
||
|
|
.parse::<u32>()
|
||
|
|
.unwrap_or_else(|e| panic!("failed to parse `{out}`: {e}"))
|
||
|
|
};
|
||
|
|
let num_roots_before = num_roots();
|
||
|
|
|
||
|
|
let pre_merge_sha = check_output(["git", "rev-parse", "HEAD"]);
|
||
|
|
println!("pre-merge HEAD: {pre_merge_sha}");
|
||
|
|
|
||
|
|
// Merge the fetched commit.
|
||
|
|
run([
|
||
|
|
"git",
|
||
|
|
"merge",
|
||
|
|
"FETCH_HEAD",
|
||
|
|
"--no-verify",
|
||
|
|
"--no-ff",
|
||
|
|
"-m",
|
||
|
|
&merge_message,
|
||
|
|
]);
|
||
|
|
|
||
|
|
let current_sha = check_output(["git", "rev-parse", "HEAD"]);
|
||
|
|
if current_sha == pre_merge_sha {
|
||
|
|
run(["git", "reset", "--hard", &orig_head]);
|
||
|
|
eprintln!(
|
||
|
|
"No merge was performed, no changes to pull were found. \
|
||
|
|
Rolled back the preparation commit."
|
||
|
|
);
|
||
|
|
exit(1);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check that the number of roots did not increase.
|
||
|
|
assert_eq!(
|
||
|
|
num_roots(),
|
||
|
|
num_roots_before,
|
||
|
|
"Josh created a new root commit. This is probably not the history you want."
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Construct an update to rust-lang/rust from compiler-builtins.
|
||
|
|
pub fn rustc_push(&self, github_user: &str, branch: Option<&str>) {
|
||
|
|
let Self {
|
||
|
|
josh_filter,
|
||
|
|
upstream_url,
|
||
|
|
..
|
||
|
|
} = self;
|
||
|
|
|
||
|
|
let branch = branch.unwrap_or(DEFAULT_PR_BRANCH);
|
||
|
|
let josh_url = self.josh_url(&format!("{github_user}/rust"), None, Some(josh_filter));
|
||
|
|
let user_upstream_url = format!("git@github.com:{github_user}/rust.git");
|
||
|
|
|
||
|
|
let Ok(rustc_git) = env::var("RUSTC_GIT") else {
|
||
|
|
panic!("the RUSTC_GIT environment variable must be set to a rust-lang/rust checkout")
|
||
|
|
};
|
||
|
|
|
||
|
|
ensure_clean();
|
||
|
|
let base = fs::read_to_string("rust-version")
|
||
|
|
.expect("failed to read `rust-version`")
|
||
|
|
.trim()
|
||
|
|
.to_string();
|
||
|
|
|
||
|
|
// Make sure josh is running.
|
||
|
|
let _josh = Josh::start();
|
||
|
|
|
||
|
|
// Prepare the branch. Pushing works much better if we use as base exactly
|
||
|
|
// the commit that we pulled from last time, so we use the `rust-version`
|
||
|
|
// file to find out which commit that would be.
|
||
|
|
println!("Preparing {github_user}/rust (base: {base})...");
|
||
|
|
|
||
|
|
if Command::new("git")
|
||
|
|
.args(["-C", &rustc_git, "fetch", &user_upstream_url, branch])
|
||
|
|
.output() // capture output
|
||
|
|
.expect("could not run fetch")
|
||
|
|
.status
|
||
|
|
.success()
|
||
|
|
{
|
||
|
|
panic!(
|
||
|
|
"The branch '{branch}' seems to already exist in '{user_upstream_url}'. \
|
||
|
|
Please delete it and try again."
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
run(["git", "-C", &rustc_git, "fetch", upstream_url, &base]);
|
||
|
|
|
||
|
|
run_cfg("git", |c| {
|
||
|
|
c.args([
|
||
|
|
"-C",
|
||
|
|
&rustc_git,
|
||
|
|
"push",
|
||
|
|
&user_upstream_url,
|
||
|
|
&format!("{base}:refs/heads/{branch}"),
|
||
|
|
])
|
||
|
|
.stdout(Stdio::null())
|
||
|
|
.stderr(Stdio::null()) // silence the "create GitHub PR" message
|
||
|
|
});
|
||
|
|
println!("pushed PR branch");
|
||
|
|
|
||
|
|
// Do the actual push.
|
||
|
|
println!("Pushing changes...");
|
||
|
|
run(["git", "push", &josh_url, &format!("HEAD:{branch}")]);
|
||
|
|
println!();
|
||
|
|
|
||
|
|
// Do a round-trip check to make sure the push worked as expected.
|
||
|
|
run(["git", "fetch", &josh_url, branch]);
|
||
|
|
|
||
|
|
let head = check_output(["git", "rev-parse", "HEAD"]);
|
||
|
|
let fetch_head = check_output(["git", "rev-parse", "FETCH_HEAD"]);
|
||
|
|
assert_eq!(
|
||
|
|
head, fetch_head,
|
||
|
|
"Josh created a non-roundtrip push! Do NOT merge this into rustc!\n\
|
||
|
|
Expected {head}, got {fetch_head}."
|
||
|
|
);
|
||
|
|
println!(
|
||
|
|
"Confirmed that the push round-trips back to compiler-builtins properly. Please \
|
||
|
|
create a rustc PR:"
|
||
|
|
);
|
||
|
|
// Open PR with `subtree update` title to silence the `no-merges` triagebot check
|
||
|
|
println!(
|
||
|
|
" {upstream_url}/compare/{github_user}:{branch}?quick_pull=1\
|
||
|
|
&title=Update%20the%20%60compiler-builtins%60%20subtree\
|
||
|
|
&body=Update%20the%20Josh%20subtree%20to%20https%3A%2F%2Fgithub.com%2Frust-lang%2F\
|
||
|
|
compiler-builtins%2Fcommit%2F{head_short}.%0A%0Ar%3F%20%40ghost",
|
||
|
|
head_short = &head[..12],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Construct a url to the local Josh server with (optionally)
|
||
|
|
fn josh_url(&self, repo: &str, rev: Option<&str>, filter: Option<&str>) -> String {
|
||
|
|
format!(
|
||
|
|
"{base}/{repo}.git{at}{rev}{filter}{filt_git}",
|
||
|
|
base = self.josh_url_base,
|
||
|
|
at = if rev.is_some() { "@" } else { "" },
|
||
|
|
rev = rev.unwrap_or_default(),
|
||
|
|
filter = filter.unwrap_or_default(),
|
||
|
|
filt_git = if filter.is_some() { ".git" } else { "" }
|
||
|
|
)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Fail if there are files that need to be checked in.
|
||
|
|
fn ensure_clean() {
|
||
|
|
let read = check_output(["git", "status", "--untracked-files=no", "--porcelain"]);
|
||
|
|
assert!(
|
||
|
|
read.is_empty(),
|
||
|
|
"working directory must be clean before performing rustc pull"
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* Helpers for running commands with logged invocations */
|
||
|
|
|
||
|
|
/// Run a command from an array, passing its output through.
|
||
|
|
fn run<'a, Args: AsRef<[&'a str]>>(l: Args) {
|
||
|
|
let l = l.as_ref();
|
||
|
|
run_cfg(l[0], |c| c.args(&l[1..]));
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Run a command from an array, collecting its output.
|
||
|
|
fn check_output<'a, Args: AsRef<[&'a str]>>(l: Args) -> String {
|
||
|
|
let l = l.as_ref();
|
||
|
|
check_output_cfg(l[0], |c| c.args(&l[1..]))
|
||
|
|
}
|
||
|
|
|
||
|
|
/// [`run`] with configuration.
|
||
|
|
fn run_cfg(prog: &str, f: impl FnOnce(&mut Command) -> &mut Command) {
|
||
|
|
// self.read(l.as_ref());
|
||
|
|
check_output_cfg(prog, |c| f(c.stdout(Stdio::inherit())));
|
||
|
|
}
|
||
|
|
|
||
|
|
/// [`read`] with configuration. All shell helpers print the command and pass stderr.
|
||
|
|
fn check_output_cfg(prog: &str, f: impl FnOnce(&mut Command) -> &mut Command) -> String {
|
||
|
|
let mut cmd = Command::new(prog);
|
||
|
|
cmd.stderr(Stdio::inherit());
|
||
|
|
f(&mut cmd);
|
||
|
|
eprintln!("+ {cmd:?}");
|
||
|
|
let out = cmd.output().expect("command failed");
|
||
|
|
assert!(out.status.success());
|
||
|
|
String::from_utf8(out.stdout.trim_ascii().to_vec()).expect("non-UTF8 output")
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Create a wrapper that stops Josh on drop.
|
||
|
|
pub struct Josh(process::Child);
|
||
|
|
|
||
|
|
impl Josh {
|
||
|
|
pub fn start() -> Self {
|
||
|
|
// Determine cache directory.
|
||
|
|
let user_dirs =
|
||
|
|
directories::ProjectDirs::from("org", "rust-lang", "rustc-compiler-builtins-josh")
|
||
|
|
.unwrap();
|
||
|
|
let local_dir = user_dirs.cache_dir().to_owned();
|
||
|
|
|
||
|
|
// Start josh, silencing its output.
|
||
|
|
#[expect(clippy::zombie_processes, reason = "clippy can't handle the loop")]
|
||
|
|
let josh = process::Command::new("josh-proxy")
|
||
|
|
.arg("--local")
|
||
|
|
.arg(local_dir)
|
||
|
|
.args([
|
||
|
|
"--remote=https://github.com",
|
||
|
|
&format!("--port={JOSH_PORT}"),
|
||
|
|
"--no-background",
|
||
|
|
])
|
||
|
|
.stdout(Stdio::null())
|
||
|
|
.stderr(Stdio::null())
|
||
|
|
.spawn()
|
||
|
|
.expect("failed to start josh-proxy, make sure it is installed");
|
||
|
|
|
||
|
|
// Wait until the port is open. We try every 10ms until 1s passed.
|
||
|
|
for _ in 0..100 {
|
||
|
|
// This will generally fail immediately when the port is still closed.
|
||
|
|
let addr = SocketAddr::from(([127, 0, 0, 1], JOSH_PORT));
|
||
|
|
let josh_ready = TcpStream::connect_timeout(&addr, Duration::from_millis(1));
|
||
|
|
|
||
|
|
if josh_ready.is_ok() {
|
||
|
|
println!("josh up and running");
|
||
|
|
return Josh(josh);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Not ready yet.
|
||
|
|
thread::sleep(Duration::from_millis(10));
|
||
|
|
}
|
||
|
|
panic!("Even after waiting for 1s, josh-proxy is still not available.")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
impl Drop for Josh {
|
||
|
|
fn drop(&mut self) {
|
||
|
|
if cfg!(unix) {
|
||
|
|
// Try to gracefully shut it down.
|
||
|
|
Command::new("kill")
|
||
|
|
.args(["-s", "INT", &self.0.id().to_string()])
|
||
|
|
.output()
|
||
|
|
.expect("failed to SIGINT josh-proxy");
|
||
|
|
// Sadly there is no "wait with timeout"... so we just give it some time to finish.
|
||
|
|
thread::sleep(Duration::from_millis(100));
|
||
|
|
// Now hopefully it is gone.
|
||
|
|
if self
|
||
|
|
.0
|
||
|
|
.try_wait()
|
||
|
|
.expect("failed to wait for josh-proxy")
|
||
|
|
.is_some()
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// If that didn't work (or we're not on Unix), kill it hard.
|
||
|
|
eprintln!(
|
||
|
|
"I have to kill josh-proxy the hard way, let's hope this does not \
|
||
|
|
break anything."
|
||
|
|
);
|
||
|
|
self.0.kill().expect("failed to SIGKILL josh-proxy");
|
||
|
|
}
|
||
|
|
}
|