1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
|
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use crate::paths::ChromiumPaths;
use handlebars::handlebars_helper;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::process;
use std::{fmt::Write, path::PathBuf};
use anyhow::{format_err, Context, Result};
pub fn check_spawn(cmd: &mut process::Command, cmd_msg: &str) -> Result<process::Child> {
cmd.spawn().with_context(|| format!("failed to start {cmd_msg}"))
}
pub fn check_wait_with_output(child: process::Child, cmd_msg: &str) -> Result<process::Output> {
child.wait_with_output().with_context(|| format!("unexpected error while running {cmd_msg}"))
}
pub fn run_command(mut cmd: process::Command, cmd_msg: &str, stdin: Option<&[u8]>) -> Result<()> {
if stdin.is_some() {
cmd.stdin(std::process::Stdio::piped());
}
let mut child = check_spawn(&mut cmd, cmd_msg)?;
if let Some(stdin) = stdin {
use std::io::Write;
child.stdin.as_mut().unwrap().write_all(stdin)?;
}
let status = child.wait()?;
if !status.success() {
Err(format_err!("command '{}' failed: {}", cmd_msg, status))
} else {
Ok(())
}
}
pub fn check_exit_ok(output: &process::Output, cmd_msg: &str) -> Result<()> {
if output.status.success() {
Ok(())
} else {
let mut msg: String = format!("{cmd_msg} failed with ");
match output.status.code() {
Some(code) => write!(msg, "{code}.").unwrap(),
None => write!(msg, "no code.").unwrap(),
};
write!(msg, " stderr:\n\n{}", String::from_utf8_lossy(&output.stderr)).unwrap();
Err(format_err!(msg))
}
}
pub fn create_dirs_if_needed(path: &Path) -> Result<()> {
if path.is_dir() {
return Ok(());
}
if let Some(parent) = path.parent() {
create_dirs_if_needed(parent)?;
}
fs::create_dir(path)
.with_context(|| format_err!("Could not create directories for {}", path.to_string_lossy()))
}
/// Runs a function with the `.cargo/config.toml` file removed for the duration
/// of the function. This allows access to the online crates.io repository
/// instead of using our vendor/ directory as the source of truth. It should
/// only be done for actions like adding or updating crates.
pub fn without_cargo_config_toml<T>(
paths: &ChromiumPaths,
f: impl FnOnce() -> Result<T>,
) -> Result<T> {
let config_file = paths.third_party_cargo_root.join(".cargo").join("config.toml");
let config_contents =
std::fs::read_to_string(&config_file).context("reading .cargo/config.toml");
if config_contents.is_ok() {
std::fs::remove_file(&config_file)?;
}
let r = f();
if let Ok(contents) = config_contents {
std::fs::write(config_file, contents).context("writing .cargo/config.toml")?;
}
r
}
/// Run cargo metadata command, optionally with extra flags and environment.
pub fn run_cargo_metadata(
workspace_path: PathBuf,
mut extra_options: Vec<String>,
extra_env: HashMap<std::ffi::OsString, std::ffi::OsString>,
) -> Result<cargo_metadata::Metadata> {
let mut command = cargo_metadata::MetadataCommand::new();
command.current_dir(workspace_path);
// Allow the binary dependency on cxxbridge-cmd.
extra_options.push("-Zbindeps".to_string());
command.other_options(extra_options);
for (k, v) in extra_env.into_iter() {
command.env(k, v);
}
log::debug!("invoking cargo with:\n`{:?}`", command.cargo_command());
command.exec().context("running cargo metadata")
}
/// Run a cargo command, other than metadata which should use
/// `run_cargo_metadata`.
pub fn run_cargo_command(
workspace_path: PathBuf,
subcommand: &str,
extra_options: Vec<String>,
extra_env: HashMap<std::ffi::OsString, std::ffi::OsString>,
) -> Result<()> {
assert!(subcommand != "metadata");
let mut command = std::process::Command::new("cargo");
command.current_dir(workspace_path);
// Allow the binary dependency on cxxbridge-cmd.
command.arg("-Zbindeps");
command.arg(subcommand);
command.args(extra_options);
for (k, v) in extra_env.into_iter() {
command.env(k, v);
}
log::debug!("invoking cargo {}", subcommand);
let mut handle = command.spawn().with_context(|| format!("running cargo {}", subcommand))?;
let code = handle.wait().context("waiting for cargo process")?;
if !code.success() {
Err(format_err!("cargo {} exited with status {}", subcommand, code))
} else {
Ok(())
}
}
pub fn remove_checksums_from_lock(cargo_root: &Path) -> Result<()> {
let lock_file_path = cargo_root.join("Cargo.lock");
let lock_contents = std::fs::read_to_string(&lock_file_path)?
.lines()
.filter(|line| !line.starts_with("checksum = "))
.map(String::from)
// Add (back) the trailing newline.
.chain(std::iter::once(String::new()))
.collect::<Vec<_>>();
std::fs::write(&lock_file_path, lock_contents.join("\n"))?;
Ok(())
}
pub fn init_handlebars(template_path: &Path) -> Result<handlebars::Handlebars> {
let mut handlebars = handlebars::Handlebars::new();
// Don't escape output strings; the default is to escape for HTML output. Do
// not auto-escape for GN either, so that non-string GN may also be passed.
handlebars.register_escape_fn(handlebars::no_escape);
handlebars.register_template_file("template", template_path).context("loading gn template")?;
// Install helper to escape inputs pasted in GN `".."` strings.
handlebars_helper!(gn_escape: |x: String| escape_for_handlebars(&x));
handlebars.register_helper("gn_escape", Box::new(gn_escape));
Ok(handlebars)
}
fn escape_for_handlebars(x: &str) -> String {
let mut out = String::new();
for c in x.chars() {
match c {
// Note: we don't escape '$' here because we sometimes want to use
// $var syntax.
c @ ('"' | '\\') => write!(out, "\\{c}").unwrap(),
// GN strings can encode literal ASCII with "$0x<hex_code>" syntax,
// so we could embed newlines with "$0x0A". However, GN seems to
// escape these incorrectly in its Ninja output so we just replace
// it with a space.
'\n' => out.push(' '),
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_excaping() {
assert_eq!("foo bar", format!("{}", escape_for_handlebars("foo bar")));
assert_eq!("foo bar ", format!("{}", escape_for_handlebars("foo\nbar\n")));
assert_eq!(r#"foo \"bar\""#, format!("{}", escape_for_handlebars(r#"foo "bar""#)));
assert_eq!("foo 'bar'", format!("{}", escape_for_handlebars("foo 'bar'")));
}
}
|