diff --git a/Cargo.lock b/Cargo.lock index 2a80d03..e16c2d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -227,9 +227,9 @@ dependencies = [ [[package]] name = "async-signal" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "794f185324c2f00e771cd9f1ae8b5ac68be2ca7abb129a87afd6e86d228bc54d" +checksum = "dfb3634b73397aa844481f814fad23bbf07fdb0eabec10f2eb95e58944b1ec32" dependencies = [ "async-io", "async-lock", @@ -370,9 +370,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" [[package]] name = "bzip2" @@ -406,13 +406,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.1.1" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907d8581360765417f8f2e0e7d602733bbed60156b4465b7617243689ef9b83d" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" dependencies = [ "jobserver", "libc", - "once_cell", ] [[package]] @@ -597,25 +596,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-deque" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.20" @@ -723,9 +703,9 @@ dependencies = [ [[package]] name = "deflate64" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d" +checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b" [[package]] name = "deranged" @@ -1152,6 +1132,7 @@ dependencies = [ "gix-utils", "gix-validate", "gix-worktree", + "gix-worktree-state", "once_cell", "parking_lot", "regex", @@ -1172,7 +1153,7 @@ dependencies = [ "itoa", "serde", "thiserror", - "winnow 0.6.13", + "winnow 0.6.14", ] [[package]] @@ -1256,7 +1237,7 @@ dependencies = [ "smallvec", "thiserror", "unicode-bom", - "winnow 0.6.13", + "winnow 0.6.14", ] [[package]] @@ -1529,7 +1510,7 @@ dependencies = [ "serde", "smallvec", "thiserror", - "winnow 0.6.13", + "winnow 0.6.14", ] [[package]] @@ -1600,9 +1581,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca987128ffb056d732bd545db5db3d8b103d252fbf083c2567bb0796876619a4" +checksum = "8d23d5bbda31344d8abc8de7c075b3cf26e5873feba7c4a15d916bce67382bd9" dependencies = [ "bstr", "gix-trace", @@ -1655,7 +1636,7 @@ dependencies = [ "maybe-async", "serde", "thiserror", - "winnow 0.6.13", + "winnow 0.6.14", ] [[package]] @@ -1689,7 +1670,7 @@ dependencies = [ "memmap2", "serde", "thiserror", - "winnow 0.6.13", + "winnow 0.6.14", ] [[package]] @@ -1878,16 +1859,23 @@ dependencies = [ ] [[package]] -name = "globset" -version = "0.4.14" +name = "gix-worktree-state" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1" +checksum = "e64b2835892ce553b15aef7f6f7bb1e39e146fdf71eb99609b86710a7786cf34" dependencies = [ - "aho-corasick", "bstr", - "log", - "regex-automata", - "regex-syntax", + "gix-features", + "gix-filter", + "gix-fs", + "gix-glob", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", + "gix-worktree", + "io-close", + "thiserror", ] [[package]] @@ -2119,22 +2107,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "ignore" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b46810df39e66e925525d6e38ce1e7f6e1d208f72dc39757880fcb66e2c58af1" -dependencies = [ - "crossbeam-deque", - "globset", - "log", - "memchr", - "regex-automata", - "same-file", - "walkdir", - "winapi-util", -] - [[package]] name = "indexmap" version = "1.9.3" @@ -2216,6 +2188,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "io-close" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cadcf447f06744f8ce713d2d6239bb5bde2c357a452397a9ed90c625da390bc" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "ipnet" version = "2.9.0" @@ -2284,9 +2266,9 @@ dependencies = [ [[package]] name = "keyring" -version = "3.0.1" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f32930aef50aff920e88e6cf66b70a6d587a6f2bae6aeade5c1e583661a40f8e" +checksum = "9961b98f55dc0b2737000132505bdafa249abab147ee9de43c50ae04a054aa6c" dependencies = [ "byteorder", "dbus-secret-service", @@ -2728,7 +2710,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.2", + "redox_syscall 0.5.3", "smallvec", "windows-targets 0.52.6", ] @@ -2774,7 +2756,6 @@ dependencies = [ "flate2", "full_moon", "gix", - "ignore", "indicatif", "indicatif-log-bridge", "inquire", @@ -2787,7 +2768,6 @@ dependencies = [ "pretty_env_logger", "relative-path", "reqwest", - "secrecy", "semver", "serde", "serde_json", @@ -2867,9 +2847,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" +checksum = "da544ee218f0d287a911e9c99a39a8c9bc8fcad3cb8db5959940044ecfc67265" [[package]] name = "powerfmt" @@ -3014,9 +2994,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags 2.6.0", ] @@ -3228,15 +3208,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "secrecy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" -dependencies = [ - "zeroize", -] - [[package]] name = "secret-service" version = "4.0.0" @@ -3258,9 +3229,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.6.0", "core-foundation", @@ -3271,9 +3242,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -3353,9 +3324,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.3" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e73139bc5ec2d45e6c5fd85be5a46949c1c39a4c18e56915f5eb4c12f975e377" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ "base64", "chrono", @@ -3371,9 +3342,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.8.3" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b80d3d6b56b64335c0180e5ffde23b3c5e08c14c585b51a15bd0e95393f46703" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" dependencies = [ "darling", "proc-macro2", @@ -3407,9 +3378,9 @@ dependencies = [ [[package]] name = "sha1_smol" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" [[package]] name = "sha2" @@ -3608,18 +3579,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", @@ -3695,9 +3666,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" dependencies = [ "backtrace", "bytes", @@ -3734,14 +3705,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.15", + "toml_edit 0.22.16", ] [[package]] @@ -3766,15 +3737,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.15" +version = "0.22.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59a3a72298453f564e2b111fa896f8d07fabb36f51f06d7e875fc5e0b5a3ef1" +checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.13", + "winnow 0.6.14", ] [[package]] @@ -4253,9 +4224,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "374ec40a2d767a3c1b4972d9475ecd557356637be906f2cb3f7fe17a6eb5e22f" dependencies = [ "memchr", ] @@ -4389,9 +4360,9 @@ dependencies = [ [[package]] name = "zip" -version = "2.1.3" +version = "2.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775a2b471036342aa69bc5a602bc889cb0a06cda00477d0c69566757d5553d39" +checksum = "b895748a3ebcb69b9d38dcfdf21760859a4b0d0b0015277640c2ef4c69640e6f" dependencies = [ "aes", "arbitrary", diff --git a/Cargo.toml b/Cargo.toml index dca31ef..88a3974 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://github.com/daimond113/pesde" include = ["src/**/*", "Cargo.toml", "Cargo.lock", "README.md", "LICENSE", "CHANGELOG.md"] [features] -bin = ["clap", "directories", "ignore", "pretty_env_logger", "reqwest/json", "reqwest/multipart", "indicatif", "indicatif-log-bridge", "inquire", "nondestructive", "colored", "anyhow", "keyring", "open"] +bin = ["clap", "directories", "pretty_env_logger", "reqwest/json", "reqwest/multipart", "indicatif", "indicatif-log-bridge", "inquire", "nondestructive", "colored", "anyhow", "keyring", "open", "gix/worktree-mutation"] wally-compat = ["toml", "zip"] roblox = [] lune = [] @@ -21,11 +21,14 @@ name = "pesde" path = "src/main.rs" required-features = ["bin"] +[lints.clippy] +uninlined_format_args = "warn" + [dependencies] serde = { version = "1.0.204", features = ["derive"] } serde_yaml = "0.9.34" serde_json = "1.0.120" -serde_with = "3.8.3" +serde_with = "3.9.0" gix = { version = "0.63.0", default-features = false, features = ["blocking-http-transport-reqwest-rust-tls", "revparse-regex", "credentials", "serde"] } semver = { version = "1.0.23", features = ["serde"] } reqwest = { version = "0.12.5", default-features = false, features = ["rustls-tls", "blocking"] } @@ -34,26 +37,26 @@ flate2 = "1.0.30" pathdiff = "0.2.1" relative-path = { version = "1.9.3", features = ["serde"] } log = "0.4.22" -thiserror = "1.0.62" +thiserror = "1.0.63" threadpool = "1.8.1" full_moon = { version = "1.0.0-rc.5", features = ["luau"] } url = { version = "2.5.2", features = ["serde"] } cfg-if = "1.0.0" once_cell = "1.19.0" -secrecy = "0.8.0" +# TODO: reevaluate whether to use this +# secrecy = "0.8.0" chrono = { version = "0.4.38", features = ["serde"] } -toml = { version = "0.8.14", optional = true } -zip = { version = "2.1.3", optional = true } +toml = { version = "0.8.15", optional = true } +zip = { version = "2.1.5", optional = true } anyhow = { version = "1.0.86", optional = true } open = { version = "5.3.0", optional = true } -keyring = { version = "3.0.1", features = ["crypto-rust", "windows-native", "apple-native", "linux-native"], optional = true } +keyring = { version = "3.0.3", features = ["crypto-rust", "windows-native", "apple-native", "linux-native"], optional = true } colored = { version = "2.1.0", optional = true } nondestructive = { version = "0.0.25", optional = true } clap = { version = "4.5.9", features = ["derive"], optional = true } directories = { version = "5.0.1", optional = true } -ignore = { version = "0.4.22", optional = true } pretty_env_logger = { version = "0.5.0", optional = true } indicatif = { version = "0.17.8", optional = true } indicatif-log-bridge = { version = "0.2.2", optional = true } diff --git a/src/cli/auth/login.rs b/src/cli/auth/login.rs index f0dd9b4..7c10e0b 100644 --- a/src/cli/auth/login.rs +++ b/src/cli/auth/login.rs @@ -15,6 +15,10 @@ pub struct LoginCommand { /// The index to use. Defaults to `default`, or the configured default index if current directory doesn't have a manifest #[arg(short, long)] index: Option, + + /// The token to use for authentication, skipping login + #[arg(short, long, conflicts_with = "index")] + token: Option, } #[derive(Debug, Deserialize)] @@ -44,7 +48,11 @@ enum AccessTokenResponse { } impl LoginCommand { - pub fn run(self, project: Project) -> anyhow::Result<()> { + pub fn authenticate_device_flow( + &self, + project: &Project, + reqwest: &reqwest::blocking::Client, + ) -> anyhow::Result { let manifest = match project.deser_manifest() { Ok(manifest) => Some(manifest), Err(e) => match e { @@ -82,18 +90,13 @@ impl LoginCommand { .try_into() .context("cannot parse URL to git URL")?, ); - source - .refresh(&project) - .context("failed to refresh index")?; - - dbg!(source.all_packages(&project).unwrap()); + source.refresh(project).context("failed to refresh index")?; let config = source - .config(&project) + .config(project) .context("failed to read index config")?; let client_id = config.github_oauth_client_id; - let reqwest = reqwest_client(project.data_dir())?; let response = reqwest .post(Url::parse_with_params( "https://github.com/login/device/code", @@ -150,13 +153,7 @@ impl LoginCommand { match response { AccessTokenResponse::Success { access_token } => { - set_token(project.data_dir(), Some(&access_token))?; - - println!( - "logged in as {}", - get_token_login(&reqwest, &access_token)?.bold() - ); - return Ok(()); + return Ok(access_token); } AccessTokenResponse::Error(e) => match e { AccessTokenError::AuthorizationPending => continue, @@ -178,4 +175,19 @@ impl LoginCommand { anyhow::bail!("code expired, please re-run the login command"); } + + pub fn run(self, project: Project) -> anyhow::Result<()> { + let reqwest = reqwest_client(project.data_dir())?; + + let token = match self.token { + Some(token) => token, + None => self.authenticate_device_flow(&project, &reqwest)?, + }; + + println!("logged in as {}", get_token_login(&reqwest, &token)?.bold()); + + set_token(project.data_dir(), Some(&token))?; + + Ok(()) + } } diff --git a/src/cli/config/default_index.rs b/src/cli/config/default_index.rs index 63e78ad..9b3483c 100644 --- a/src/cli/config/default_index.rs +++ b/src/cli/config/default_index.rs @@ -27,7 +27,7 @@ impl DefaultIndexCommand { Some(index) => { config.default_index = index.clone(); write_config(project.data_dir(), &config)?; - println!("default index set to: {}", index); + println!("default index set to: {index}"); } None => { println!("current default index: {}", config.default_index); diff --git a/src/cli/config/mod.rs b/src/cli/config/mod.rs index ff7ff24..4e4bba3 100644 --- a/src/cli/config/mod.rs +++ b/src/cli/config/mod.rs @@ -2,17 +2,22 @@ use clap::Subcommand; use pesde::Project; mod default_index; +mod scripts_repo; #[derive(Debug, Subcommand)] pub enum ConfigCommands { /// Configuration for the default index DefaultIndex(default_index::DefaultIndexCommand), + + /// Configuration for the scripts repository + ScriptsRepo(scripts_repo::ScriptsRepoCommand), } impl ConfigCommands { pub fn run(self, project: Project) -> anyhow::Result<()> { match self { ConfigCommands::DefaultIndex(default_index) => default_index.run(project), + ConfigCommands::ScriptsRepo(scripts_repo) => scripts_repo.run(project), } } } diff --git a/src/cli/config/scripts_repo.rs b/src/cli/config/scripts_repo.rs new file mode 100644 index 0000000..320cb83 --- /dev/null +++ b/src/cli/config/scripts_repo.rs @@ -0,0 +1,39 @@ +use crate::cli::{read_config, write_config, CliConfig}; +use clap::Args; +use pesde::Project; + +#[derive(Debug, Args)] +pub struct ScriptsRepoCommand { + /// The new repo URL to set as default, don't pass any value to check the current default repo + #[arg(index = 1)] + repo: Option, + + /// Resets the default repo to the default value + #[arg(short, long, conflicts_with = "repo")] + reset: bool, +} + +impl ScriptsRepoCommand { + pub fn run(self, project: Project) -> anyhow::Result<()> { + let mut config = read_config(project.data_dir())?; + + let repo = if self.reset { + Some(CliConfig::default().scripts_repo) + } else { + self.repo + }; + + match repo { + Some(repo) => { + config.scripts_repo = repo.clone(); + write_config(project.data_dir(), &config)?; + println!("scripts repo set to: {repo}"); + } + None => { + println!("current scripts repo: {}", config.scripts_repo); + } + } + + Ok(()) + } +} diff --git a/src/cli/init.rs b/src/cli/init.rs index 9485a42..2e5606a 100644 --- a/src/cli/init.rs +++ b/src/cli/init.rs @@ -1,13 +1,32 @@ use crate::cli::read_config; +use anyhow::Context; use clap::Args; use colored::Colorize; use inquire::validator::Validation; -use pesde::{errors::ManifestReadError, names::PackageName, Project, DEFAULT_INDEX_NAME}; -use std::str::FromStr; +use pesde::{ + errors::ManifestReadError, manifest::ScriptName, names::PackageName, Project, + DEFAULT_INDEX_NAME, +}; +use std::{path::Path, str::FromStr}; #[derive(Debug, Args)] pub struct InitCommand {} +fn script_contents(path: &Path) -> String { + format!( + concat!( + r#"local process = require("@lune/process") +local home_dir = if process.os == "windows" then process.env.userprofile else process.env.HOME + +require(home_dir .. ""#, + "/.", + env!("CARGO_PKG_NAME"), + r#"/scripts/{}")"#, + ), + path.display() + ) +} + impl InitCommand { pub fn run(self, project: Project) -> anyhow::Result<()> { match project.read_manifest() { @@ -94,6 +113,57 @@ impl InitCommand { mapping.insert_str("license", license); } + let target_env = inquire::Select::new( + "What environment are you targeting for your package?", + vec![ + #[cfg(feature = "roblox")] + "roblox", + #[cfg(feature = "lune")] + "lune", + #[cfg(feature = "luau")] + "luau", + ], + ) + .prompt() + .unwrap(); + + let mut target = mapping + .insert("target", nondestructive::yaml::Separator::Auto) + .make_mapping(); + target.insert_str("environment", target_env); + + if target_env == "roblox" + || inquire::Confirm::new(&format!( + "Would you like to setup a default {} script?", + ScriptName::RobloxSyncConfigGenerator + )) + .prompt() + .unwrap() + { + let folder = project.path().join(concat!(".", env!("CARGO_PKG_NAME"))); + std::fs::create_dir_all(&folder).context("failed to create scripts folder")?; + + std::fs::write( + folder.join(format!("{}.luau", ScriptName::RobloxSyncConfigGenerator)), + script_contents(Path::new(&format!( + "lune/rojo/{}.luau", + ScriptName::RobloxSyncConfigGenerator + ))), + ) + .context("failed to write script file")?; + + mapping + .insert("scripts", nondestructive::yaml::Separator::Auto) + .make_mapping() + .insert_str( + ScriptName::RobloxSyncConfigGenerator.to_string(), + format!( + concat!(concat!(".", env!("CARGO_PKG_NAME")), "/{}.luau"), + ScriptName::RobloxSyncConfigGenerator + ), + ); + } + let mut indices = mapping .insert("indices", nondestructive::yaml::Separator::Auto) .make_mapping(); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 0175bdb..a5f6fa5 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,18 +1,23 @@ +use crate::git::authenticate_conn; use anyhow::Context; +use gix::remote::Direction; use keyring::Entry; use pesde::Project; use serde::{Deserialize, Serialize}; -use std::path::Path; +use std::{collections::HashSet, path::Path}; mod auth; mod config; mod init; mod install; +mod publish; mod run; +mod self_install; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CliConfig { pub default_index: url::Url, + pub scripts_repo: url::Url, pub token: Option, } @@ -20,6 +25,9 @@ impl Default for CliConfig { fn default() -> Self { Self { default_index: "https://github.com/daimond113/pesde-index".parse().unwrap(), + scripts_repo: "https://github.com/daimond113/pesde-scripts" + .parse() + .unwrap(), token: None, } } @@ -102,7 +110,7 @@ pub fn reqwest_client(data_dir: &Path) -> anyhow::Result anyhow::Result anyhow::Result<()> { + let home_dir = directories::UserDirs::new() + .context("failed to get home directory")? + .home_dir() + .to_owned(); + + let scripts_dir = home_dir + .join(concat!(".", env!("CARGO_PKG_NAME"))) + .join("scripts"); + + if scripts_dir.exists() { + let repo = gix::open(&scripts_dir).context("failed to open scripts repository")?; + + let remote = repo + .find_default_remote(Direction::Fetch) + .context("missing default remote of scripts repository")? + .context("failed to find default remote of scripts repository")?; + + let mut connection = remote + .connect(Direction::Fetch) + .context("failed to connect to default remote of scripts repository")?; + + authenticate_conn(&mut connection, project.auth_config()); + + let results = connection + .prepare_fetch(gix::progress::Discard, Default::default()) + .context("failed to prepare scripts repository fetch")? + .receive(gix::progress::Discard, &false.into()) + .context("failed to receive new scripts repository contents")?; + + let remote_ref = results + .ref_map + .remote_refs + .first() + .context("failed to get remote refs of scripts repository")?; + + let unpacked = remote_ref.unpack(); + let oid = unpacked + .1 + .or(unpacked.2) + .context("couldn't find oid in remote ref")?; + + let tree = repo + .find_object(oid) + .context("failed to find scripts repository tree")? + .peel_to_tree() + .context("failed to peel scripts repository object to tree")?; + + let mut index = gix::index::File::from_state( + gix::index::State::from_tree(&tree.id, &repo.objects, Default::default()) + .context("failed to create index state from scripts repository tree")?, + repo.index_path(), + ); + + let opts = gix::worktree::state::checkout::Options { + overwrite_existing: true, + destination_is_initially_empty: false, + ..Default::default() + }; + + gix::worktree::state::checkout( + &mut index, + repo.work_dir().context("scripts repo is bare")?, + repo.objects + .clone() + .into_arc() + .context("failed to clone objects")?, + &gix::progress::Discard, + &gix::progress::Discard, + &false.into(), + opts, + ) + .context("failed to checkout scripts repository")?; + + index + .write(gix::index::write::Options::default()) + .context("failed to write index")?; + } else { + std::fs::create_dir_all(&scripts_dir).context("failed to create scripts directory")?; + + let cli_config = read_config(project.data_dir())?; + + gix::prepare_clone(cli_config.scripts_repo.as_str(), &scripts_dir) + .context("failed to prepare scripts repository clone")? + .fetch_then_checkout(gix::progress::Discard, &false.into()) + .context("failed to fetch and checkout scripts repository")? + .0 + .main_worktree(gix::progress::Discard, &false.into()) + .context("failed to set scripts repository as main worktree")?; + }; + + Ok(()) +} + +pub trait IsUpToDate { + fn is_up_to_date(&self) -> anyhow::Result; +} + +impl IsUpToDate for Project { + fn is_up_to_date(&self) -> anyhow::Result { + let manifest = self.deser_manifest()?; + let lockfile = match self.deser_lockfile() { + Ok(lockfile) => lockfile, + Err(pesde::errors::LockfileReadError::Io(e)) + if e.kind() == std::io::ErrorKind::NotFound => + { + return Ok(false); + } + Err(e) => return Err(e.into()), + }; + + if manifest.name != lockfile.name || manifest.version != lockfile.version { + return Ok(false); + } + + let specs = lockfile + .graph + .into_iter() + .flat_map(|(_, versions)| versions) + .filter_map(|(_, node)| match node.node.direct { + Some((_, spec)) => Some((spec, node.node.ty)), + None => None, + }) + .collect::>(); + + Ok(!manifest + .all_dependencies() + .context("failed to get all dependencies")? + .iter() + .all(|(_, (spec, ty))| specs.contains(&(spec.clone(), *ty)))) + } +} + #[derive(Debug, clap::Subcommand)] pub enum Subcommand { /// Authentication-related commands @@ -143,6 +284,12 @@ pub enum Subcommand { /// Installs all dependencies for the project Install(install::InstallCommand), + + /// Publishes the project to the registry + Publish(publish::PublishCommand), + + /// Installs the pesde binary and scripts + SelfInstall(self_install::SelfInstallCommand), } impl Subcommand { @@ -153,6 +300,8 @@ impl Subcommand { Subcommand::Init(init) => init.run(project), Subcommand::Run(run) => run.run(project), Subcommand::Install(install) => install.run(project), + Subcommand::Publish(publish) => publish.run(project), + Subcommand::SelfInstall(self_install) => self_install.run(project), } } } diff --git a/src/cli/publish.rs b/src/cli/publish.rs new file mode 100644 index 0000000..f7cd41d --- /dev/null +++ b/src/cli/publish.rs @@ -0,0 +1,289 @@ +use anyhow::Context; +use clap::Args; +use colored::Colorize; +use pesde::{manifest::Target, Project, MANIFEST_FILE_NAME, MAX_ARCHIVE_SIZE}; +use std::{io::Seek, path::Component}; + +#[derive(Debug, Args)] +pub struct PublishCommand { + /// Whether to output a tarball instead of publishing + #[arg(short, long)] + dry_run: bool, +} + +impl PublishCommand { + pub fn run(self, project: Project) -> anyhow::Result<()> { + let mut manifest = project + .deser_manifest() + .context("failed to read manifest")?; + + if manifest.private { + println!("{}", "package is private, cannot publish".red().bold()); + + return Ok(()); + } + + manifest + .target + .validate_publish() + .context("manifest not fit for publishing")?; + + let mut archive = tar::Builder::new(flate2::write::GzEncoder::new( + vec![], + flate2::Compression::best(), + )); + + let mut display_includes: Vec = vec![MANIFEST_FILE_NAME.to_string()]; + #[cfg(feature = "roblox")] + let mut display_build_files: Vec = vec![]; + + let (lib_path, bin_path) = ( + manifest.target.lib_path().cloned(), + manifest.target.bin_path().cloned(), + ); + + #[cfg(feature = "roblox")] + let mut roblox_target = match &mut manifest.target { + Target::Roblox { build_files, .. } => Some(build_files), + _ => None, + }; + #[cfg(not(feature = "roblox"))] + let roblox_target = None::<()>; + + if !manifest.includes.insert(MANIFEST_FILE_NAME.to_string()) { + display_includes.push(MANIFEST_FILE_NAME.to_string()); + + println!( + "{}: {MANIFEST_FILE_NAME} was not in includes, adding it", + "warn".yellow().bold() + ); + } + + for (name, path) in [("lib path", lib_path), ("bin path", bin_path)] { + let Some(export_path) = path else { continue }; + + let export_path = export_path.to_path(project.path()); + if !export_path.exists() { + anyhow::bail!("{name} points to non-existent file"); + } + + if !export_path.is_file() { + anyhow::bail!("{name} must point to a file"); + } + + let contents = + std::fs::read_to_string(&export_path).context(format!("failed to read {name}"))?; + + if let Err(err) = full_moon::parse(&contents).map_err(|errs| { + errs.into_iter() + .map(|err| err.to_string()) + .collect::>() + .join(", ") + }) { + anyhow::bail!("{name} is not a valid Luau file: {err}"); + } + + let first_part = export_path + .strip_prefix(project.path()) + .context(format!("{name} not within project directory"))? + .components() + .next() + .context(format!("{name} must contain at least one part"))?; + + let first_part = match first_part { + Component::Normal(part) => part, + _ => anyhow::bail!("{name} must be within project directory"), + }; + + let first_part_str = first_part.to_string_lossy(); + + if manifest.includes.insert(first_part_str.to_string()) { + println!( + "{}: {name} was not in includes, adding {first_part_str}", + "warn".yellow().bold() + ); + } + + if roblox_target.as_mut().map_or(false, |build_files| { + build_files.insert(first_part_str.to_string()) + }) { + println!( + "{}: {name} was not in build files, adding {first_part_str}", + "warn".yellow().bold() + ); + } + } + + for included_name in &manifest.includes { + let included_path = project.path().join(included_name); + + if !included_path.exists() { + anyhow::bail!("included file {included_name} does not exist"); + } + + // it'll be included later, with our mut modifications + if included_name.eq_ignore_ascii_case(MANIFEST_FILE_NAME) { + continue; + } + + if included_path.is_file() { + display_includes.push(included_name.clone()); + + archive.append_file( + included_name, + &mut std::fs::File::open(&included_path) + .context(format!("failed to read {included_name}"))?, + )?; + } else { + display_includes.push(format!("{included_name}/*")); + + archive + .append_dir_all(included_name, &included_path) + .context(format!("failed to include directory {included_name}"))?; + } + } + + if let Some(build_files) = &roblox_target { + for build_file in build_files.iter() { + if build_file.eq_ignore_ascii_case(MANIFEST_FILE_NAME) { + println!( + "{}: {MANIFEST_FILE_NAME} is in build files, please remove it", + "warn".yellow().bold() + ); + + continue; + } + + let build_file_path = project.path().join(build_file); + + if !build_file_path.exists() { + anyhow::bail!("build file {build_file} does not exist"); + } + + if !manifest.includes.contains(build_file) { + anyhow::bail!("build file {build_file} is not in includes, please add it"); + } + + if build_file_path.is_file() { + display_build_files.push(build_file.clone()); + } else { + display_build_files.push(format!("{build_file}/*")); + } + } + } + + { + println!("\n{}", "please confirm the following information:".bold()); + println!("name: {}", manifest.name); + println!("version: {}", manifest.version); + println!( + "description: {}", + manifest.description.as_deref().unwrap_or("(none)") + ); + println!( + "license: {}", + manifest.license.as_deref().unwrap_or("(none)") + ); + println!( + "authors: {}", + manifest + .authors + .as_ref() + .map_or("(none)".to_string(), |a| a.join(", ")) + ); + println!( + "repository: {}", + manifest.repository.as_deref().unwrap_or("(none)") + ); + + let roblox_target = roblox_target.is_some_and(|_| true); + + println!("target: {}", manifest.target); + println!( + "\tlib path: {}", + manifest + .target + .lib_path() + .map_or("(none)".to_string(), |p| p.to_string()) + ); + + match roblox_target { + #[cfg(feature = "roblox")] + true => { + println!("\tbuild files: {}", display_build_files.join(", ")); + } + _ => { + println!( + "\tbin path: {}", + manifest + .target + .bin_path() + .map_or("(none)".to_string(), |p| p.to_string()) + ); + } + } + + println!( + "includes: {}", + display_includes.into_iter().collect::>().join(", ") + ); + + if !self.dry_run && !inquire::Confirm::new("is this information correct?").prompt()? { + println!("{}", "publish aborted".red().bold()); + + return Ok(()); + } + } + + let temp_manifest_path = project + .data_dir() + .join(format!("temp_manifest_{}", chrono::Utc::now().timestamp())); + + let mut temp_manifest = std::fs::File::options() + .read(true) + .write(true) + .create(true) + .truncate(true) + .open(&temp_manifest_path) + .context("failed to create temp manifest file")?; + + serde_yaml::to_writer(&mut temp_manifest, &manifest) + .context("failed to write temp manifest file")?; + temp_manifest + .rewind() + .context("failed to rewind temp manifest file")?; + + archive.append_file(MANIFEST_FILE_NAME, &mut temp_manifest)?; + + drop(temp_manifest); + + std::fs::remove_file(temp_manifest_path)?; + + let archive = archive + .into_inner() + .context("failed to encode archive")? + .finish() + .context("failed to get archive bytes")?; + + if archive.len() > MAX_ARCHIVE_SIZE { + anyhow::bail!( + "archive size exceeds maximum size of {} bytes by {} bytes", + MAX_ARCHIVE_SIZE, + archive.len() - MAX_ARCHIVE_SIZE + ); + } + + if self.dry_run { + std::fs::write("package.tar.gz", archive)?; + + println!( + "{}", + "(dry run) package written to package.tar.gz".green().bold() + ); + + return Ok(()); + } + + todo!("publishing to registry"); + } +} diff --git a/src/cli/run.rs b/src/cli/run.rs index 94f428f..799f0ff 100644 --- a/src/cli/run.rs +++ b/src/cli/run.rs @@ -1,9 +1,10 @@ +use crate::cli::IsUpToDate; use anyhow::Context; use clap::Args; use pesde::{ - names::PackageName, - scripts::{execute_lune_script, execute_script}, - Project, + names::{PackageName, PackageNames}, + scripts::execute_script, + Project, PACKAGES_CONTAINER_NAME, }; use relative_path::RelativePathBuf; @@ -20,14 +21,59 @@ pub struct RunCommand { impl RunCommand { pub fn run(self, project: Project) -> anyhow::Result<()> { - if let Ok(_pkg_name) = self.package_or_script.parse::() { - todo!("implement binary package execution") + if let Ok(pkg_name) = self.package_or_script.parse::() { + let graph = if project.is_up_to_date()? { + project.deser_lockfile()?.graph + } else { + anyhow::bail!("outdated lockfile, please run the install command first") + }; + + let pkg_name = PackageNames::Pesde(pkg_name); + + for (version, node) in graph.get(&pkg_name).context("package not found in graph")? { + if node.node.direct.is_none() { + continue; + } + + let Some(bin_path) = node.target.bin_path() else { + anyhow::bail!("package has no bin path"); + }; + + let base_folder = node + .node + .base_folder(project.deser_manifest()?.target.kind(), true); + let container_folder = node.node.container_folder( + &project + .path() + .join(base_folder) + .join(PACKAGES_CONTAINER_NAME), + &pkg_name, + version, + ); + + let path = bin_path.to_path(&container_folder); + + execute_script( + Some(pkg_name.as_str().1), + &path, + &self.args, + project.path(), + false, + ) + .context("failed to execute script")?; + } } if let Ok(manifest) = project.deser_manifest() { - if manifest.scripts.contains_key(&self.package_or_script) { - execute_script(&manifest, &self.package_or_script, &self.args) - .context("failed to execute script")?; + if let Some(script_path) = manifest.scripts.get(&self.package_or_script) { + execute_script( + Some(&self.package_or_script), + &script_path.to_path(project.path()), + &self.args, + project.path(), + false, + ) + .context("failed to execute script")?; return Ok(()); } @@ -40,7 +86,7 @@ impl RunCommand { anyhow::bail!("path does not exist: {}", path.display()); } - execute_lune_script(None, &relative_path, &self.args) + execute_script(None, &path, &self.args, project.path(), false) .context("failed to execute script")?; Ok(()) diff --git a/src/cli/self_install.rs b/src/cli/self_install.rs new file mode 100644 index 0000000..44d35a3 --- /dev/null +++ b/src/cli/self_install.rs @@ -0,0 +1,14 @@ +use crate::cli::update_scripts_folder; +use clap::Args; +use pesde::Project; + +#[derive(Debug, Args)] +pub struct SelfInstallCommand {} + +impl SelfInstallCommand { + pub fn run(self, project: Project) -> anyhow::Result<()> { + update_scripts_folder(&project)?; + + Ok(()) + } +} diff --git a/src/download.rs b/src/download.rs index d9db138..9542d4f 100644 --- a/src/download.rs +++ b/src/download.rs @@ -10,6 +10,7 @@ use crate::{ }; impl Project { + // TODO: use threadpool for concurrent downloads pub fn download_graph( &self, graph: &DependencyGraph, diff --git a/src/git.rs b/src/git.rs new file mode 100644 index 0000000..62f8e83 --- /dev/null +++ b/src/git.rs @@ -0,0 +1,23 @@ +use crate::AuthConfig; + +pub fn authenticate_conn( + conn: &mut gix::remote::Connection< + '_, + '_, + Box, + >, + auth_config: &AuthConfig, +) { + if let Some(iden) = auth_config.git_credentials().cloned() { + conn.set_credentials(move |action| match action { + gix::credentials::helper::Action::Get(ctx) => { + Ok(Some(gix::credentials::protocol::Outcome { + identity: iden.clone(), + next: gix::credentials::helper::NextAction::from(ctx), + })) + } + gix::credentials::helper::Action::Store(_) => Ok(None), + gix::credentials::helper::Action::Erase(_) => Ok(None), + }); + } +} diff --git a/src/lib.rs b/src/lib.rs index e5860f6..cf89d33 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ use once_cell::sync::Lazy; use std::path::{Path, PathBuf}; pub mod download; +mod git; pub mod linking; pub mod lockfile; pub mod manifest; @@ -20,6 +21,7 @@ pub const MANIFEST_FILE_NAME: &str = "pesde.yaml"; pub const LOCKFILE_FILE_NAME: &str = "pesde.lock"; pub const DEFAULT_INDEX_NAME: &str = "default"; pub const PACKAGES_CONTAINER_NAME: &str = ".pesde"; +pub const MAX_ARCHIVE_SIZE: usize = 4 * 1024 * 1024; pub(crate) static REQWEST_CLIENT: Lazy = Lazy::new(|| { reqwest::blocking::Client::builder() @@ -32,43 +34,10 @@ pub(crate) static REQWEST_CLIENT: Lazy = Lazy::new(|| .expect("failed to create reqwest client") }); -#[derive(Debug, Clone)] -pub struct GitAccount { - username: String, - password: secrecy::SecretString, -} - -impl GitAccount { - pub fn new>(username: String, password: S) -> Self { - GitAccount { - username, - password: password.into(), - } - } - - pub fn as_account(&self) -> gix::sec::identity::Account { - use secrecy::ExposeSecret; - - gix::sec::identity::Account { - username: self.username.clone(), - password: self.password.expose_secret().to_string(), - } - } -} - -impl From for GitAccount { - fn from(account: gix::sec::identity::Account) -> Self { - GitAccount { - username: account.username, - password: account.password.into(), - } - } -} - #[derive(Debug, Default, Clone)] pub struct AuthConfig { - pesde_token: Option, - git_credentials: Option, + pesde_token: Option, + git_credentials: Option, } impl AuthConfig { @@ -76,39 +45,28 @@ impl AuthConfig { AuthConfig::default() } - pub fn with_pesde_token>(mut self, token: Option) -> Self { - self.pesde_token = token.map(Into::into); + pub fn pesde_token(&self) -> Option<&str> { + self.pesde_token.as_deref() + } + + pub fn git_credentials(&self) -> Option<&gix::sec::identity::Account> { + self.git_credentials.as_ref() + } + + pub fn with_pesde_token>(mut self, token: Option) -> Self { + self.pesde_token = token.map(|s| s.as_ref().to_string()); self } - pub fn with_git_credentials(mut self, git_credentials: Option) -> Self { + pub fn with_git_credentials( + mut self, + git_credentials: Option, + ) -> Self { self.git_credentials = git_credentials; self } } -pub(crate) fn authenticate_conn( - conn: &mut gix::remote::Connection< - '_, - '_, - Box, - >, - auth_config: AuthConfig, -) { - if let Some(iden) = auth_config.git_credentials { - conn.set_credentials(move |action| match action { - gix::credentials::helper::Action::Get(ctx) => { - Ok(Some(gix::credentials::protocol::Outcome { - identity: iden.as_account(), - next: gix::credentials::helper::NextAction::from(ctx), - })) - } - gix::credentials::helper::Action::Store(_) => Ok(None), - gix::credentials::helper::Action::Erase(_) => Ok(None), - }); - } -} - #[derive(Debug)] pub struct Project { path: PathBuf, @@ -137,6 +95,10 @@ impl Project { &self.data_dir } + pub fn auth_config(&self) -> &AuthConfig { + &self.auth_config + } + pub fn read_manifest(&self) -> Result, errors::ManifestReadError> { let bytes = std::fs::read(self.path.join(MANIFEST_FILE_NAME))?; Ok(bytes) diff --git a/src/linking/mod.rs b/src/linking/mod.rs index 6022507..c097993 100644 --- a/src/linking/mod.rs +++ b/src/linking/mod.rs @@ -1,6 +1,11 @@ use crate::{ - linking::generator::get_file_types, lockfile::DownloadedGraph, manifest::Manifest, - names::PackageNames, source::PackageRef, Project, MANIFEST_FILE_NAME, PACKAGES_CONTAINER_NAME, + linking::generator::get_file_types, + lockfile::DownloadedGraph, + manifest::{Manifest, ScriptName, Target}, + names::PackageNames, + scripts::execute_script, + source::PackageRef, + Project, MANIFEST_FILE_NAME, PACKAGES_CONTAINER_NAME, }; use semver::Version; use std::{collections::BTreeMap, fs::create_dir_all}; @@ -34,7 +39,7 @@ impl Project { version, ); - let lib_file = lib_file.to_path(container_folder); + let lib_file = lib_file.to_path(&container_folder); let contents = match std::fs::read_to_string(&lib_file) { Ok(contents) => contents, @@ -60,6 +65,30 @@ impl Project { .entry(name) .or_default() .insert(version, types); + + #[cfg(feature = "roblox")] + if let Target::Roblox { build_files, .. } = &node.target { + let script_name = ScriptName::RobloxSyncConfigGenerator.to_string(); + + let Some(script_path) = manifest.scripts.get(&script_name) else { + log::warn!("not having a `{script_name}` script in the manifest might cause issues with Roblox linking"); + continue; + }; + + execute_script( + Some(&script_name), + &script_path.to_path(self.path()), + build_files, + &container_folder, + false, + ) + .map_err(|e| { + errors::LinkingError::GenerateRobloxSyncConfig( + container_folder.display().to_string(), + e, + ) + })?; + } } } @@ -173,5 +202,9 @@ pub mod errors { #[error("error generating require path")] GetRequirePath(#[from] crate::linking::generator::errors::GetRequirePathError), + + #[cfg(feature = "roblox")] + #[error("error generating roblox sync config for {0}")] + GenerateRobloxSyncConfig(String, #[source] std::io::Error), } } diff --git a/src/lockfile.rs b/src/lockfile.rs index 0828763..67959ee 100644 --- a/src/lockfile.rs +++ b/src/lockfile.rs @@ -91,5 +91,5 @@ pub struct Lockfile { pub version: Version, pub overrides: BTreeMap, - pub graph: DependencyGraph, + pub graph: DownloadedGraph, } diff --git a/src/main.rs b/src/main.rs index 66b45ca..e257011 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use pesde::{AuthConfig, Project}; use std::fs::create_dir_all; mod cli; +pub mod git; #[derive(Parser, Debug)] #[clap(version, about = "pesde is a feature-rich package manager for Luau")] diff --git a/src/manifest.rs b/src/manifest.rs index 52756f0..ae707cd 100644 --- a/src/manifest.rs +++ b/src/manifest.rs @@ -1,15 +1,14 @@ +use crate::{names::PackageName, source::DependencySpecifiers}; use relative_path::RelativePathBuf; use semver::Version; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde::{Deserialize, Serialize}; use serde_with::{DeserializeFromStr, SerializeDisplay}; use std::{ - collections::BTreeMap, + collections::{BTreeMap, BTreeSet}, fmt::{Display, Formatter}, str::FromStr, }; -use crate::{names::PackageName, source::DependencySpecifiers}; - #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] #[serde(rename_all = "snake_case", deny_unknown_fields)] pub enum TargetKind { @@ -54,23 +53,32 @@ impl TargetKind { return "packages".to_string(); } - format!("{}_packages", dependency) + format!("{dependency}_packages") } } #[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -#[serde(rename_all = "snake_case", tag = "environment", remote = "Self")] +#[serde(rename_all = "snake_case", tag = "environment")] pub enum Target { #[cfg(feature = "roblox")] - Roblox { lib: RelativePathBuf }, + Roblox { + #[serde(default)] + lib: Option, + #[serde(default)] + build_files: BTreeSet, + }, #[cfg(feature = "lune")] Lune { + #[serde(default)] lib: Option, + #[serde(default)] bin: Option, }, #[cfg(feature = "luau")] Luau { + #[serde(default)] lib: Option, + #[serde(default)] bin: Option, }, } @@ -90,55 +98,47 @@ impl Target { pub fn lib_path(&self) -> Option<&RelativePathBuf> { match self { #[cfg(feature = "roblox")] - Target::Roblox { lib } => Some(lib), + Target::Roblox { lib, .. } => lib.as_ref(), #[cfg(feature = "lune")] Target::Lune { lib, .. } => lib.as_ref(), #[cfg(feature = "luau")] Target::Luau { lib, .. } => lib.as_ref(), } } -} -impl Serialize for Target { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - Self::serialize(self, serializer) - } -} - -impl<'de> Deserialize<'de> for Target { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let target = Self::deserialize(deserializer)?; - - match &target { + pub fn bin_path(&self) -> Option<&RelativePathBuf> { + match self { + #[cfg(feature = "roblox")] + Target::Roblox { .. } => None, #[cfg(feature = "lune")] - Target::Lune { lib, bin } => { - if lib.is_none() && bin.is_none() { - return Err(serde::de::Error::custom( - "one of `lib` or `bin` exports must be defined", - )); - } - } - + Target::Lune { bin, .. } => bin.as_ref(), #[cfg(feature = "luau")] - Target::Luau { lib, bin } => { - if lib.is_none() && bin.is_none() { - return Err(serde::de::Error::custom( - "one of `lib` or `bin` exports must be defined", - )); - } - } + Target::Luau { bin, .. } => bin.as_ref(), + } + } - #[allow(unreachable_patterns)] - _ => {} + pub fn validate_publish(&self) -> Result<(), errors::TargetValidatePublishError> { + let has_exports = match self { + #[cfg(feature = "roblox")] + Target::Roblox { lib, .. } => lib.is_some(), + #[cfg(feature = "lune")] + Target::Lune { lib, bin } => lib.is_some() || bin.is_some(), + #[cfg(feature = "luau")] + Target::Luau { lib, bin } => lib.is_some() || bin.is_some(), }; - Ok(target) + if !has_exports { + return Err(errors::TargetValidatePublishError::NoExportedFiles); + } + + match self { + #[cfg(feature = "roblox")] + Target::Roblox { build_files, .. } if build_files.is_empty() => { + Err(errors::TargetValidatePublishError::NoBuildFiles) + } + + _ => Ok(()), + } } } @@ -190,39 +190,57 @@ impl Display for OverrideKey { } } -#[derive(Deserialize, Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum ScriptName { + #[cfg(feature = "roblox")] + RobloxSyncConfigGenerator, + #[cfg(all(feature = "wally-compat", feature = "roblox"))] + SourcemapGenerator, +} + +impl Display for ScriptName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + #[cfg(feature = "roblox")] + ScriptName::RobloxSyncConfigGenerator => write!(f, "roblox_sync_config_generator"), + #[cfg(all(feature = "wally-compat", feature = "roblox"))] + ScriptName::SourcemapGenerator => write!(f, "sourcemap_generator"), + } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct Manifest { pub name: PackageName, pub version: Version, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub license: Option, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub authors: Option>, - #[serde(default)] + #[serde(default, skip_serializing_if = "Option::is_none")] pub repository: Option, pub target: Target, #[serde(default)] pub private: bool, - #[serde(default)] + #[serde(default, skip_serializing)] pub scripts: BTreeMap, #[serde(default)] pub indices: BTreeMap, #[cfg(feature = "wally-compat")] #[serde(default)] pub wally_indices: BTreeMap, - #[cfg(all(feature = "wally-compat", feature = "roblox"))] - #[serde(default)] - pub sourcemap_generator: Option, - #[serde(default)] + #[serde(default, skip_serializing)] pub overrides: BTreeMap, + #[serde(default)] + pub includes: BTreeSet, - #[serde(default)] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub dependencies: BTreeMap, - #[serde(default)] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub peer_dependencies: BTreeMap, - #[serde(default)] + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub dev_dependencies: BTreeMap, } @@ -275,4 +293,15 @@ pub mod errors { #[error("another specifier is already using the alias {0}")] AliasConflict(String), } + + #[derive(Debug, Error)] + #[non_exhaustive] + pub enum TargetValidatePublishError { + #[error("no exported files specified")] + NoExportedFiles, + + #[cfg(feature = "roblox")] + #[error("roblox target must have at least one build file")] + NoBuildFiles, + } } diff --git a/src/names.rs b/src/names.rs index b47c202..df6ee62 100644 --- a/src/names.rs +++ b/src/names.rs @@ -91,7 +91,7 @@ impl PackageNames { impl Display for PackageNames { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - PackageNames::Pesde(name) => write!(f, "{}", name), + PackageNames::Pesde(name) => write!(f, "{name}"), } } } diff --git a/src/scripts.rs b/src/scripts.rs index e7552be..822ec11 100644 --- a/src/scripts.rs +++ b/src/scripts.rs @@ -1,21 +1,23 @@ -use crate::manifest::Manifest; -use relative_path::RelativePathBuf; use std::{ ffi::OsStr, io::{BufRead, BufReader}, + path::Path, process::{Command, Stdio}, thread::spawn, }; -pub fn execute_lune_script, S: AsRef>( +pub fn execute_script, S: AsRef, P: AsRef>( script_name: Option<&str>, - script_path: &RelativePathBuf, + script_path: &Path, args: A, -) -> Result<(), std::io::Error> { + cwd: P, + return_stdout: bool, +) -> Result, std::io::Error> { match Command::new("lune") .arg("run") - .arg(script_path.as_str()) + .arg(script_path.as_os_str()) .args(args) + .current_dir(cwd) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -27,7 +29,7 @@ pub fn execute_lune_script, S: AsRef>( let script = match script_name { Some(script) => script.to_string(), - None => script_path.to_string(), + None => script_path.to_string_lossy().to_string(), }; let script_2 = script.to_string(); @@ -46,10 +48,17 @@ pub fn execute_lune_script, S: AsRef>( } }); + let mut stdout_str = String::new(); + for line in stdout.lines() { match line { Ok(line) => { log::info!("[{script_2}]: {line}"); + + if return_stdout { + stdout_str.push_str(&line); + stdout_str.push('\n'); + } } Err(e) => { log::error!("ERROR IN READING STDOUT OF {script_2}: {e}"); @@ -57,24 +66,18 @@ pub fn execute_lune_script, S: AsRef>( } } } + + if return_stdout { + Ok(Some(stdout_str)) + } else { + Ok(None) + } } Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - log::warn!("Lune could not be found in PATH: {e}") + log::warn!("Lune could not be found in PATH: {e}"); + + Ok(None) } - Err(e) => return Err(e), - }; - - Ok(()) -} - -pub fn execute_script, S: AsRef>( - manifest: &Manifest, - script: &str, - args: A, -) -> Result<(), std::io::Error> { - if let Some(script_path) = manifest.scripts.get(script) { - return execute_lune_script(Some(script), script_path, args); + Err(e) => Err(e), } - - Ok(()) } diff --git a/src/source/mod.rs b/src/source/mod.rs index 9ce172c..b584856 100644 --- a/src/source/mod.rs +++ b/src/source/mod.rs @@ -34,7 +34,7 @@ impl DependencySpecifier for DependencySpecifiers {} impl Display for DependencySpecifiers { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - DependencySpecifiers::Pesde(specifier) => write!(f, "{}", specifier), + DependencySpecifiers::Pesde(specifier) => write!(f, "{specifier}"), } } } diff --git a/src/source/pesde/mod.rs b/src/source/pesde/mod.rs index d29fb81..ee80bc5 100644 --- a/src/source/pesde/mod.rs +++ b/src/source/pesde/mod.rs @@ -8,7 +8,7 @@ use pkg_ref::PesdePackageRef; use specifier::PesdeDependencySpecifier; use crate::{ - authenticate_conn, + git::authenticate_conn, manifest::{DependencyType, Target}, names::{PackageName, PackageNames}, source::{hash, DependencySpecifiers, PackageSource, ResolveResult}, @@ -264,7 +264,7 @@ impl PackageSource for PesdePackageSource { .connect(Direction::Fetch) .map_err(|e| Self::RefreshError::Connect(self.repo_url.clone(), e))?; - authenticate_conn(&mut connection, project.auth_config.clone()); + authenticate_conn(&mut connection, &project.auth_config); connection .prepare_fetch(gix::progress::Discard, Default::default()) @@ -276,12 +276,13 @@ impl PackageSource for PesdePackageSource { } std::fs::create_dir_all(&path)?; + let auth_config = project.auth_config.clone(); gix::prepare_clone_bare(self.repo_url.clone(), &path) .map_err(|e| Self::RefreshError::Clone(self.repo_url.clone(), e))? .configure_connection(move |c| { - authenticate_conn(c, auth_config.clone()); + authenticate_conn(c, &auth_config); Ok(()) }) .fetch_only(gix::progress::Discard, &false.into()) @@ -344,9 +345,7 @@ impl PackageSource for PesdePackageSource { let mut response = REQWEST_CLIENT.get(url); if let Some(token) = &project.auth_config.pesde_token { - use secrecy::ExposeSecret; - response = - response.header("Authorization", format!("Bearer {}", token.expose_secret())); + response = response.header("Authorization", format!("Bearer {token}")); } let response = response.send()?;