Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 10 additions & 7 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,37 @@ license = "Mozilla Public License 2.0"
repository = "https://github.com/tryandromeda/andromeda"
version = "0.1.0"


[workspace.dependencies]
andromeda-core = { path = "core" }
andromeda-runtime = { path = "runtime" , features = ["canvas", "crypto"] }
andromeda-runtime = { path = "runtime", features = ["canvas", "crypto"] }
anyhow = "1.0.98"
anymap = "0.12.1"
base64-simd = "0.8.0"
wgpu = { version = "25.0.2", features = ["wgsl", "webgpu"]}
clap = { version = "4.5.39", features = ["derive"] }
console = "0.15.8"
image = "0.25.5"
libsui = "0.10.0"
nova_vm = { git = "https://github.com/trynova/nova", rev="cd94cf0c6d9610a68adb9582b2d1d09a27c8d1da", features = ["typescript"] }
nova_vm = { git = "https://github.com/trynova/nova", rev = "76688c29a4d9f8ab1d9ad42ba79485366ca3ed75", features = [
"typescript"
] }
nu-ansi-term = "0.50.0"
owo-colors = "4.2.1"
oxc_ast = "0.72.0"
oxc_allocator = "0.72.0"
oxc_diagnostics = "0.72.0"
oxc-miette = { version = "2.3.0", features = ["fancy"] }
oxc_parser = "0.72.0"
oxc_semantic = "0.72.0"
oxc_span = "0.72.0"
rand = "0.9.1"
reedline = "0.40.0"

regex = "1.11.1"
ring = "0.17.8"
serde = { version = "1.0.130", features = ["derive"] }
thiserror = "2.0.12"
tokio = { version = "1.45.1", features = ["rt", "sync", "time"] }
url = { version = "2", features = ["serde", "expose_internals"] }
ring = "0.17.8"
rand = "0.9.1"
wgpu = { version = "25.0.2", features = ["wgsl", "webgpu"] }

[profile.release]
lto = true
5 changes: 5 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ oxc_allocator.workspace = true
oxc_parser.workspace = true
oxc_span.workspace = true
oxc_semantic.workspace = true
oxc-miette.workspace = true
oxc_diagnostics.workspace = true
owo-colors.workspace = true
thiserror.workspace = true
regex.workspace = true
anymap.workspace = true
tokio.workspace = true
libsui.workspace = true
177 changes: 152 additions & 25 deletions cli/src/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,46 +2,173 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.

use crate::error::{AndromedaError, Result, read_file_with_context};
use libsui::{Elf, Macho, PortableExecutable};
use std::error::Error;
use std::fs::File;
use std::{env::current_exe, path::Path};
use std::{env::current_exe, fs::File, path::Path};

pub static ANDROMEDA_JS_CODE_SECTION: &str = "ANDROMEDABINCODE";

pub fn compile(result_name: &Path, input_file: &Path) -> Result<(), Box<dyn Error>> {
let exe_path = current_exe()?;
let exe = std::fs::read(exe_path)?;
let js = std::fs::read(input_file)?;
let mut out = File::create(result_name)?;
#[allow(clippy::result_large_err)]
pub fn compile(result_name: &Path, input_file: &Path) -> Result<()> {
// Validate input file exists and is readable
if !input_file.exists() {
return Err(AndromedaError::file_not_found(
input_file.to_path_buf(),
std::io::Error::new(std::io::ErrorKind::NotFound, "Input file not found"),
));
}

// Validate we can read the input file
let js_content = read_file_with_context(input_file)?;

// Validate JS content is not empty
if js_content.trim().is_empty() {
return Err(AndromedaError::invalid_argument(
"input_file".to_string(),
"non-empty JavaScript/TypeScript file".to_string(),
"empty file".to_string(),
));
}

// Get current executable
let exe_path = current_exe().map_err(|e| {
AndromedaError::config_error(
"Failed to get current executable path".to_string(),
None,
Some(Box::new(e)),
)
})?;

let exe = std::fs::read(&exe_path)
.map_err(|e| AndromedaError::file_read_error(exe_path.clone(), e))?;

let js = js_content.into_bytes();

// Validate output directory exists or can be created
if let Some(parent) = result_name.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent).map_err(|e| {
AndromedaError::permission_denied(
format!("creating output directory {}", parent.display()),
Some(parent.to_path_buf()),
e,
)
})?;
}
}

let mut out = File::create(result_name).map_err(|e| {
AndromedaError::permission_denied(
format!("creating output file {}", result_name.display()),
Some(result_name.to_path_buf()),
e,
)
})?;

// TODO(lino-levan): Replace this with a flag in the CLI
let os = std::env::consts::OS;

if os == "macos" {
Macho::from(exe)?
.write_section(ANDROMEDA_JS_CODE_SECTION, js)?
.build_and_sign(&mut out)?;
} else if os == "linux" {
Elf::new(&exe).append(ANDROMEDA_JS_CODE_SECTION, &js, &mut out)?;
} else if os == "windows" {
PortableExecutable::from(&exe)?
.write_resource(ANDROMEDA_JS_CODE_SECTION, js)?
.build(&mut out)?;
} else {
return Err("Unsupported operating system".into());
match os {
"macos" => {
Macho::from(exe)
.map_err(|e| {
AndromedaError::compile_error(
"Failed to parse macOS executable".to_string(),
input_file.to_path_buf(),
result_name.to_path_buf(),
Some(Box::new(e)),
)
})?
.write_section(ANDROMEDA_JS_CODE_SECTION, js)
.map_err(|e| {
AndromedaError::compile_error(
"Failed to write JavaScript section to macOS executable".to_string(),
input_file.to_path_buf(),
result_name.to_path_buf(),
Some(Box::new(e)),
)
})?
.build_and_sign(&mut out)
.map_err(|e| {
AndromedaError::compile_error(
"Failed to build and sign macOS executable".to_string(),
input_file.to_path_buf(),
result_name.to_path_buf(),
Some(Box::new(e)),
)
})?;
}
"linux" => {
Elf::new(&exe)
.append(ANDROMEDA_JS_CODE_SECTION, &js, &mut out)
.map_err(|e| {
AndromedaError::compile_error(
"Failed to append JavaScript section to Linux executable".to_string(),
input_file.to_path_buf(),
result_name.to_path_buf(),
Some(Box::new(e)),
)
})?;
}
"windows" => {
PortableExecutable::from(&exe)
.map_err(|e| {
AndromedaError::compile_error(
"Failed to parse Windows executable".to_string(),
input_file.to_path_buf(),
result_name.to_path_buf(),
Some(Box::new(e)),
)
})?
.write_resource(ANDROMEDA_JS_CODE_SECTION, js)
.map_err(|e| {
AndromedaError::compile_error(
"Failed to write JavaScript resource to Windows executable".to_string(),
input_file.to_path_buf(),
result_name.to_path_buf(),
Some(Box::new(e)),
)
})?
.build(&mut out)
.map_err(|e| {
AndromedaError::compile_error(
"Failed to build Windows executable".to_string(),
input_file.to_path_buf(),
result_name.to_path_buf(),
Some(Box::new(e)),
)
})?;
}
_ => {
return Err(AndromedaError::unsupported_platform(os.to_string()));
}
}

// Make the binary executable on Unix-like systems
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
use std::fs::{metadata, set_permissions};
use std::os::unix::fs::PermissionsExt;

// Make the binary executable on Unix-like systems
if os == "macos" || os == "linux" {
let mut perms = metadata(result_name)?.permissions();
if matches!(os, "macos" | "linux") {
let metadata = metadata(result_name).map_err(|e| {
AndromedaError::permission_denied(
format!("reading permissions for {}", result_name.display()),
Some(result_name.to_path_buf()),
e,
)
})?;
let mut perms = metadata.permissions();
perms.set_mode(0o755); // rwxr-xr-x permissions
set_permissions(result_name, perms)?;
set_permissions(result_name, perms).map_err(|e| {
AndromedaError::permission_denied(
format!(
"setting executable permissions for {}",
result_name.display()
),
Some(result_name.to_path_buf()),
e,
)
})?;
}
}

Expand Down
Loading
Loading