1use std::{
5 collections::HashMap,
6 ffi::OsStr,
7 fs::File,
8 io::{self, Seek},
9 path::{Path, PathBuf},
10 process::Command,
11};
12
13use anyhow::{anyhow, bail, ensure};
14use colored::Colorize;
15use move_binary_format::CompiledModule;
16use move_bytecode_source_map::utils::source_map_from_file;
17use move_command_line_common::{
18 env::MOVE_HOME,
19 files::{
20 DEBUG_INFO_EXTENSION, MOVE_COMPILED_EXTENSION, MOVE_EXTENSION, extension_equals,
21 find_filenames,
22 },
23};
24use move_compiler::{
25 compiled_unit::NamedCompiledModule,
26 editions::{Edition, Flavor},
27 shared::{NumericalAddress, files::FileName},
28};
29use move_package::{
30 compilation::{
31 compiled_package::CompiledUnitWithSource, package_layout::CompiledPackageLayout,
32 },
33 lock_file::schema::{Header, ToolchainVersion},
34 source_package::{layout::SourcePackageLayout, parsed_manifest::PackageName},
35};
36use move_symbol_pool::Symbol;
37use tar::Archive;
38use tempfile::TempDir;
39use tracing::{debug, info};
40
41pub(crate) const CURRENT_COMPILER_VERSION: &str = env!("CARGO_PKG_VERSION");
42const LEGACY_COMPILER_VERSION: &str = CURRENT_COMPILER_VERSION; const PRE_TOOLCHAIN_MOVE_LOCK_VERSION: u16 = 0; const CANONICAL_UNIX_BINARY_NAME: &str = "sui";
45const CANONICAL_WIN_BINARY_NAME: &str = "sui.exe";
46
47pub(crate) fn current_toolchain() -> ToolchainVersion {
48 ToolchainVersion {
49 compiler_version: CURRENT_COMPILER_VERSION.into(),
50 edition: Edition::LEGACY, flavor: Flavor::Sui, }
53}
54
55pub(crate) fn legacy_toolchain() -> ToolchainVersion {
56 ToolchainVersion {
57 compiler_version: LEGACY_COMPILER_VERSION.into(),
58 edition: Edition::LEGACY,
59 flavor: Flavor::Sui,
60 }
61}
62
63pub(crate) fn units_for_toolchain(
68 compiled_units: &Vec<(PackageName, CompiledUnitWithSource)>,
69) -> anyhow::Result<Vec<(PackageName, CompiledUnitWithSource)>> {
70 if std::env::var("SUI_RUN_TOOLCHAIN_BUILD").is_err() {
71 return Ok(compiled_units.clone());
72 }
73 let mut package_version_map: HashMap<Symbol, (ToolchainVersion, Vec<CompiledUnitWithSource>)> =
74 HashMap::new();
75 for (package, local_unit) in compiled_units {
77 if let Some((_, units)) = package_version_map.get_mut(package) {
78 units.push(local_unit.clone());
80 continue;
81 }
82
83 if sui_types::is_system_package(local_unit.unit.address.into_inner()) {
84 package_version_map.insert(*package, (current_toolchain(), vec![local_unit.clone()]));
86 continue;
87 }
88
89 let package_root = SourcePackageLayout::try_find_root(&local_unit.source_path)?;
90 let lock_file = package_root.join(SourcePackageLayout::Lock.path());
91 if !lock_file.exists() {
92 package_version_map.insert(*package, (current_toolchain(), vec![local_unit.clone()]));
94 continue;
95 }
96
97 let mut lock_file = File::open(lock_file)?;
98 let lock_version = Header::read(&mut lock_file)?.version;
99 if lock_version == PRE_TOOLCHAIN_MOVE_LOCK_VERSION {
100 debug!("{package} on legacy compiler",);
102 package_version_map.insert(*package, (legacy_toolchain(), vec![local_unit.clone()]));
103 continue;
104 }
105
106 lock_file.rewind()?;
108 let toolchain_version = ToolchainVersion::read(&mut lock_file)?;
109 match toolchain_version {
110 None => {
112 debug!("{package} on current compiler @ {CURRENT_COMPILER_VERSION}",);
113 package_version_map
114 .insert(*package, (current_toolchain(), vec![local_unit.clone()]));
115 }
116 Some(ToolchainVersion {
118 compiler_version, ..
119 }) if compiler_version == CURRENT_COMPILER_VERSION => {
120 debug!("{package} on current compiler @ {CURRENT_COMPILER_VERSION}",);
121 package_version_map
122 .insert(*package, (current_toolchain(), vec![local_unit.clone()]));
123 }
124 Some(toolchain_version) => {
126 println!(
127 "{} {package} compiler @ {}",
128 "REQUIRE".bold().green(),
129 toolchain_version.compiler_version.yellow(),
130 );
131 package_version_map.insert(*package, (toolchain_version, vec![local_unit.clone()]));
132 }
133 }
134 }
135
136 let mut units = vec![];
137 for (package, (toolchain_version, local_units)) in package_version_map {
139 if toolchain_version.compiler_version == CURRENT_COMPILER_VERSION {
140 let local_units: Vec<_> = local_units.iter().map(|u| (package, u.clone())).collect();
141 units.extend(local_units);
142 continue;
143 }
144
145 if local_units.is_empty() {
146 bail!("Expected one or more modules, but none found");
147 }
148 let package_root = SourcePackageLayout::try_find_root(&local_units[0].source_path)?;
149 let install_dir = tempfile::tempdir()?; download_and_compile(
151 package_root.clone(),
152 &install_dir,
153 &toolchain_version,
154 &package,
155 )?;
156
157 let compiled_unit_paths = vec![package_root.clone()];
158 let compiled_units = find_filenames(&compiled_unit_paths, |path| {
159 extension_equals(path, MOVE_COMPILED_EXTENSION)
160 })?;
161 let build_path = install_dir
162 .path()
163 .join(CompiledPackageLayout::path(&CompiledPackageLayout::Root))
164 .join(package.as_str());
165 debug!("build path is {}", build_path.display());
166
167 for bytecode_path in compiled_units {
169 info!("bytecode path {bytecode_path}, {package}");
170 let local_unit = decode_bytecode_file(build_path.clone(), &package, &bytecode_path)?;
171 units.push((package, local_unit))
172 }
173 }
174 Ok(units)
175}
176
177fn download_and_compile(
178 root: PathBuf,
179 install_dir: &TempDir,
180 ToolchainVersion {
181 compiler_version,
182 edition,
183 flavor,
184 }: &ToolchainVersion,
185 dep_name: &Symbol,
186) -> anyhow::Result<()> {
187 let dest_dir = PathBuf::from_iter([&*MOVE_HOME, "binaries"]); let dest_version = dest_dir.join(compiler_version);
189 let mut dest_canonical_path = dest_version.clone();
190 dest_canonical_path.extend(["target", "release"]);
191 let mut dest_canonical_binary = dest_canonical_path.clone();
192
193 let platform = detect_platform(&root, compiler_version, &dest_canonical_path)?;
194 if platform == "windows-x86_64" {
195 dest_canonical_binary.push(CANONICAL_WIN_BINARY_NAME);
196 } else {
197 dest_canonical_binary.push(CANONICAL_UNIX_BINARY_NAME);
198 }
199
200 if !dest_canonical_binary.exists() {
201 let mainnet_url = format!(
204 "https://github.com/MystenLabs/sui/releases/download/mainnet-v{compiler_version}/sui-mainnet-v{compiler_version}-{platform}.tgz",
205 );
206
207 println!(
208 "{} mainnet compiler @ {} (this may take a while)",
209 "DOWNLOADING".bold().green(),
210 compiler_version.yellow()
211 );
212
213 let mut response = match ureq::get(&mainnet_url).call() {
214 Ok(response) => response,
215 Err(ureq::Error::Status(404, _)) => {
216 println!(
217 "{} sui mainnet compiler {} not available, attempting to download testnet compiler release...",
218 "WARNING".bold().yellow(),
219 compiler_version.yellow()
220 );
221 println!(
222 "{} testnet compiler @ {} (this may take a while)",
223 "DOWNLOADING".bold().green(),
224 compiler_version.yellow()
225 );
226 let testnet_url = format!("https://github.com/MystenLabs/sui/releases/download/testnet-v{compiler_version}/sui-testnet-v{compiler_version}-{platform}.tgz");
227 ureq::get(&testnet_url).call()?
228 }
229 Err(e) => return Err(e.into()),
230 }.into_reader();
231
232 let dest_tarball = dest_version.join(format!("{}.tgz", compiler_version));
233 debug!("tarball destination: {} ", dest_tarball.display());
234 if let Some(parent) = dest_tarball.parent() {
235 std::fs::create_dir_all(parent)
236 .map_err(|e| anyhow!("failed to create directory for tarball: {e}"))?;
237 }
238 let mut dest_file = File::create(&dest_tarball)?;
239 io::copy(&mut response, &mut dest_file)?;
240
241 let tar_gz = File::open(&dest_tarball)?;
243 let tar = flate2::read::GzDecoder::new(tar_gz);
244 let mut archive = Archive::new(tar);
245 archive
246 .unpack(&dest_version)
247 .map_err(|e| anyhow!("failed to untar compiler binary: {e}"))?;
248
249 let mut dest_binary = dest_version.clone();
250 dest_binary.extend(["target", "release"]);
251 if platform == "windows-x86_64" {
252 dest_binary.push(format!("sui-{platform}.exe"));
253 } else {
254 dest_binary.push(format!("sui-{platform}"));
255 }
256 let dest_binary_os = OsStr::new(dest_binary.as_path());
257 set_executable_permission(dest_binary_os)?;
258 std::fs::rename(dest_binary_os, dest_canonical_binary.clone())?;
259 }
260
261 debug!(
262 "{} move build --default-move-edition {} --default-move-flavor {} -p {} --install-dir {}",
263 dest_canonical_binary.display(),
264 edition.to_string().as_str(),
265 flavor.to_string().as_str(),
266 root.display(),
267 install_dir.path().display(),
268 );
269 info!(
270 "{} {} (compiler @ {})",
271 "BUILDING".bold().green(),
272 dep_name.as_str(),
273 compiler_version.yellow()
274 );
275 Command::new(dest_canonical_binary)
276 .args([
277 OsStr::new("move"),
278 OsStr::new("build"),
279 OsStr::new("--default-move-edition"),
280 OsStr::new(edition.to_string().as_str()),
281 OsStr::new("--default-move-flavor"),
282 OsStr::new(flavor.to_string().as_str()),
283 OsStr::new("-p"),
284 OsStr::new(root.as_path()),
285 OsStr::new("--install-dir"),
286 OsStr::new(install_dir.path()),
287 ])
288 .output()
289 .map_err(|e| {
290 anyhow!("failed to build package from compiler binary {compiler_version}: {e}",)
291 })?;
292 Ok(())
293}
294
295fn detect_platform(
296 package_path: &Path,
297 compiler_version: &String,
298 dest_dir: &Path,
299) -> anyhow::Result<String> {
300 let s = match (std::env::consts::OS, std::env::consts::ARCH) {
301 ("macos", "aarch64") => "macos-arm64",
302 ("macos", "x86_64") => "macos-x86_64",
303 ("linux", "x86_64") => "ubuntu-x86_64",
304 ("windows", "x86_64") => "windows-x86_64",
305 (os, arch) => {
306 let mut binary_name = CANONICAL_UNIX_BINARY_NAME;
307 if os == "windows" {
308 binary_name = CANONICAL_WIN_BINARY_NAME;
309 };
310 bail!(
311 "The package {} needs to be built with sui compiler version {compiler_version} but there \
312 is no binary release available to download for your platform:\n\
313 Operating System: {os}\n\
314 Architecture: {arch}\n\
315 You can manually put a {binary_name} binary for your platform in {} and rerun your command to continue.",
316 package_path.display(),
317 dest_dir.display(),
318 )
319 }
320 };
321 Ok(s.into())
322}
323
324#[cfg(unix)]
325fn set_executable_permission(path: &OsStr) -> anyhow::Result<()> {
326 use std::fs;
327 use std::os::unix::prelude::PermissionsExt;
328 let mut perms = fs::metadata(path)?.permissions();
329 perms.set_mode(0o755);
330 fs::set_permissions(path, perms)?;
331 Ok(())
332}
333
334#[cfg(not(unix))]
335fn set_executable_permission(path: &OsStr) -> anyhow::Result<()> {
336 Command::new("icacls")
337 .args([path, OsStr::new("/grant"), OsStr::new("Everyone:(RX)")])
338 .status()?;
339 Ok(())
340}
341
342fn decode_bytecode_file(
343 root_path: PathBuf,
344 package_name: &Symbol,
345 bytecode_path_str: &str,
346) -> anyhow::Result<CompiledUnitWithSource> {
347 let package_name_opt = Some(*package_name);
348 let bytecode_path = Path::new(bytecode_path_str);
349 let path_to_file = CompiledPackageLayout::path_to_file_after_category(bytecode_path);
350 let bytecode_bytes = std::fs::read(bytecode_path)?;
351 let source_map = source_map_from_file(
352 &root_path
353 .join(CompiledPackageLayout::DebugInfo.path())
354 .join(&path_to_file)
355 .with_extension(DEBUG_INFO_EXTENSION),
356 )?;
357 let source_path = &root_path
358 .join(CompiledPackageLayout::Sources.path())
359 .join(path_to_file)
360 .with_extension(MOVE_EXTENSION);
361 ensure!(
362 source_path.is_file(),
363 "Error decoding package: Unable to find corresponding source file for '{bytecode_path_str}' in package {package_name}"
364 );
365 let module = CompiledModule::deserialize_with_defaults(&bytecode_bytes)?;
366 let (address_bytes, module_name) = {
367 let id = module.self_id();
368 let parsed_addr = NumericalAddress::new(
369 id.address().into_bytes(),
370 move_compiler::shared::NumberFormat::Hex,
371 );
372 let module_name = FileName::from(id.name().as_str());
373 (parsed_addr, module_name)
374 };
375 let unit = NamedCompiledModule {
376 package_name: package_name_opt,
377 address: address_bytes,
378 name: module_name,
379 module,
380 source_map,
381 address_name: None,
382 };
383 Ok(CompiledUnitWithSource {
384 unit,
385 source_path: source_path.clone(),
386 })
387}