monerochan_cli/commands/
install_toolchain.rs

1use std::{
2    fs::{self},
3    process::Command,
4};
5
6use anyhow::Result;
7use clap::Parser;
8use dirs::home_dir;
9use rand::{distributions::Alphanumeric, Rng};
10use reqwest::Client;
11use monerochan::install::download_file;
12
13#[cfg(target_family = "unix")]
14use std::os::unix::fs::PermissionsExt;
15
16use crate::{
17    get_target, get_toolchain_download_url, is_supported_target, url_exists, RUSTUP_TOOLCHAIN_NAME,
18};
19
20#[derive(Parser)]
21#[command(name = "install-toolchain", about = "Install the cargo-monerochan toolchain.")]
22pub struct InstallToolchainCmd {
23    #[arg(short, long, env = "GITHUB_TOKEN")]
24    pub token: Option<String>,
25}
26
27impl InstallToolchainCmd {
28    pub fn run(&self) -> Result<()> {
29        // Check if rust is installed.
30        if Command::new("rustup")
31            .arg("--version")
32            .stdout(std::process::Stdio::null())
33            .stderr(std::process::Stdio::null())
34            .status()
35            .is_err()
36        {
37            return Err(anyhow::anyhow!(
38                "Rust is not installed. Please install Rust from https://rustup.rs/ and try again."
39            ));
40        }
41
42        // Setup client with optional token.
43        let client_builder = Client::builder().user_agent("Mozilla/5.0");
44        let client = if let Some(ref token) = self.token {
45            client_builder
46                .default_headers({
47                    let mut headers = reqwest::header::HeaderMap::new();
48                    headers.insert(
49                        reqwest::header::AUTHORIZATION,
50                        reqwest::header::HeaderValue::from_str(&format!("token {token}")).unwrap(),
51                    );
52                    headers
53                })
54                .build()?
55        } else {
56            client_builder.build()?
57        };
58
59        // Setup variables.
60        let root_dir = home_dir().unwrap().join(".monerochan");
61        match fs::read_dir(&root_dir) {
62            Ok(entries) =>
63            {
64                #[allow(clippy::manual_flatten)]
65                for entry in entries {
66                    if let Ok(entry) = entry {
67                        let entry_path = entry.path();
68                        let entry_name = entry_path.file_name().unwrap();
69                        if entry_path.is_dir() &&
70                            entry_name != "bin" &&
71                            entry_name != "circuits" &&
72                            entry_name != "toolchains"
73                        {
74                            if let Err(err) = fs::remove_dir_all(&entry_path) {
75                                println!("Failed to remove directory {entry_path:?}: {err}");
76                            }
77                        } else if entry_path.is_file() {
78                            if let Err(err) = fs::remove_file(&entry_path) {
79                                println!("Failed to remove file {entry_path:?}: {err}");
80                            }
81                        }
82                    }
83                }
84            }
85            Err(_) => println!("No existing ~/.monerochan directory to remove."),
86        }
87        println!("Successfully cleaned up ~/.monerochan directory.");
88        match fs::create_dir_all(&root_dir) {
89            Ok(_) => println!("Successfully created ~/.monerochan directory."),
90            Err(err) => println!("Failed to create ~/.monerochan directory: {err}"),
91        };
92
93        assert!(
94            is_supported_target(),
95            "Unsupported architecture. Please build the toolchain from source."
96        );
97        let target = get_target();
98        let toolchain_asset_name = format!("rust-toolchain-{target}.tar.gz");
99        let toolchain_archive_path = root_dir.join(toolchain_asset_name.clone());
100        let toolchain_dir = root_dir.join(&target);
101        let rt = tokio::runtime::Runtime::new()?;
102
103        let toolchain_download_url =
104            rt.block_on(get_toolchain_download_url(&client, target.to_string()));
105
106        let artifact_exists = rt.block_on(url_exists(&client, toolchain_download_url.as_str()));
107        if !artifact_exists {
108            return Err(anyhow::anyhow!(
109                "Unsupported architecture. Please build the toolchain from source."
110            ));
111        }
112
113        // Download the toolchain.
114        let mut file = fs::File::create(toolchain_archive_path)?;
115        rt.block_on(download_file(&client, toolchain_download_url.as_str(), &mut file)).unwrap();
116
117        // Remove the existing toolchain from rustup, if it exists.
118        let res = Command::new("rustup")
119            .current_dir(&root_dir)
120            .args(["toolchain", "remove", RUSTUP_TOOLCHAIN_NAME])
121            .stdout(std::process::Stdio::null())
122            .stderr(std::process::Stdio::null())
123            .status();
124        match res {
125            Ok(_) => println!("Successfully removed existing toolchain."),
126            Err(_) => println!("Failed to remove existing toolchain."),
127        }
128
129        // Unpack the toolchain.
130        fs::create_dir_all(toolchain_dir.clone())?;
131        Command::new("tar")
132            .current_dir(&root_dir)
133            .args(["-xzf", &toolchain_asset_name, "-C", &toolchain_dir.to_string_lossy()])
134            .status()?;
135
136        // Move the toolchain to a randomly named directory in the 'toolchains' folder
137        let toolchains_dir = root_dir.join("toolchains");
138        fs::create_dir_all(&toolchains_dir)?;
139        let random_string: String =
140            rand::thread_rng().sample_iter(&Alphanumeric).take(10).map(char::from).collect();
141        let new_toolchain_dir = toolchains_dir.join(random_string);
142        fs::rename(&toolchain_dir, &new_toolchain_dir)?;
143
144        // Link the new toolchain directory to rustup
145        Command::new("rustup")
146            .current_dir(&root_dir)
147            .args([
148                "toolchain",
149                "link",
150                RUSTUP_TOOLCHAIN_NAME,
151                &new_toolchain_dir.to_string_lossy(),
152            ])
153            .status()?;
154        println!("Successfully linked toolchain to rustup.");
155
156        // Ensure permissions.
157        let bin_dir = new_toolchain_dir.join("bin");
158        let rustlib_bin_dir = new_toolchain_dir.join(format!("lib/rustlib/{target}/bin"));
159        for entry in fs::read_dir(bin_dir)?.chain(fs::read_dir(rustlib_bin_dir)?) {
160            let entry = entry?;
161            if entry.path().is_file() {
162                let mut perms = entry.metadata()?.permissions();
163                perms.set_mode(0o755);
164                fs::set_permissions(entry.path(), perms)?;
165            }
166        }
167
168        Ok(())
169    }
170}