near_sandbox_utils/
lib.rs

1use anyhow::{anyhow, Context};
2use binary_install::Cache;
3use fs4::FileExt;
4use tokio::process::{Child, Command};
5
6use std::fs::File;
7use std::path::{Path, PathBuf};
8
9pub mod sync;
10
11// The current version of the sandbox node we want to point to.
12// Should be updated to the latest release of nearcore.
13// Currently pointing to nearcore@v2.9.0
14pub const DEFAULT_NEAR_SANDBOX_VERSION: &str = "2.9.0";
15
16const fn platform() -> Option<&'static str> {
17    #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
18    return Some("Linux-x86_64");
19
20    // Darwin-x86_64 is not supported for some time now.
21    #[cfg(all(target_os = "macos", target_arch = "x86_64"))]
22    return None;
23
24    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
25    return Some("Darwin-arm64");
26
27    #[cfg(all(
28        not(target_os = "macos"),
29        not(all(target_os = "linux", target_arch = "x86_64"))
30    ))]
31    return None;
32}
33
34fn local_addr(port: u16) -> String {
35    format!("0.0.0.0:{}", port)
36}
37
38// if the `SANDBOX_ARTIFACT_URL` env var is set, we short-circuit and use that.
39fn bin_url(version: &str) -> Option<String> {
40    if let Ok(val) = std::env::var("SANDBOX_ARTIFACT_URL") {
41        return Some(val);
42    }
43
44    Some(format!(
45        "https://s3-us-west-1.amazonaws.com/build.nearprotocol.com/nearcore/{}/{}/near-sandbox.tar.gz",
46        platform()?,
47        version,
48    ))
49}
50
51// Returns a path to the binary in the form of: `{home}/.near/near-sandbox-{version}` || `{$OUT_DIR}/.near/near-sandbox-{version}`
52fn download_path(version: &str) -> PathBuf {
53    #[cfg(feature = "global_install")]
54    let mut out = dirs_next::home_dir().expect("could not retrieve home_dir");
55    #[cfg(not(feature = "global_install"))]
56    let mut out = PathBuf::from(env!("OUT_DIR"));
57
58    out.push(".near");
59    out.push(format!("near-sandbox-{}", normalize_name(version)));
60    if !out.exists() {
61        std::fs::create_dir_all(&out).expect("could not create download path");
62    }
63
64    out
65}
66
67/// Returns a path to the binary in the form of {home}/.near/near-sandbox-{version}/near-sandbox
68pub fn bin_path(version: &str) -> anyhow::Result<PathBuf> {
69    if let Ok(path) = std::env::var("NEAR_SANDBOX_BIN_PATH") {
70        let path = PathBuf::from(path);
71        if !path.exists() {
72            anyhow::bail!("binary {} does not exist", path.display());
73        }
74        return Ok(path);
75    }
76
77    let mut buf = download_path(version);
78    buf.push("near-sandbox");
79
80    Ok(buf)
81}
82
83fn normalize_name(input: &str) -> String {
84    input.replace('/', "_")
85}
86
87/// Install the sandbox node given the version, which is either a commit hash or tagged version
88/// number from the nearcore project. Note that commits pushed to master within the latest 12h
89/// will likely not have the binaries made available quite yet.
90pub fn install_with_version(version: &str) -> anyhow::Result<PathBuf> {
91    if let Some(bin_path) = check_for_version(version)? {
92        return Ok(bin_path);
93    }
94
95    // Download binary into temp dir
96    let bin_name = format!("near-sandbox-{}", normalize_name(version));
97    let dl_cache = Cache::at(&download_path(version));
98    let bin_path = bin_url(version).ok_or_else(|| {
99        anyhow!("Unsupported platform: only linux-x86 and darwin-arm are supported")
100    })?;
101    let dl = dl_cache
102        .download(true, &bin_name, &["near-sandbox"], &bin_path)
103        .map_err(anyhow::Error::msg)
104        .with_context(|| "unable to download near-sandbox")?
105        .ok_or_else(|| anyhow!("Could not install near-sandbox"))?;
106
107    let path = dl.binary("near-sandbox").map_err(anyhow::Error::msg)?;
108
109    // Move near-sandbox binary to correct location from temp folder.
110    let dest = download_path(version).join("near-sandbox");
111    std::fs::rename(path, &dest)?;
112
113    Ok(dest)
114}
115
116/// Installs sandbox node with the default version. This is a version that is usually stable
117/// and has landed into mainnet to reflect the latest stable features and fixes.
118pub fn install() -> anyhow::Result<PathBuf> {
119    ensure_sandbox_bin_with_version(DEFAULT_NEAR_SANDBOX_VERSION)
120}
121
122fn installable(bin_path: &Path) -> anyhow::Result<Option<std::fs::File>> {
123    // Sandbox bin already exists
124    if bin_path.exists() {
125        return Ok(None);
126    }
127
128    let mut lockpath = bin_path.to_path_buf();
129    lockpath.set_extension("lock");
130
131    // Acquire the lockfile
132    let lockfile = File::create(lockpath)?;
133    lockfile.lock_exclusive()?;
134
135    // Check again after acquiring if no one has written to the dest path
136    if bin_path.exists() {
137        Ok(None)
138    } else {
139        Ok(Some(lockfile))
140    }
141}
142
143pub fn ensure_sandbox_bin() -> anyhow::Result<PathBuf> {
144    ensure_sandbox_bin_with_version(DEFAULT_NEAR_SANDBOX_VERSION)
145}
146
147pub fn run_with_options(options: &[&str]) -> anyhow::Result<Child> {
148    let bin_path = crate::ensure_sandbox_bin()?;
149    Command::new(&bin_path)
150        .args(options)
151        .envs(crate::log_vars())
152        .spawn()
153        .with_context(|| format!("failed to run sandbox using '{}'", bin_path.display()))
154}
155
156pub fn run(home_dir: impl AsRef<Path>, rpc_port: u16, network_port: u16) -> anyhow::Result<Child> {
157    #[allow(deprecated)]
158    run_with_version(
159        home_dir,
160        rpc_port,
161        network_port,
162        DEFAULT_NEAR_SANDBOX_VERSION,
163    )
164}
165
166pub fn init(home_dir: impl AsRef<Path>) -> anyhow::Result<Child> {
167    init_with_version(home_dir, DEFAULT_NEAR_SANDBOX_VERSION)
168}
169
170pub fn ensure_sandbox_bin_with_version(version: &str) -> anyhow::Result<PathBuf> {
171    let mut bin_path = bin_path(version)?;
172    if let Some(lockfile) = installable(&bin_path)? {
173        bin_path = install_with_version(version)?;
174        println!("Installed near-sandbox into {}", bin_path.to_str().unwrap());
175        std::env::set_var("NEAR_SANDBOX_BIN_PATH", bin_path.as_os_str());
176        FileExt::unlock(&lockfile).map_err(anyhow::Error::msg)?;
177    }
178
179    Ok(bin_path)
180}
181
182pub fn run_with_options_with_version(options: &[&str], version: &str) -> anyhow::Result<Child> {
183    let bin_path = ensure_sandbox_bin_with_version(version)?;
184    Command::new(&bin_path)
185        .args(options)
186        .envs(crate::log_vars())
187        .spawn()
188        .with_context(|| format!("failed to run sandbox using '{}'", bin_path.display()))
189}
190
191pub fn run_with_version(
192    home_dir: impl AsRef<Path>,
193    rpc_port: u16,
194    network_port: u16,
195    version: &str,
196) -> anyhow::Result<Child> {
197    let home_dir = home_dir.as_ref().to_str().unwrap();
198
199    run_with_options_with_version(
200        &[
201            "--home",
202            home_dir,
203            "run",
204            "--rpc-addr",
205            &local_addr(rpc_port),
206            "--network-addr",
207            &local_addr(network_port),
208        ],
209        version,
210    )
211}
212
213/// Initialize a sandbox node with the provided version and home directory.
214pub fn init_with_version(home_dir: impl AsRef<Path>, version: &str) -> anyhow::Result<Child> {
215    let bin_path = ensure_sandbox_bin_with_version(version)?;
216    let home_dir = home_dir.as_ref().to_str().unwrap();
217    Command::new(&bin_path)
218        .envs(log_vars())
219        .args(["--home", home_dir, "init", "--fast"])
220        .spawn()
221        .with_context(|| format!("failed to init sandbox using '{}'", bin_path.display()))
222}
223
224fn log_vars() -> Vec<(String, String)> {
225    let mut vars = Vec::new();
226    if let Ok(val) = std::env::var("NEAR_SANDBOX_LOG") {
227        vars.push(("RUST_LOG".into(), val));
228    }
229    if let Ok(val) = std::env::var("NEAR_SANDBOX_LOG_STYLE") {
230        vars.push(("RUST_LOG_STYLE".into(), val));
231    }
232    vars
233}
234
235/// Check if the sandbox version is already downloaded to the bin path.
236/// It does not disambiguate between a commit hash and a tagged version, so it's recommeded to
237/// pick one format and stick to it.
238fn check_for_version(version: &str) -> anyhow::Result<Option<PathBuf>> {
239    // short circuit if we are using the sandbox binary from the environment
240    if let Ok(bin_path) = &std::env::var("NEAR_SANDBOX_BIN_PATH") {
241        return Ok(Some(PathBuf::from(bin_path)));
242    }
243
244    // version saved under {home}/.near/near-sandbox-{version}/near-sandbox
245    let out_dir = download_path(version).join("near-sandbox");
246    if !out_dir.exists() {
247        return Ok(None);
248    }
249
250    Ok(Some(out_dir))
251}