mirror of
https://github.com/pesde-pkg/pesde.git
synced 2025-04-08 04:40:56 +01:00
575 lines
21 KiB
Rust
575 lines
21 KiB
Rust
use cfg_if::cfg_if;
|
|
use chrono::Utc;
|
|
use std::{
|
|
collections::{BTreeMap, HashMap},
|
|
fs::{create_dir_all, read, remove_dir_all, write},
|
|
str::FromStr,
|
|
time::Duration,
|
|
};
|
|
|
|
use flate2::{write::GzEncoder, Compression};
|
|
use futures_executor::block_on;
|
|
use ignore::{overrides::OverrideBuilder, WalkBuilder};
|
|
use inquire::{validator::Validation, Select, Text};
|
|
use log::debug;
|
|
use lune::Runtime;
|
|
use once_cell::sync::Lazy;
|
|
use reqwest::{header::AUTHORIZATION, Url};
|
|
use semver::Version;
|
|
use serde_json::Value;
|
|
use tar::Builder as TarBuilder;
|
|
|
|
use pesde::{
|
|
dependencies::{registry::RegistryDependencySpecifier, DependencySpecifier, PackageRef},
|
|
index::Index,
|
|
manifest::{Manifest, PathStyle, Realm},
|
|
multithread::MultithreadedJob,
|
|
package_name::{PackageName, StandardPackageName},
|
|
patches::{create_patch, setup_patches_repo},
|
|
project::{InstallOptions, Project, DEFAULT_INDEX_NAME},
|
|
DEV_PACKAGES_FOLDER, IGNORED_FOLDERS, MANIFEST_FILE_NAME, PACKAGES_FOLDER, PATCHES_FOLDER,
|
|
SERVER_PACKAGES_FOLDER,
|
|
};
|
|
|
|
use crate::cli::{
|
|
clone_index, send_request, Command, CLI_CONFIG, CWD, DEFAULT_INDEX, DEFAULT_INDEX_URL, DIRS,
|
|
MULTI, REQWEST_CLIENT,
|
|
};
|
|
|
|
pub const MAX_ARCHIVE_SIZE: usize = 4 * 1024 * 1024;
|
|
|
|
fn multithreaded_bar<E: Send + Sync + Into<anyhow::Error> + 'static>(
|
|
job: MultithreadedJob<E>,
|
|
len: u64,
|
|
message: String,
|
|
) -> Result<(), anyhow::Error> {
|
|
let bar = MULTI.add(
|
|
indicatif::ProgressBar::new(len)
|
|
.with_style(
|
|
indicatif::ProgressStyle::default_bar()
|
|
.template("{msg} {bar:40.208/166} {pos}/{len} {percent}% {elapsed_precise}")?,
|
|
)
|
|
.with_message(message),
|
|
);
|
|
bar.enable_steady_tick(Duration::from_millis(100));
|
|
|
|
while let Ok(result) = job.progress().recv() {
|
|
result.map_err(Into::into)?;
|
|
bar.inc(1);
|
|
}
|
|
|
|
bar.finish_with_message("done");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
macro_rules! none_if_empty {
|
|
($s:expr) => {
|
|
if $s.is_empty() {
|
|
None
|
|
} else {
|
|
Some($s)
|
|
}
|
|
};
|
|
}
|
|
|
|
pub fn root_command(cmd: Command) -> anyhow::Result<()> {
|
|
let mut project: Lazy<Project> = Lazy::new(|| {
|
|
let manifest = Manifest::from_path(CWD.to_path_buf()).unwrap();
|
|
let indices = manifest
|
|
.indices
|
|
.clone()
|
|
.into_iter()
|
|
.map(|(k, v)| (k, Box::new(clone_index(&v)) as Box<dyn Index>))
|
|
.collect::<HashMap<_, _>>();
|
|
|
|
Project::new(CWD.to_path_buf(), CLI_CONFIG.cache_dir(), indices, manifest).unwrap()
|
|
});
|
|
|
|
match cmd {
|
|
Command::Install { locked } => {
|
|
for packages_folder in &[PACKAGES_FOLDER, DEV_PACKAGES_FOLDER, SERVER_PACKAGES_FOLDER] {
|
|
if let Err(e) = remove_dir_all(CWD.join(packages_folder)) {
|
|
if e.kind() != std::io::ErrorKind::NotFound {
|
|
return Err(e.into());
|
|
} else {
|
|
debug!("no {packages_folder} folder found, skipping removal");
|
|
}
|
|
};
|
|
}
|
|
|
|
let manifest = project.manifest().clone();
|
|
let lockfile = manifest.dependency_graph(&mut project, locked)?;
|
|
|
|
let download_job = project.download(&lockfile)?;
|
|
|
|
multithreaded_bar(
|
|
download_job,
|
|
lockfile.children.values().map(|v| v.len() as u64).sum(),
|
|
"Downloading packages".to_string(),
|
|
)?;
|
|
|
|
#[allow(unused_variables)]
|
|
project.convert_manifests(&lockfile, |path| {
|
|
cfg_if! {
|
|
if #[cfg(feature = "wally")] {
|
|
if let Some(sourcemap_generator) = &manifest.sourcemap_generator {
|
|
cfg_if! {
|
|
if #[cfg(target_os = "windows")] {
|
|
std::process::Command::new("pwsh")
|
|
.args(["-C", &sourcemap_generator])
|
|
.current_dir(path)
|
|
.output()
|
|
.expect("failed to execute process");
|
|
} else {
|
|
std::process::Command::new("sh")
|
|
.args(["-c", &sourcemap_generator])
|
|
.current_dir(path)
|
|
.output()
|
|
.expect("failed to execute process");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})?;
|
|
|
|
let project = Lazy::force_mut(&mut project);
|
|
|
|
project.install(
|
|
InstallOptions::new()
|
|
.locked(locked)
|
|
.auto_download(false)
|
|
.lockfile(lockfile),
|
|
)?;
|
|
}
|
|
Command::Run { package, args } => {
|
|
let bin_path = if let Some(package) = package {
|
|
let lockfile = project
|
|
.lockfile()?
|
|
.ok_or(anyhow::anyhow!("lockfile not found"))?;
|
|
|
|
let resolved_pkg = lockfile
|
|
.children
|
|
.get(&package.clone().into())
|
|
.and_then(|versions| {
|
|
versions
|
|
.values()
|
|
.find(|pkg_ref| lockfile.root_specifier(pkg_ref).is_some())
|
|
})
|
|
.ok_or(anyhow::anyhow!(
|
|
"package not found in lockfile (or isn't root)"
|
|
))?;
|
|
|
|
let pkg_path = resolved_pkg.directory(project.path()).1;
|
|
let manifest = Manifest::from_path(&pkg_path)?;
|
|
|
|
let Some(bin_path) = manifest.exports.bin else {
|
|
anyhow::bail!("no bin found in package");
|
|
};
|
|
|
|
bin_path.to_path(pkg_path)
|
|
} else {
|
|
let manifest = project.manifest();
|
|
let bin_path = manifest
|
|
.exports
|
|
.bin
|
|
.clone()
|
|
.ok_or(anyhow::anyhow!("no bin found in package"))?;
|
|
|
|
bin_path.to_path(project.path())
|
|
};
|
|
|
|
let mut runtime = Runtime::new().with_args(args);
|
|
|
|
block_on(runtime.run(
|
|
bin_path.with_extension("").display().to_string(),
|
|
&read(bin_path)?,
|
|
))?;
|
|
}
|
|
Command::Search { query } => {
|
|
let config = DEFAULT_INDEX.config()?;
|
|
let api_url = config.api();
|
|
|
|
let response = send_request(REQWEST_CLIENT.get(Url::parse_with_params(
|
|
&format!("{}/v0/search", api_url),
|
|
&query.map(|q| vec![("query", q)]).unwrap_or_default(),
|
|
)?))?
|
|
.json::<Value>()?;
|
|
|
|
for package in response.as_array().unwrap() {
|
|
println!(
|
|
"{}@{}{}",
|
|
package["name"].as_str().unwrap(),
|
|
package["version"].as_str().unwrap(),
|
|
package["description"]
|
|
.as_str()
|
|
.map(|d| if d.is_empty() {
|
|
d.to_string()
|
|
} else {
|
|
format!("\n{}\n", d)
|
|
})
|
|
.unwrap_or_default()
|
|
);
|
|
}
|
|
}
|
|
Command::Publish => {
|
|
if project.manifest().private {
|
|
anyhow::bail!("package is private, cannot publish");
|
|
}
|
|
|
|
let encoder = GzEncoder::new(vec![], Compression::default());
|
|
let mut archive = TarBuilder::new(encoder);
|
|
|
|
let cwd = &CWD.to_path_buf();
|
|
|
|
let mut walk_builder = WalkBuilder::new(cwd);
|
|
walk_builder.add_custom_ignore_filename(".pesdeignore");
|
|
let mut overrides = OverrideBuilder::new(cwd);
|
|
|
|
for packages_folder in IGNORED_FOLDERS {
|
|
overrides.add(&format!("!{}", packages_folder))?;
|
|
}
|
|
|
|
walk_builder.overrides(overrides.build()?);
|
|
|
|
for entry in walk_builder.build() {
|
|
let entry = entry?;
|
|
let path = entry.path();
|
|
let relative_path = path.strip_prefix(cwd)?;
|
|
let entry_type = entry
|
|
.file_type()
|
|
.ok_or(anyhow::anyhow!("failed to get file type"))?;
|
|
|
|
if relative_path.as_os_str().is_empty() {
|
|
continue;
|
|
}
|
|
|
|
if entry_type.is_file() {
|
|
archive.append_path_with_name(path, relative_path)?;
|
|
} else if entry_type.is_dir() {
|
|
archive.append_dir(relative_path, path)?;
|
|
}
|
|
}
|
|
|
|
let archive = archive.into_inner()?.finish()?;
|
|
|
|
if archive.len() > MAX_ARCHIVE_SIZE {
|
|
anyhow::bail!(
|
|
"archive is too big ({} bytes), max {MAX_ARCHIVE_SIZE}. aborting...",
|
|
archive.len()
|
|
);
|
|
}
|
|
|
|
let part = reqwest::blocking::multipart::Part::bytes(archive)
|
|
.file_name("tarball.tar.gz")
|
|
.mime_str("application/gzip")?;
|
|
|
|
let index = project.indices().get(DEFAULT_INDEX_NAME).unwrap();
|
|
|
|
let mut request = REQWEST_CLIENT
|
|
.post(format!("{}/v0/packages", index.config()?.api()))
|
|
.multipart(reqwest::blocking::multipart::Form::new().part("tarball", part));
|
|
|
|
if let Some(token) = index.registry_auth_token() {
|
|
request = request.header(AUTHORIZATION, format!("Bearer {token}"));
|
|
} else {
|
|
request = request.header(AUTHORIZATION, "");
|
|
}
|
|
|
|
println!("{}", send_request(request)?.text()?);
|
|
}
|
|
Command::Patch { package } => {
|
|
let lockfile = project
|
|
.lockfile()?
|
|
.ok_or(anyhow::anyhow!("lockfile not found"))?;
|
|
|
|
let resolved_pkg = lockfile
|
|
.children
|
|
.get(&package.0)
|
|
.and_then(|versions| versions.get(&package.1))
|
|
.ok_or(anyhow::anyhow!("package not found in lockfile"))?;
|
|
|
|
let dir = DIRS
|
|
.data_dir()
|
|
.join("patches")
|
|
.join(format!("{}@{}", package.0.escaped(), package.1))
|
|
.join(Utc::now().timestamp().to_string());
|
|
|
|
if dir.exists() {
|
|
anyhow::bail!(
|
|
"patch already exists. remove the directory {} to create a new patch",
|
|
dir.display()
|
|
);
|
|
}
|
|
|
|
create_dir_all(&dir)?;
|
|
|
|
let project = Lazy::force_mut(&mut project);
|
|
let url = resolved_pkg.pkg_ref.resolve_url(project)?;
|
|
|
|
let index = project.indices().get(DEFAULT_INDEX_NAME).unwrap();
|
|
|
|
resolved_pkg.pkg_ref.download(
|
|
&REQWEST_CLIENT,
|
|
index.registry_auth_token().map(|t| t.to_string()),
|
|
url.as_ref(),
|
|
index.credentials_fn().cloned(),
|
|
&dir,
|
|
)?;
|
|
|
|
match &resolved_pkg.pkg_ref {
|
|
PackageRef::Git(_) => {}
|
|
_ => {
|
|
setup_patches_repo(&dir)?;
|
|
}
|
|
}
|
|
|
|
println!("done! modify the files in {} and run `{} patch-commit <DIRECTORY>` to commit the changes", dir.display(), env!("CARGO_BIN_NAME"));
|
|
}
|
|
Command::PatchCommit { dir } => {
|
|
let name = dir
|
|
.parent()
|
|
.and_then(|p| p.file_name())
|
|
.and_then(|f| f.to_str())
|
|
.unwrap();
|
|
|
|
let patch_path = project.path().join(PATCHES_FOLDER);
|
|
create_dir_all(&patch_path)?;
|
|
|
|
let patch_path = patch_path.join(format!("{name}.patch"));
|
|
if patch_path.exists() {
|
|
anyhow::bail!(
|
|
"patch already exists. remove the file {} to create a new patch",
|
|
patch_path.display()
|
|
);
|
|
}
|
|
|
|
let patches = create_patch(&dir)?;
|
|
|
|
write(&patch_path, patches)?;
|
|
|
|
remove_dir_all(&dir)?;
|
|
|
|
println!(
|
|
"done! to apply the patch, run `{} install`",
|
|
env!("CARGO_BIN_NAME")
|
|
);
|
|
}
|
|
Command::Init => {
|
|
if CWD.join(MANIFEST_FILE_NAME).exists() {
|
|
anyhow::bail!("manifest already exists");
|
|
}
|
|
|
|
let default_name = CWD.file_name().and_then(|s| s.to_str());
|
|
|
|
let mut name =
|
|
Text::new("What is the name of the package?").with_validator(|name: &str| {
|
|
Ok(match StandardPackageName::from_str(name) {
|
|
Ok(_) => Validation::Valid,
|
|
Err(e) => Validation::Invalid(e.into()),
|
|
})
|
|
});
|
|
|
|
if let Some(name_str) = default_name {
|
|
name = name.with_initial_value(name_str);
|
|
}
|
|
|
|
let name = name.prompt()?;
|
|
|
|
let path_style =
|
|
Select::new("What style of paths do you want to use?", vec!["roblox"]).prompt()?;
|
|
let path_style = match path_style {
|
|
"roblox" => PathStyle::Roblox {
|
|
place: Default::default(),
|
|
},
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
let description = Text::new("What is the description of the package?").prompt()?;
|
|
let license = Text::new("What is the license of the package?").prompt()?;
|
|
let authors = Text::new("Who are the authors of the package? (split using ;)")
|
|
.prompt()?
|
|
.split(';')
|
|
.map(|s| s.trim().to_string())
|
|
.filter(|s| !s.is_empty())
|
|
.collect::<Vec<String>>();
|
|
let repository = Text::new("What is the repository of the package?").prompt()?;
|
|
|
|
let private = Select::new("Is this package private?", vec!["yes", "no"]).prompt()?;
|
|
let private = private == "yes";
|
|
|
|
let realm = Select::new(
|
|
"What is the realm of the package?",
|
|
vec!["shared", "server", "dev"],
|
|
)
|
|
.prompt()?;
|
|
|
|
let realm = match realm {
|
|
"shared" => Realm::Shared,
|
|
"server" => Realm::Server,
|
|
"dev" => Realm::Development,
|
|
_ => unreachable!(),
|
|
};
|
|
|
|
let manifest = Manifest {
|
|
name: name.parse()?,
|
|
version: Version::parse("0.1.0")?,
|
|
exports: Default::default(),
|
|
path_style,
|
|
private,
|
|
realm: Some(realm),
|
|
indices: BTreeMap::from([(
|
|
DEFAULT_INDEX_NAME.to_string(),
|
|
DEFAULT_INDEX_URL.to_string(),
|
|
)]),
|
|
#[cfg(feature = "wally")]
|
|
sourcemap_generator: None,
|
|
overrides: Default::default(),
|
|
|
|
dependencies: Default::default(),
|
|
peer_dependencies: Default::default(),
|
|
description: none_if_empty!(description),
|
|
license: none_if_empty!(license),
|
|
authors: none_if_empty!(authors),
|
|
repository: none_if_empty!(repository),
|
|
};
|
|
|
|
manifest.write(CWD.to_path_buf())?;
|
|
}
|
|
Command::Add {
|
|
package,
|
|
realm,
|
|
peer,
|
|
} => {
|
|
let mut manifest = project.manifest().clone();
|
|
|
|
let specifier = match package.0.clone() {
|
|
PackageName::Standard(name) => {
|
|
DependencySpecifier::Registry(RegistryDependencySpecifier {
|
|
name,
|
|
version: package.1,
|
|
realm,
|
|
index: DEFAULT_INDEX_NAME.to_string(),
|
|
})
|
|
}
|
|
#[cfg(feature = "wally")]
|
|
PackageName::Wally(name) => DependencySpecifier::Wally(
|
|
pesde::dependencies::wally::WallyDependencySpecifier {
|
|
name,
|
|
version: package.1,
|
|
realm,
|
|
index_url: crate::cli::DEFAULT_WALLY_INDEX_URL.parse().unwrap(),
|
|
},
|
|
),
|
|
};
|
|
|
|
fn insert_into(
|
|
deps: &mut BTreeMap<String, DependencySpecifier>,
|
|
specifier: DependencySpecifier,
|
|
name: PackageName,
|
|
) {
|
|
macro_rules! not_taken {
|
|
($key:expr) => {
|
|
(!deps.contains_key(&$key)).then_some($key)
|
|
};
|
|
}
|
|
|
|
let key = not_taken!(name.name().to_string())
|
|
.or_else(|| not_taken!(format!("{}/{}", name.scope(), name.name())))
|
|
.or_else(|| not_taken!(name.to_string()))
|
|
.unwrap();
|
|
deps.insert(key, specifier);
|
|
}
|
|
|
|
if peer {
|
|
insert_into(
|
|
&mut manifest.peer_dependencies,
|
|
specifier,
|
|
package.0.clone(),
|
|
);
|
|
} else {
|
|
insert_into(&mut manifest.dependencies, specifier, package.0.clone());
|
|
}
|
|
|
|
manifest.write(CWD.to_path_buf())?
|
|
}
|
|
Command::Remove { package } => {
|
|
let mut manifest = project.manifest().clone();
|
|
|
|
for dependencies in [&mut manifest.dependencies, &mut manifest.peer_dependencies] {
|
|
dependencies.retain(|_, d| {
|
|
if let DependencySpecifier::Registry(registry) = d {
|
|
match &package {
|
|
PackageName::Standard(name) => ®istry.name != name,
|
|
#[cfg(feature = "wally")]
|
|
PackageName::Wally(_) => true,
|
|
}
|
|
} else {
|
|
cfg_if! {
|
|
if #[cfg(feature = "wally")] {
|
|
#[allow(clippy::collapsible_else_if)]
|
|
if let DependencySpecifier::Wally(wally) = d {
|
|
match &package {
|
|
PackageName::Standard(_) => true,
|
|
PackageName::Wally(name) => &wally.name != name,
|
|
}
|
|
} else {
|
|
true
|
|
}
|
|
} else {
|
|
true
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
manifest.write(project.path())?
|
|
}
|
|
Command::Outdated => {
|
|
let project = Lazy::force_mut(&mut project);
|
|
|
|
let manifest = project.manifest().clone();
|
|
let lockfile = manifest.dependency_graph(project, false)?;
|
|
|
|
for (name, versions) in &lockfile.children {
|
|
for (version, resolved_pkg) in versions {
|
|
if lockfile.root_specifier(resolved_pkg).is_none() {
|
|
continue;
|
|
}
|
|
|
|
if let PackageRef::Registry(registry) = &resolved_pkg.pkg_ref {
|
|
let latest_version = send_request(REQWEST_CLIENT.get(format!(
|
|
"{}/v0/packages/{}/{}/versions",
|
|
resolved_pkg.pkg_ref.get_index(project).config()?.api(),
|
|
registry.name.scope(),
|
|
registry.name.name()
|
|
)))?
|
|
.json::<Value>()?
|
|
.as_array()
|
|
.and_then(|a| a.last())
|
|
.and_then(|v| v.as_str())
|
|
.and_then(|s| s.parse::<Version>().ok())
|
|
.ok_or(anyhow::anyhow!(
|
|
"failed to get latest version of {name}@{version}"
|
|
))?;
|
|
|
|
if &latest_version > version {
|
|
println!(
|
|
"{name}@{version} is outdated. latest version: {latest_version}"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#[cfg(feature = "wally")]
|
|
Command::Convert => {
|
|
Manifest::from_path_or_convert(CWD.to_path_buf())?;
|
|
}
|
|
_ => unreachable!(),
|
|
}
|
|
|
|
Ok(())
|
|
}
|