From b8196d6284b161bc647ae8d33730f8e30185f0b3 Mon Sep 17 00:00:00 2001 From: Filip Tibell Date: Sat, 20 Apr 2024 21:59:17 +0200 Subject: [PATCH] Organize everything neatly into files --- src/cli/build/base_exe.rs | 83 ++++++++++++++++++++++ src/cli/build/files.rs | 36 ++++++++++ src/cli/build/mod.rs | 142 +++----------------------------------- src/cli/build/result.rs | 22 ++++++ src/cli/build/target.rs | 38 +++++----- 5 files changed, 171 insertions(+), 150 deletions(-) create mode 100644 src/cli/build/base_exe.rs create mode 100644 src/cli/build/files.rs create mode 100644 src/cli/build/result.rs diff --git a/src/cli/build/base_exe.rs b/src/cli/build/base_exe.rs new file mode 100644 index 0000000..39dfd12 --- /dev/null +++ b/src/cli/build/base_exe.rs @@ -0,0 +1,83 @@ +use std::{ + io::{Cursor, Read}, + path::PathBuf, +}; + +use tokio::{fs, task}; + +use crate::standalone::metadata::CURRENT_EXE; + +use super::{ + files::write_executable_file_to, + result::{BuildError, BuildResult}, + target::{BuildTarget, CACHE_DIR}, +}; + +/// Discovers the path to the base executable to use for cross-compilation, and downloads it if necessary +pub async fn get_or_download_base_executable(target: BuildTarget) -> BuildResult { + // If the target matches the current system, just use the current executable + if target.is_current_system() { + return Ok(CURRENT_EXE.to_path_buf()); + } + + // If a cached target base executable doesn't exist, attempt to download it + if !target.cache_path().exists() { + return Ok(target.cache_path()); + } + + // The target is not cached, we must download it + println!("Requested target '{target}' does not exist in cache"); + let version = env!("CARGO_PKG_VERSION"); + let target_triple = format!("lune-{version}-{target}"); + + let release_url = format!( + "{base_url}/v{version}/{target_triple}.zip", + base_url = "https://github.com/lune-org/lune/releases/download", + ); + + // NOTE: This is not entirely accurate, but it is clearer for a user + println!("Downloading {target_triple}{}...", target.exe_suffix()); + + // Try to request to download the zip file from the target url, + // making sure transient errors are handled gracefully and + // with a different error message than "not found" + let response = reqwest::get(release_url).await?; + if !response.status().is_success() { + if response.status().as_u16() == 404 { + return Err(BuildError::ReleaseTargetNotFound(target)); + } + return Err(BuildError::Download( + response.error_for_status().unwrap_err(), + )); + } + + // Receive the full zip file + let zip_bytes = response.bytes().await?.to_vec(); + let zip_file = Cursor::new(zip_bytes); + + // Look for and extract the binary file from the zip file + // NOTE: We use spawn_blocking here since reading a zip + // archive is a somewhat slow / blocking operation + let binary_file_name = format!("lune{}", target.exe_suffix()); + let binary_file_handle = task::spawn_blocking(move || { + let mut archive = zip_next::ZipArchive::new(zip_file)?; + + let mut binary = Vec::new(); + archive + .by_name(&binary_file_name) + .or(Err(BuildError::ZippedBinaryNotFound(binary_file_name)))? + .read_to_end(&mut binary)?; + + Ok::<_, BuildError>(binary) + }); + let binary_file_contents = binary_file_handle.await??; + + // Finally, write the extracted binary to the cache + if !CACHE_DIR.exists() { + fs::create_dir_all(CACHE_DIR.as_path()).await?; + } + write_executable_file_to(target.cache_path(), binary_file_contents).await?; + println!("Downloaded successfully and added to cache"); + + Ok(target.cache_path()) +} diff --git a/src/cli/build/files.rs b/src/cli/build/files.rs new file mode 100644 index 0000000..f35af21 --- /dev/null +++ b/src/cli/build/files.rs @@ -0,0 +1,36 @@ +use std::path::{Path, PathBuf}; + +use anyhow::Result; +use tokio::{fs, io::AsyncWriteExt}; + +/// Removes the source file extension from the given path, if it has one +/// A source file extension is an extension such as `.lua` or `.luau` +pub fn remove_source_file_ext(path: &Path) -> PathBuf { + if path + .extension() + .is_some_and(|ext| matches!(ext.to_str(), Some("lua" | "luau"))) + { + path.with_extension("") + } else { + path.to_path_buf() + } +} + +/// Writes the given bytes to a file at the specified path, and makes sure it has permissions to be executed +pub async fn write_executable_file_to( + path: impl AsRef, + bytes: impl AsRef<[u8]>, +) -> Result<(), std::io::Error> { + let mut options = fs::OpenOptions::new(); + options.write(true).create(true).truncate(true); + + #[cfg(unix)] + { + options.mode(0o755); // Read & execute for all, write for owner + } + + let mut file = options.open(path).await?; + file.write_all(bytes.as_ref()).await?; + + Ok(()) +} diff --git a/src/cli/build/mod.rs b/src/cli/build/mod.rs index 6c03644..8079131 100644 --- a/src/cli/build/mod.rs +++ b/src/cli/build/mod.rs @@ -1,20 +1,20 @@ -use std::{ - io::{Cursor, Read}, - path::{Path, PathBuf}, - process::ExitCode, -}; +use std::{path::PathBuf, process::ExitCode}; use anyhow::{bail, Context, Result}; use clap::Parser; use console::style; -use thiserror::Error; -use tokio::{fs, io::AsyncWriteExt, task::spawn_blocking}; +use tokio::fs; -use crate::standalone::metadata::{Metadata, CURRENT_EXE}; +use crate::standalone::metadata::Metadata; +mod base_exe; +mod files; +mod result; mod target; -use self::target::{Target, CACHE_DIR}; +use self::base_exe::get_or_download_base_executable; +use self::files::{remove_source_file_ext, write_executable_file_to}; +use self::target::BuildTarget; /// Build a standalone executable #[derive(Debug, Clone, Parser)] @@ -30,13 +30,13 @@ pub struct BuildCommand { /// The target to compile for in the format `os-arch` - /// defaults to the os and arch of the current system #[clap(short, long)] - pub target: Option, + pub target: Option, } impl BuildCommand { pub async fn run(self) -> Result { // Derive target spec to use, or default to the current host system - let target = self.target.unwrap_or_else(Target::current_system); + let target = self.target.unwrap_or_else(BuildTarget::current_system); // Derive paths to use, and make sure the output path is // not the same as the input, so that we don't overwrite it @@ -79,123 +79,3 @@ impl BuildCommand { Ok(ExitCode::SUCCESS) } } - -/// Removes the source file extension from the given path, if it has one -/// A source file extension is an extension such as `.lua` or `.luau` -pub fn remove_source_file_ext(path: &Path) -> PathBuf { - if path - .extension() - .is_some_and(|ext| matches!(ext.to_str(), Some("lua" | "luau"))) - { - path.with_extension("") - } else { - path.to_path_buf() - } -} - -/// Writes the given bytes to a file at the specified path, and makes sure it has permissions to be executed -pub async fn write_executable_file_to( - path: impl AsRef, - bytes: impl AsRef<[u8]>, -) -> Result<(), std::io::Error> { - let mut options = fs::OpenOptions::new(); - options.write(true).create(true).truncate(true); - - #[cfg(unix)] - { - options.mode(0o755); // Read & execute for all, write for owner - } - - let mut file = options.open(path).await?; - file.write_all(bytes.as_ref()).await?; - - Ok(()) -} - -/// Errors that may occur when building a standalone binary -#[derive(Debug, Error)] -pub enum BuildError { - #[error("failed to find lune target '{0}' in GitHub release")] - ReleaseTargetNotFound(Target), - #[error("failed to find lune binary '{0}' in downloaded zip file")] - ZippedBinaryNotFound(String), - #[error("failed to download lune binary: {0}")] - Download(#[from] reqwest::Error), - #[error("failed to unzip lune binary: {0}")] - Unzip(#[from] zip_next::result::ZipError), - #[error("panicked while unzipping lune binary: {0}")] - UnzipJoin(#[from] tokio::task::JoinError), - #[error("io error: {0}")] - IoError(#[from] std::io::Error), -} - -pub type BuildResult = std::result::Result; - -/// Discovers the path to the base executable to use for cross-compilation, and downloads it if necessary -pub async fn get_or_download_base_executable(target: Target) -> BuildResult { - // If the target matches the current system, just use the current executable - if target.is_current_system() { - return Ok(CURRENT_EXE.to_path_buf()); - } - - // If a cached target base executable doesn't exist, attempt to download it - if !target.cache_path().exists() { - return Ok(target.cache_path()); - } - - // The target is not cached, we must download it - println!("Requested target '{target}' does not exist in cache"); - let version = env!("CARGO_PKG_VERSION"); - let target_triple = format!("lune-{version}-{target}"); - - let release_url = format!( - "{base_url}/v{version}/{target_triple}.zip", - base_url = "https://github.com/lune-org/lune/releases/download", - ); - - // NOTE: This is not entirely accurate, but it is clearer for a user - println!("Downloading {target_triple}{}...", target.exe_suffix()); - - // Try to request to download the zip file from the target url, - // making sure transient errors are handled gracefully and - // with a different error message than "not found" - let response = reqwest::get(release_url).await?; - if !response.status().is_success() { - if response.status().as_u16() == 404 { - return Err(BuildError::ReleaseTargetNotFound(target)); - } - return Err(BuildError::Download( - response.error_for_status().unwrap_err(), - )); - } - - // Receive the full zip file - let zip_bytes = response.bytes().await?.to_vec(); - let zip_file = Cursor::new(zip_bytes); - - // Look for and extract the binary file from the zip file - // NOTE: We use spawn_blocking here since reading a zip - // archive is a somewhat slow / blocking operation - let binary_file_name = format!("lune{}", target.exe_suffix()); - let binary_file_handle = spawn_blocking(move || { - let mut archive = zip_next::ZipArchive::new(zip_file)?; - - let mut binary = Vec::new(); - archive - .by_name(&binary_file_name) - .or(Err(BuildError::ZippedBinaryNotFound(binary_file_name)))? - .read_to_end(&mut binary)?; - - Ok::<_, BuildError>(binary) - }); - let binary_file_contents = binary_file_handle.await??; - - // Finally, write the extracted binary to the cache - if !CACHE_DIR.exists() { - fs::create_dir_all(CACHE_DIR.as_path()).await?; - } - write_executable_file_to(target.cache_path(), binary_file_contents).await?; - println!("Downloaded successfully and added to cache"); - - Ok(target.cache_path()) -} diff --git a/src/cli/build/result.rs b/src/cli/build/result.rs new file mode 100644 index 0000000..f987c9e --- /dev/null +++ b/src/cli/build/result.rs @@ -0,0 +1,22 @@ +use thiserror::Error; + +use super::target::BuildTarget; + +/// Errors that may occur when building a standalone binary +#[derive(Debug, Error)] +pub enum BuildError { + #[error("failed to find lune target '{0}' in GitHub release")] + ReleaseTargetNotFound(BuildTarget), + #[error("failed to find lune binary '{0}' in downloaded zip file")] + ZippedBinaryNotFound(String), + #[error("failed to download lune binary: {0}")] + Download(#[from] reqwest::Error), + #[error("failed to unzip lune binary: {0}")] + Unzip(#[from] zip_next::result::ZipError), + #[error("panicked while unzipping lune binary: {0}")] + UnzipJoin(#[from] tokio::task::JoinError), + #[error("io error: {0}")] + IoError(#[from] std::io::Error), +} + +pub type BuildResult = std::result::Result; diff --git a/src/cli/build/target.rs b/src/cli/build/target.rs index 993c5c7..0cbff94 100644 --- a/src/cli/build/target.rs +++ b/src/cli/build/target.rs @@ -14,13 +14,13 @@ pub const CACHE_DIR: Lazy = Lazy::new(|| HOME_DIR.join(".lune").join("t /// A target operating system supported by Lune #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TargetOS { +pub enum BuildTargetOS { Windows, Linux, MacOS, } -impl TargetOS { +impl BuildTargetOS { fn current_system() -> Self { match std::env::consts::OS { "windows" => Self::Windows, @@ -47,7 +47,7 @@ impl TargetOS { } } -impl fmt::Display for TargetOS { +impl fmt::Display for BuildTargetOS { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Windows => write!(f, "windows"), @@ -57,13 +57,13 @@ impl fmt::Display for TargetOS { } } -impl FromStr for TargetOS { +impl FromStr for BuildTargetOS { type Err = &'static str; fn from_str(s: &str) -> Result { match s.trim().to_ascii_lowercase().as_str() { - "windows" => Ok(Self::Windows), + "win" | "windows" => Ok(Self::Windows), "linux" => Ok(Self::Linux), - "macos" => Ok(Self::MacOS), + "mac" | "macos" | "darwin" => Ok(Self::MacOS), _ => Err("invalid target OS"), } } @@ -71,12 +71,12 @@ impl FromStr for TargetOS { /// A target architecture supported by Lune #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TargetArch { +pub enum BuildTargetArch { X86_64, Aarch64, } -impl TargetArch { +impl BuildTargetArch { fn current_system() -> Self { match ARCH { "x86_64" => Self::X86_64, @@ -86,7 +86,7 @@ impl TargetArch { } } -impl fmt::Display for TargetArch { +impl fmt::Display for BuildTargetArch { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::X86_64 => write!(f, "x86_64"), @@ -95,7 +95,7 @@ impl fmt::Display for TargetArch { } } -impl FromStr for TargetArch { +impl FromStr for BuildTargetArch { type Err = &'static str; fn from_str(s: &str) -> Result { match s.trim().to_ascii_lowercase().as_str() { @@ -108,21 +108,21 @@ impl FromStr for TargetArch { /// A full target description for cross-compilation (OS + Arch) #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Target { - pub os: TargetOS, - pub arch: TargetArch, +pub struct BuildTarget { + pub os: BuildTargetOS, + pub arch: BuildTargetArch, } -impl Target { +impl BuildTarget { pub fn current_system() -> Self { Self { - os: TargetOS::current_system(), - arch: TargetArch::current_system(), + os: BuildTargetOS::current_system(), + arch: BuildTargetArch::current_system(), } } pub fn is_current_system(&self) -> bool { - self.os == TargetOS::current_system() && self.arch == TargetArch::current_system() + self.os == BuildTargetOS::current_system() && self.arch == BuildTargetArch::current_system() } pub fn exe_extension(&self) -> &'static str { @@ -138,13 +138,13 @@ impl Target { } } -impl fmt::Display for Target { +impl fmt::Display for BuildTarget { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}-{}", self.os, self.arch) } } -impl FromStr for Target { +impl FromStr for BuildTarget { type Err = &'static str; fn from_str(s: &str) -> Result { let (left, right) = s