1use std::path::{Path, PathBuf};
2use std::process::Command;
3use std::str::FromStr;
4use tempfile::{tempdir, TempDir};
5
6const OBNAM_URL: &str = "https://gitlab.com/obnam/obnam.git";
7
8#[derive(Debug, Clone)]
9pub enum WhichObnam {
10 Installed,
11 Built(String),
12}
13
14impl FromStr for WhichObnam {
15 type Err = ObnamBuilderError;
16
17 fn from_str(s: &str) -> Result<Self, Self::Err> {
18 match s {
19 "installed" => Ok(WhichObnam::Installed),
20 _ => Ok(WhichObnam::Built(s.to_string())),
21 }
22 }
23}
24
25pub fn which_obnam(s: &str) -> Result<WhichObnam, ObnamBuilderError> {
26 WhichObnam::from_str(s)
27}
28
29#[derive(Debug, thiserror::Error)]
30pub enum ObnamBuilderError {
31 #[error("don't understand which Obnam to use: {0}")]
32 Unknown(String),
33
34 #[error("failed to create temporary directory for building Obnam: {0}")]
35 TempDir(std::io::Error),
36
37 #[error("failed to run client binary {0}: {1}")]
38 Client(PathBuf, std::io::Error),
39
40 #[error("failed to run git: {0}")]
41 Git(std::io::Error),
42
43 #[error("failed to run cargo: {0}")]
44 Cargo(std::io::Error),
45}
46
47pub struct ObnamBuilder {
48 #[allow(dead_code)]
49 tempdir: TempDir,
50 client: PathBuf,
51 server: PathBuf,
52 commit: Option<String>,
53}
54
55impl ObnamBuilder {
56 pub fn new(which: &WhichObnam) -> Result<Self, ObnamBuilderError> {
57 let tempdir = tempdir().map_err(ObnamBuilderError::TempDir)?;
58 let builder = match which {
59 WhichObnam::Installed => Self {
60 tempdir,
61 client: Path::new("/usr/bin/obnam").to_path_buf(),
62 server: Path::new("/usr/bin/obnam-server").to_path_buf(),
63 commit: None,
64 },
65 WhichObnam::Built(commit) => {
66 let (commit, bin) = build_obnam(tempdir.path(), commit)?;
67 let client = bin.join("obnam");
68 let server = bin.join("obnam-server");
69 assert!(client.exists());
70 assert!(server.exists());
71 Self {
72 tempdir,
73 client,
74 server,
75 commit: Some(commit),
76 }
77 }
78 };
79 Ok(builder)
80 }
81
82 pub fn version(&self) -> Result<String, ObnamBuilderError> {
83 let binary = self.client_binary();
84 let output = Command::new(binary)
85 .arg("--version")
86 .output()
87 .map_err(|err| ObnamBuilderError::Client(binary.to_path_buf(), err))?;
88 if output.status.code() != Some(0) {
89 eprintln!("{}", String::from_utf8_lossy(&output.stdout));
90 eprintln!("{}", String::from_utf8_lossy(&output.stderr));
91 std::process::exit(1);
92 }
93
94 let v = String::from_utf8_lossy(&output.stdout);
95 let v = v.strip_suffix('\n').or(Some(&v)).unwrap().to_string();
96 Ok(v)
97 }
98
99 pub fn commit(&self) -> Option<String> {
100 self.commit.clone()
101 }
102
103 pub fn client_binary(&self) -> &Path {
104 &self.client
105 }
106
107 pub fn server_binary(&self) -> &Path {
108 &self.server
109 }
110}
111
112fn build_obnam(dir: &Path, commit: &str) -> Result<(String, PathBuf), ObnamBuilderError> {
113 let src = dir.join("git");
114 let bin = dir.join("bin");
115 git_clone(OBNAM_URL, &src)?;
116 git_create_branch(&src, "build", commit)?;
117 let commit = git_resolve(&src, commit)?;
118 cargo_build(&src)?;
119 cargo_install(&src, dir)?;
120 Ok((commit, bin))
121}
122
123fn git_clone(url: &str, dir: &Path) -> Result<(), ObnamBuilderError> {
124 eprintln!("cloning {} to {}", url, dir.display());
125 run(
126 "git",
127 &["clone", url, &dir.display().to_string()],
128 Path::new("."),
129 )
130 .map_err(ObnamBuilderError::Git)
131 .map(|_| ())
132}
133
134fn git_create_branch(dir: &Path, branch: &str, commit: &str) -> Result<(), ObnamBuilderError> {
135 eprintln!("checking out {}", commit);
136 run("git", &["checkout", "-b", branch, commit], dir)
137 .map_err(ObnamBuilderError::Git)
138 .map(|_| ())
139}
140
141fn git_resolve(dir: &Path, commit: &str) -> Result<String, ObnamBuilderError> {
142 run("git", &["rev-parse", commit], dir)
143 .map(|s| s.strip_suffix('\n').or(Some("")).unwrap().to_string())
144 .map_err(ObnamBuilderError::Git)
145}
146
147fn cargo_build(dir: &Path) -> Result<(), ObnamBuilderError> {
148 eprintln!("building in {}", dir.display());
149 run("cargo", &["build", "--release"], dir)
150 .map_err(ObnamBuilderError::Git)
151 .map(|_| ())
152}
153
154fn cargo_install(src: &Path, bin: &Path) -> Result<(), ObnamBuilderError> {
155 eprintln!("install to {}", bin.display());
156 run(
157 "cargo",
158 &["install", "--path=.", "--root", &bin.display().to_string()],
159 src,
160 )
161 .map_err(ObnamBuilderError::Git)
162 .map(|_| ())
163}
164
165fn run(cmd: &str, args: &[&str], cwd: &Path) -> Result<String, std::io::Error> {
166 let output = Command::new(cmd).args(args).current_dir(cwd).output()?;
167
168 if output.status.code() != Some(0) {
169 eprintln!("{}", String::from_utf8_lossy(&output.stdout));
170 eprintln!("{}", String::from_utf8_lossy(&output.stderr));
171 std::process::exit(1);
172 }
173
174 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
175 Ok(stdout)
176}