mirror of
https://github.com/lune-org/lune.git
synced 2024-12-12 13:00:37 +00:00
Organize everything neatly into files
This commit is contained in:
parent
a11c1558ed
commit
b8196d6284
5 changed files with 171 additions and 150 deletions
83
src/cli/build/base_exe.rs
Normal file
83
src/cli/build/base_exe.rs
Normal file
|
@ -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<PathBuf> {
|
||||||
|
// 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())
|
||||||
|
}
|
36
src/cli/build/files.rs
Normal file
36
src/cli/build/files.rs
Normal file
|
@ -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<Path>,
|
||||||
|
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(())
|
||||||
|
}
|
|
@ -1,20 +1,20 @@
|
||||||
use std::{
|
use std::{path::PathBuf, process::ExitCode};
|
||||||
io::{Cursor, Read},
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
process::ExitCode,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use console::style;
|
use console::style;
|
||||||
use thiserror::Error;
|
use tokio::fs;
|
||||||
use tokio::{fs, io::AsyncWriteExt, task::spawn_blocking};
|
|
||||||
|
|
||||||
use crate::standalone::metadata::{Metadata, CURRENT_EXE};
|
use crate::standalone::metadata::Metadata;
|
||||||
|
|
||||||
|
mod base_exe;
|
||||||
|
mod files;
|
||||||
|
mod result;
|
||||||
mod target;
|
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
|
/// Build a standalone executable
|
||||||
#[derive(Debug, Clone, Parser)]
|
#[derive(Debug, Clone, Parser)]
|
||||||
|
@ -30,13 +30,13 @@ pub struct BuildCommand {
|
||||||
/// The target to compile for in the format `os-arch` -
|
/// The target to compile for in the format `os-arch` -
|
||||||
/// defaults to the os and arch of the current system
|
/// defaults to the os and arch of the current system
|
||||||
#[clap(short, long)]
|
#[clap(short, long)]
|
||||||
pub target: Option<Target>,
|
pub target: Option<BuildTarget>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BuildCommand {
|
impl BuildCommand {
|
||||||
pub async fn run(self) -> Result<ExitCode> {
|
pub async fn run(self) -> Result<ExitCode> {
|
||||||
// Derive target spec to use, or default to the current host system
|
// 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
|
// Derive paths to use, and make sure the output path is
|
||||||
// not the same as the input, so that we don't overwrite it
|
// not the same as the input, so that we don't overwrite it
|
||||||
|
@ -79,123 +79,3 @@ impl BuildCommand {
|
||||||
Ok(ExitCode::SUCCESS)
|
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<Path>,
|
|
||||||
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<T, E = BuildError> = std::result::Result<T, E>;
|
|
||||||
|
|
||||||
/// 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<PathBuf> {
|
|
||||||
// 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())
|
|
||||||
}
|
|
||||||
|
|
22
src/cli/build/result.rs
Normal file
22
src/cli/build/result.rs
Normal file
|
@ -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<T, E = BuildError> = std::result::Result<T, E>;
|
|
@ -14,13 +14,13 @@ pub const CACHE_DIR: Lazy<PathBuf> = Lazy::new(|| HOME_DIR.join(".lune").join("t
|
||||||
|
|
||||||
/// A target operating system supported by Lune
|
/// A target operating system supported by Lune
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum TargetOS {
|
pub enum BuildTargetOS {
|
||||||
Windows,
|
Windows,
|
||||||
Linux,
|
Linux,
|
||||||
MacOS,
|
MacOS,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TargetOS {
|
impl BuildTargetOS {
|
||||||
fn current_system() -> Self {
|
fn current_system() -> Self {
|
||||||
match std::env::consts::OS {
|
match std::env::consts::OS {
|
||||||
"windows" => Self::Windows,
|
"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 {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Windows => write!(f, "windows"),
|
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;
|
type Err = &'static str;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.trim().to_ascii_lowercase().as_str() {
|
match s.trim().to_ascii_lowercase().as_str() {
|
||||||
"windows" => Ok(Self::Windows),
|
"win" | "windows" => Ok(Self::Windows),
|
||||||
"linux" => Ok(Self::Linux),
|
"linux" => Ok(Self::Linux),
|
||||||
"macos" => Ok(Self::MacOS),
|
"mac" | "macos" | "darwin" => Ok(Self::MacOS),
|
||||||
_ => Err("invalid target OS"),
|
_ => Err("invalid target OS"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,12 +71,12 @@ impl FromStr for TargetOS {
|
||||||
|
|
||||||
/// A target architecture supported by Lune
|
/// A target architecture supported by Lune
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum TargetArch {
|
pub enum BuildTargetArch {
|
||||||
X86_64,
|
X86_64,
|
||||||
Aarch64,
|
Aarch64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TargetArch {
|
impl BuildTargetArch {
|
||||||
fn current_system() -> Self {
|
fn current_system() -> Self {
|
||||||
match ARCH {
|
match ARCH {
|
||||||
"x86_64" => Self::X86_64,
|
"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 {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::X86_64 => write!(f, "x86_64"),
|
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;
|
type Err = &'static str;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s.trim().to_ascii_lowercase().as_str() {
|
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)
|
/// A full target description for cross-compilation (OS + Arch)
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Target {
|
pub struct BuildTarget {
|
||||||
pub os: TargetOS,
|
pub os: BuildTargetOS,
|
||||||
pub arch: TargetArch,
|
pub arch: BuildTargetArch,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Target {
|
impl BuildTarget {
|
||||||
pub fn current_system() -> Self {
|
pub fn current_system() -> Self {
|
||||||
Self {
|
Self {
|
||||||
os: TargetOS::current_system(),
|
os: BuildTargetOS::current_system(),
|
||||||
arch: TargetArch::current_system(),
|
arch: BuildTargetArch::current_system(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_current_system(&self) -> bool {
|
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 {
|
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 {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
write!(f, "{}-{}", self.os, self.arch)
|
write!(f, "{}-{}", self.os, self.arch)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Target {
|
impl FromStr for BuildTarget {
|
||||||
type Err = &'static str;
|
type Err = &'static str;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
let (left, right) = s
|
let (left, right) = s
|
||||||
|
|
Loading…
Reference in a new issue