From e8ed3ff5a83d56dcb347f2734ac63738ae15bd91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 25 Mar 2023 18:12:38 -0700 Subject: [PATCH] feat(build): link bins (#212) Fixes: https://github.com/orogene/orogene/issues/176 --- Cargo.lock | 24 +- Cargo.toml | 2 + LICENSE | 218 ++---------- WORKSPACE_README.tpl | 4 +- crates/nassun/README.md | 4 +- crates/node-maintainer/Cargo.toml | 3 + crates/node-maintainer/README.md | 4 +- crates/node-maintainer/src/error.rs | 5 + crates/node-maintainer/src/maintainer.rs | 137 ++++++- crates/oro-client/README.md | 4 +- crates/oro-common/Cargo.toml | 2 + crates/oro-common/README.md | 4 +- crates/oro-common/src/build_manifest.rs | 138 ++++++++ crates/oro-common/src/lib.rs | 2 + crates/oro-common/src/manifest.rs | 12 +- crates/oro-config-derive/README.md | 4 +- crates/oro-config/README.md | 4 +- crates/oro-package-spec/README.md | 4 +- crates/oro-shim-bin/Cargo.toml | 18 + crates/oro-shim-bin/README.md | 19 + crates/oro-shim-bin/src/lib.rs | 335 ++++++++++++++++++ crates/oro-shim-bin/tests/fixtures/from.env | 2 + crates/oro-shim-bin/tests/fixtures/from.env.S | 2 + .../oro-shim-bin/tests/fixtures/from.env.args | 2 + .../fixtures/from.env.multiple.variables | 1 + .../tests/fixtures/from.env.nospace | 2 + .../tests/fixtures/from.env.variables | 1 + crates/oro-shim-bin/tests/fixtures/from.exe | 1 + crates/oro-shim-bin/tests/fixtures/from.sh | 2 + .../oro-shim-bin/tests/fixtures/from.sh.args | 2 + crates/oro-shim-bin/tests/shim_bin.rs | 73 ++++ .../snapshots/shim_bin__from.env.S.cmd.snap | 22 ++ .../snapshots/shim_bin__from.env.S.ps1.snap | 33 ++ .../tests/snapshots/shim_bin__from.env.S.snap | 17 + .../shim_bin__from.env.args.cmd.snap | 22 ++ .../shim_bin__from.env.args.ps1.snap | 33 ++ .../snapshots/shim_bin__from.env.args.snap | 17 + .../snapshots/shim_bin__from.env.cmd.snap | 22 ++ ..._bin__from.env.multiple.variables.cmd.snap | 24 ++ ..._bin__from.env.multiple.variables.ps1.snap | 35 ++ ...shim_bin__from.env.multiple.variables.snap | 17 + .../snapshots/shim_bin__from.env.ps1.snap | 33 ++ .../tests/snapshots/shim_bin__from.env.snap | 17 + .../shim_bin__from.env.variables.cmd.snap | 23 ++ .../shim_bin__from.env.variables.ps1.snap | 34 ++ .../shim_bin__from.env.variables.snap | 17 + .../snapshots/shim_bin__from.exe.cmd.snap | 14 + .../snapshots/shim_bin__from.exe.ps1.snap | 21 ++ .../tests/snapshots/shim_bin__from.exe.snap | 13 + .../snapshots/shim_bin__from.sh.args.cmd.snap | 22 ++ .../snapshots/shim_bin__from.sh.args.ps1.snap | 33 ++ .../snapshots/shim_bin__from.sh.args.snap | 17 + .../snapshots/shim_bin__from.sh.cmd.snap | 22 ++ .../snapshots/shim_bin__from.sh.ps1.snap | 33 ++ .../tests/snapshots/shim_bin__from.sh.snap | 17 + src/commands/restore.rs | 34 +- src/commands/view.rs | 7 + 57 files changed, 1407 insertions(+), 227 deletions(-) create mode 100644 crates/oro-common/src/build_manifest.rs create mode 100644 crates/oro-shim-bin/Cargo.toml create mode 100644 crates/oro-shim-bin/README.md create mode 100644 crates/oro-shim-bin/src/lib.rs create mode 100644 crates/oro-shim-bin/tests/fixtures/from.env create mode 100644 crates/oro-shim-bin/tests/fixtures/from.env.S create mode 100644 crates/oro-shim-bin/tests/fixtures/from.env.args create mode 100644 crates/oro-shim-bin/tests/fixtures/from.env.multiple.variables create mode 100644 crates/oro-shim-bin/tests/fixtures/from.env.nospace create mode 100644 crates/oro-shim-bin/tests/fixtures/from.env.variables create mode 100644 crates/oro-shim-bin/tests/fixtures/from.exe create mode 100644 crates/oro-shim-bin/tests/fixtures/from.sh create mode 100644 crates/oro-shim-bin/tests/fixtures/from.sh.args create mode 100644 crates/oro-shim-bin/tests/shim_bin.rs create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.S.cmd.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.S.ps1.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.S.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.args.cmd.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.args.ps1.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.args.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.cmd.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.multiple.variables.cmd.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.multiple.variables.ps1.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.multiple.variables.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.ps1.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.variables.cmd.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.variables.ps1.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.variables.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.exe.cmd.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.exe.ps1.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.exe.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.args.cmd.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.args.ps1.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.args.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.cmd.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.ps1.snap create mode 100644 crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.snap diff --git a/Cargo.lock b/Cargo.lock index 0d394684..0a13d987 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1894,6 +1894,7 @@ dependencies = [ "console_error_panic_hook", "futures 0.3.26", "indicatif", + "insta", "js-sys", "kdl", "maplit", @@ -1902,6 +1903,8 @@ dependencies = [ "node-semver", "oro-common", "oro-package-spec", + "oro-shim-bin", + "pathdiff", "petgraph", "pretty_assertions", "reflink", @@ -2088,11 +2091,13 @@ dependencies = [ "miette", "node-semver", "nom", + "pathdiff", "pretty_assertions", "serde", "serde_json", "thiserror", "url", + "walkdir", ] [[package]] @@ -2132,6 +2137,17 @@ dependencies = [ "url", ] +[[package]] +name = "oro-shim-bin" +version = "0.3.13" +dependencies = [ + "insta", + "once_cell", + "pathdiff", + "regex", + "tempfile", +] + [[package]] name = "orogene" version = "0.3.13" @@ -2547,9 +2563,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +checksum = "cce168fea28d3e05f158bda4576cf0c844d5045bc2cc3620fa0292ed5bb5814c" dependencies = [ "aho-corasick", "memchr", @@ -2567,9 +2583,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.28" +version = "0.6.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "reqwest" diff --git a/Cargo.toml b/Cargo.toml index faf08751..961d1349 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -85,6 +85,7 @@ mockito = "1.0.0" node-semver = "2.1.0" nom = "7.1.3" once_cell = "1.17.1" +pathdiff = "0.2.1" percent-encoding = "2.1.0" poloto = "17.1.0" pretty_assertions = "1.3.0" @@ -92,6 +93,7 @@ proc-macro2 = "1.0.18" quote = "1.0.7" rand = "0.8.5" reflink = "0.1.3" +regex = "1.7.2" reqwest = "0.11.14" reqwest-middleware = "0.2.0" resvg = "0.29.0" diff --git a/LICENSE b/LICENSE index d6456956..a80d8a3d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,202 +1,34 @@ +Copyright 2023 Orogene Maintainers and Contributors - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + http://www.apache.org/licenses/LICENSE-2.0 - 1. Definitions. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. +----------- - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. +Some portions of Orogene are based on code from various +projects from the NPM CLI, which contain the following copyright notice: - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. +The ISC License - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. +Copyright (c) npm, Inc. and Contributors - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/WORKSPACE_README.tpl b/WORKSPACE_README.tpl index ebad2e02..6c240aa3 100644 --- a/WORKSPACE_README.tpl +++ b/WORKSPACE_README.tpl @@ -14,5 +14,5 @@ repository](https://github.com/orogene/orogene). ## License -This project is distributed under the [Apache 2.0 -License](https://github.com/orogene/orogene/blob/main/LICENSE) +For licensing information, please check [the LICENSE file in the Orogene +repository](https://github.com/orogene/orogene/blob/main/LICENSE). diff --git a/crates/nassun/README.md b/crates/nassun/README.md index f5f70831..3c80790f 100644 --- a/crates/nassun/README.md +++ b/crates/nassun/README.md @@ -15,5 +15,5 @@ repository](https://github.com/orogene/orogene). ## License -This project is distributed under the [Apache 2.0 -License](https://github.com/orogene/orogene/blob/main/LICENSE) +For licensing information, please check [the LICENSE file in the Orogene +repository](https://github.com/orogene/orogene/blob/main/LICENSE). diff --git a/crates/node-maintainer/Cargo.toml b/crates/node-maintainer/Cargo.toml index 80195137..e11d8006 100644 --- a/crates/node-maintainer/Cargo.toml +++ b/crates/node-maintainer/Cargo.toml @@ -13,6 +13,7 @@ readme = "README.md" nassun = { version = "=0.3.13", path = "../nassun" } oro-common = { version = "=0.3.13", path = "../oro-common" } oro-package-spec = { version = "=0.3.13", path = "../oro-package-spec" } +oro-shim-bin = { version = "=0.3.13", path = "../oro-shim-bin" } async-std = { workspace = true } colored = { workspace = true } @@ -32,6 +33,7 @@ url = { workspace = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] reflink = { workspace = true } indicatif = { workspace = true } +pathdiff = { workspace = true } tempfile = { workspace = true } walkdir = { workspace = true } @@ -46,6 +48,7 @@ wasm-bindgen-futures = { workspace = true } [dev-dependencies] async-std = { workspace = true, features = ["attributes", "tokio1"] } +insta = { workspace = true } maplit = { workspace = true } miette = { workspace = true, features = ["fancy"] } pretty_assertions = { workspace = true } diff --git a/crates/node-maintainer/README.md b/crates/node-maintainer/README.md index 6186b483..2510288f 100644 --- a/crates/node-maintainer/README.md +++ b/crates/node-maintainer/README.md @@ -15,5 +15,5 @@ repository](https://github.com/orogene/orogene). ## License -This project is distributed under the [Apache 2.0 -License](https://github.com/orogene/orogene/blob/main/LICENSE) +For licensing information, please check [the LICENSE file in the Orogene +repository](https://github.com/orogene/orogene/blob/main/LICENSE). diff --git a/crates/node-maintainer/src/error.rs b/crates/node-maintainer/src/error.rs index e14a6fe5..7deb8e20 100644 --- a/crates/node-maintainer/src/error.rs +++ b/crates/node-maintainer/src/error.rs @@ -125,6 +125,11 @@ pub enum NodeMaintainerError { #[error(transparent)] #[diagnostic(code(node_maintainer::walkdir_error))] WalkDirError(#[from] walkdir::Error), + + #[cfg(not(target_arch = "wasm32"))] + #[error("Failed to read manifest during build step, at {}", .0.display())] + #[diagnostic(code(node_maintainer::build_manifest_read_error))] + BuildManifestReadError(std::path::PathBuf, #[source] std::io::Error), } impl From> for NodeMaintainerError { diff --git a/crates/node-maintainer/src/maintainer.rs b/crates/node-maintainer/src/maintainer.rs index 794bcfa2..d88a96cd 100644 --- a/crates/node-maintainer/src/maintainer.rs +++ b/crates/node-maintainer/src/maintainer.rs @@ -1,12 +1,14 @@ use std::cmp::Ordering; use std::collections::{BTreeMap, HashSet, VecDeque}; +use std::ffi::OsStr; use std::path::{Path, PathBuf}; #[cfg(not(target_arch = "wasm32"))] use std::sync::atomic::{self, AtomicUsize}; +use std::sync::Arc; #[cfg(not(target_arch = "wasm32"))] use async_std::fs; -use async_std::sync::{Arc, Mutex}; +use async_std::sync::Mutex; #[cfg(not(target_arch = "wasm32"))] use colored::*; #[cfg(not(target_arch = "wasm32"))] @@ -15,7 +17,7 @@ use futures::{StreamExt, TryFutureExt}; use nassun::client::{Nassun, NassunOpts}; use nassun::package::Package; use nassun::PackageSpec; -use oro_common::{CorgiManifest, CorgiVersionMetadata}; +use oro_common::{BuildManifest, CorgiManifest, CorgiVersionMetadata}; use petgraph::stable_graph::NodeIndex; use petgraph::visit::EdgeRef; use petgraph::Direction; @@ -405,6 +407,7 @@ impl NodeMaintainer { } let nm_osstr = Some(std::ffi::OsStr::new("node_modules")); + let bin_osstr = Some(std::ffi::OsStr::new(".bin")); let meta = prefix.join(META_FILE_NAME); let mut extraneous_packages = 0; let extraneous = &mut extraneous_packages; @@ -419,13 +422,18 @@ impl NodeMaintainer { return false; } - if entry_path.file_name() == nm_osstr { + let file_name = entry_path.file_name(); + + if file_name == nm_osstr { // We don't want to skip node_modules themselves return true; } - if entry_path - .file_name() + if file_name == bin_osstr { + return false; + } + + if file_name .expect("this should have a file name") .to_string_lossy() .starts_with('@') @@ -482,6 +490,7 @@ impl NodeMaintainer { let entry_path = entry.path(); let file_name = entry_path.file_name(); if file_name == nm_osstr + || file_name == bin_osstr || file_name .map(|s| s.to_string_lossy().starts_with('@')) .unwrap_or(false) @@ -491,11 +500,13 @@ impl NodeMaintainer { if let Some(pb) = &self.on_prune_progress { pb(entry_path); } + tracing::trace!("Pruning extraneous directory: {}", entry.path().display()); async_std::fs::remove_dir_all(entry.path()).await?; } else { if let Some(pb) = &self.on_prune_progress { pb(entry_path); } + tracing::trace!("Pruning extraneous file: {}", entry.path().display()); async_std::fs::remove_file(entry.path()).await?; } } @@ -517,6 +528,7 @@ impl NodeMaintainer { #[cfg(not(target_arch = "wasm32"))] pub async fn extract(&self) -> Result { + tracing::debug!("Extracting node_modules/..."); let start = std::time::Instant::now(); let root = &self.root; @@ -591,6 +603,111 @@ impl NodeMaintainer { Ok(actually_extracted) } + #[cfg(not(target_arch = "wasm32"))] + pub async fn run_scripts(&self) -> Result<(), NodeMaintainerError> { + tracing::debug!("Running lifecycle scripts..."); + let start = std::time::Instant::now(); + tracing::debug!( + "Ran lifecycle scripts in {}ms.", + start.elapsed().as_millis() + ); + Ok(()) + } + + #[cfg(not(target_arch = "wasm32"))] + pub async fn link_bins(&self) -> Result { + use walkdir::WalkDir; + + tracing::debug!("Linking bins..."); + let start = std::time::Instant::now(); + let root = &self.root; + let linked = Arc::new(AtomicUsize::new(0)); + let bin_file_name = Some(OsStr::new(".bin")); + let nm_file_name = Some(OsStr::new("node_modules")); + for entry in WalkDir::new(root.join("node_modules")) + .into_iter() + .filter_entry(|e| { + let path = e.path().file_name(); + path == bin_file_name || path == nm_file_name + }) + { + let entry = entry?; + if entry.path().file_name() == bin_file_name { + async_std::fs::remove_dir_all(entry.path()).await?; + } + } + futures::stream::iter(self.graph.inner.node_indices()) + .map(|idx| Ok((idx, linked.clone()))) + .try_for_each_concurrent(self.concurrency, move |(idx, linked)| async move { + if idx == self.graph.root { + return Ok(()); + } + + let subdir = self + .graph + .node_path(idx) + .iter() + .map(|x| x.to_string()) + .collect::>() + .join("/node_modules/"); + let package_dir = root.join("node_modules").join(subdir); + let parent = package_dir.parent().expect("must have parent"); + let target_dir = if parent.file_name() == Some(OsStr::new("node_modules")) { + parent.join(".bin") + } else { + // Scoped + parent.parent().expect("must have parent").join(".bin") + }; + + let build_mani = BuildManifest::from_path(package_dir.join("package.json")) + .map_err(|e| { + NodeMaintainerError::BuildManifestReadError( + package_dir.join("package.json"), + e, + ) + })?; + + for (name, path) in &build_mani.bin { + let target_dir = target_dir.clone(); + let to = target_dir.join(name); + let from = package_dir.join(path); + let name = name.clone(); + async_std::task::spawn_blocking(move || { + // We only create a symlink if the target bin exists. + if from.symlink_metadata().is_ok() { + std::fs::create_dir_all(target_dir)?; + // TODO: use a DashMap here to prevent race conditions, maybe? + if let Ok(meta) = to.symlink_metadata() { + if meta.is_dir() { + std::fs::remove_dir_all(&to)?; + } else { + std::fs::remove_file(&to)?; + } + } + link_bin(&from, &to)?; + tracing::trace!( + "Linked bin for {} from {} to {}", + name, + from.display(), + to.display() + ); + } + Ok::<_, NodeMaintainerError>(()) + }) + .await?; + linked.fetch_add(1, atomic::Ordering::SeqCst); + } + Ok::<_, NodeMaintainerError>(()) + }) + .await?; + let linked = linked.load(atomic::Ordering::SeqCst); + tracing::debug!( + "Linked {linked} package bins in {}ms.", + start.elapsed().as_millis() + ); + Ok(linked) + } + async fn run_resolver( &mut self, lockfile: Option, @@ -1067,3 +1184,13 @@ fn supports_reflink(src_dir: &Path, dest_dir: &Path) -> bool { supports_reflink } + +fn link_bin(from: &Path, to: &Path) -> Result<(), NodeMaintainerError> { + #[cfg(windows)] + oro_shim_bin::shim_bin(from, to)?; + #[cfg(not(windows))] + { + std::os::unix::fs::symlink(from, to)?; + } + Ok(()) +} diff --git a/crates/oro-client/README.md b/crates/oro-client/README.md index 08fabd9e..cfc995dd 100644 --- a/crates/oro-client/README.md +++ b/crates/oro-client/README.md @@ -14,5 +14,5 @@ repository](https://github.com/orogene/orogene). ## License -This project is distributed under the [Apache 2.0 -License](https://github.com/orogene/orogene/blob/main/LICENSE) +For licensing information, please check [the LICENSE file in the Orogene +repository](https://github.com/orogene/orogene/blob/main/LICENSE). diff --git a/crates/oro-common/Cargo.toml b/crates/oro-common/Cargo.toml index ed210aa8..408431d5 100644 --- a/crates/oro-common/Cargo.toml +++ b/crates/oro-common/Cargo.toml @@ -14,10 +14,12 @@ derive_builder = { workspace = true } miette = { workspace = true } node-semver = { workspace = true } nom = { workspace = true } +pathdiff = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } thiserror = { workspace = true } url = { workspace = true, features = ["serde"] } +walkdir = { workspace = true } [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/crates/oro-common/README.md b/crates/oro-common/README.md index 2dac70ee..675ced16 100644 --- a/crates/oro-common/README.md +++ b/crates/oro-common/README.md @@ -15,5 +15,5 @@ repository](https://github.com/orogene/orogene). ## License -This project is distributed under the [Apache 2.0 -License](https://github.com/orogene/orogene/blob/main/LICENSE) +For licensing information, please check [the LICENSE file in the Orogene +repository](https://github.com/orogene/orogene/blob/main/LICENSE). diff --git a/crates/oro-common/src/build_manifest.rs b/crates/oro-common/src/build_manifest.rs new file mode 100644 index 00000000..37a72623 --- /dev/null +++ b/crates/oro-common/src/build_manifest.rs @@ -0,0 +1,138 @@ +use std::{ + collections::HashMap, + ffi::OsStr, + path::{Path, PathBuf}, +}; + +use serde::{Deserialize, Serialize}; +use walkdir::WalkDir; + +use crate::{Bin, Directories}; + +#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +struct RawBuildManifest { + #[serde(default)] + pub name: Option, + + #[serde(default)] + pub bin: Option, + + #[serde(default)] + pub directories: Option, + + #[serde(default)] + pub scripts: HashMap, +} + +/// Manifest intended for use with the `build` step in orogene's installer. It +/// reads and normalizes a package.json's bins (including the +/// `directories.bin` field), and its scripts object. +#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BuildManifest { + /// Mapping of bin name to the relative path to the script/binary. + #[serde(default)] + pub bin: HashMap, + + /// package.json scripts object. + #[serde(default)] + pub scripts: HashMap, +} + +impl BuildManifest { + /// Create a new [`BuildManifest`] from a given path, normalizing its bin + /// field (or its `directories.bin`) into a plain HashMap. + pub fn from_path(path: impl AsRef) -> std::io::Result { + let path = path.as_ref(); + let pkg_str = std::fs::read_to_string(path)?; + let raw: RawBuildManifest = serde_json::from_str(&pkg_str)?; + Self::normalize(raw) + } + + fn normalize(raw: RawBuildManifest) -> std::io::Result { + let mut bin_map = HashMap::new(); + if let Some(Bin::Hash(bins)) = raw.bin { + for (name, bin) in &bins { + let base = Path::new(name).file_name(); + if base.is_none() || base == Some(OsStr::new("")) { + continue; + } + let base = Path::new("/") + .join(Path::new( + &base + .unwrap() + .to_string_lossy() + .to_string() + .replace(['\\', ':'], "/"), + )) + .strip_prefix( + #[cfg(windows)] + "\\", + #[cfg(not(windows))] + "/", + ) + .expect("We added this ourselves") + .to_path_buf(); + if base == Path::new("") { + continue; + } + + let bin_target = Path::new("/") + .join(bin.to_string_lossy().to_string()) + .strip_prefix( + #[cfg(windows)] + "\\", + #[cfg(not(windows))] + "/", + ) + .expect("We added this ourselves") + .to_path_buf(); + if bin_target == Path::new("") { + continue; + } + + bin_map.insert(base.to_string_lossy().to_string(), bin_target); + } + } else if let Some(Bin::Str(bin)) = raw.bin { + let mut bin_map = HashMap::new(); + if let Some(name) = raw.name { + bin_map.insert(name, PathBuf::from(bin)); + } + } else if let Some(Bin::Array(bins)) = raw.bin { + let mut bin_map = HashMap::new(); + for bin in bins { + let name = bin + .as_path() + .file_name() + .ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidData, + format!("invalid bin name: {}", bin.to_string_lossy()), + ) + })? + .to_string_lossy() + .to_string(); + bin_map.insert(name, bin); + } + } else if let Some(Directories { + bin: Some(bin_dir), .. + }) = raw.directories + { + for entry in WalkDir::new(bin_dir) { + let entry = entry?; + let path = entry.path(); + if path.starts_with(".") { + continue; + } + if let Some(file_name) = path.file_name() { + bin_map.insert(file_name.to_string_lossy().to_string(), path.into()); + } + } + }; + Ok(Self { + bin: bin_map, + scripts: raw.scripts, + }) + } +} diff --git a/crates/oro-common/src/lib.rs b/crates/oro-common/src/lib.rs index 71c986bd..560be50b 100644 --- a/crates/oro-common/src/lib.rs +++ b/crates/oro-common/src/lib.rs @@ -1,9 +1,11 @@ //! General types and utilities for Orogene, including //! packument/package.json/manifest types. +pub use build_manifest::*; pub use manifest::Bin; pub use manifest::*; pub use packument::*; +mod build_manifest; mod manifest; mod packument; diff --git a/crates/oro-common/src/manifest.rs b/crates/oro-common/src/manifest.rs index 904a103f..cd1fb374 100644 --- a/crates/oro-common/src/manifest.rs +++ b/crates/oro-common/src/manifest.rs @@ -1,4 +1,7 @@ -use std::collections::{BTreeMap, HashMap}; +use std::{ + collections::{BTreeMap, HashMap}, + path::PathBuf, +}; use derive_builder::Builder; use node_semver::{Range, Version}; @@ -273,15 +276,16 @@ pub struct Person { #[derive(Clone, Debug, Default, PartialEq, Eq, Deserialize, Serialize)] pub struct Directories { - pub bin: Option, - pub man: Option, + pub bin: Option, + pub man: Option, } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(untagged)] pub enum Bin { Str(String), - Hash(HashMap), + Hash(HashMap), + Array(Vec), } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] diff --git a/crates/oro-config-derive/README.md b/crates/oro-config-derive/README.md index 522f619f..0d58b5db 100644 --- a/crates/oro-config-derive/README.md +++ b/crates/oro-config-derive/README.md @@ -14,5 +14,5 @@ repository](https://github.com/orogene/orogene). ## License -This project is distributed under the [Apache 2.0 -License](https://github.com/orogene/orogene/blob/main/LICENSE) +For licensing information, please check [the LICENSE file in the Orogene +repository](https://github.com/orogene/orogene/blob/main/LICENSE). diff --git a/crates/oro-config/README.md b/crates/oro-config/README.md index 04518c03..96be3513 100644 --- a/crates/oro-config/README.md +++ b/crates/oro-config/README.md @@ -14,5 +14,5 @@ repository](https://github.com/orogene/orogene). ## License -This project is distributed under the [Apache 2.0 -License](https://github.com/orogene/orogene/blob/main/LICENSE) +For licensing information, please check [the LICENSE file in the Orogene +repository](https://github.com/orogene/orogene/blob/main/LICENSE). diff --git a/crates/oro-package-spec/README.md b/crates/oro-package-spec/README.md index afc07948..6f6fcd2f 100644 --- a/crates/oro-package-spec/README.md +++ b/crates/oro-package-spec/README.md @@ -15,5 +15,5 @@ repository](https://github.com/orogene/orogene). ## License -This project is distributed under the [Apache 2.0 -License](https://github.com/orogene/orogene/blob/main/LICENSE) +For licensing information, please check [the LICENSE file in the Orogene +repository](https://github.com/orogene/orogene/blob/main/LICENSE). diff --git a/crates/oro-shim-bin/Cargo.toml b/crates/oro-shim-bin/Cargo.toml new file mode 100644 index 00000000..6a86a560 --- /dev/null +++ b/crates/oro-shim-bin/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "oro-shim-bin" +version = "0.3.13" +edition = "2021" +authors = ["Kat MarchΓ‘n "] +license = "ISC" +repository = "https://github.com/orogene/orogene" +homepage = "https://github.com/orogene/orogene" +readme = "README.md" + +[dependencies] +once_cell = { workspace = true } +pathdiff = { workspace = true } +regex = { workspace = true } + +[dev-dependencies] +insta = { workspace = true } +tempfile = { workspace = true } diff --git a/crates/oro-shim-bin/README.md b/crates/oro-shim-bin/README.md new file mode 100644 index 00000000..fbb2bf3d --- /dev/null +++ b/crates/oro-shim-bin/README.md @@ -0,0 +1,19 @@ +# `oro-shim-bin` + +Creates shims for package bins on Windows. Basically a Rust port of +https://github.com/npm/cmd-shim. + +## Orogene + +This package is part of [Orogene](https://orogene.dev), a package manager for +`node_modules/`. + +## Contributing + +For contributing guidelines, please see the [main orogenee +repository](https://github.com/orogene/orogene). + +## License + +For licensing information, please check [the LICENSE file in the Orogene +repository](https://github.com/orogene/orogene/blob/main/LICENSE). diff --git a/crates/oro-shim-bin/src/lib.rs b/crates/oro-shim-bin/src/lib.rs new file mode 100644 index 00000000..02a0fcf3 --- /dev/null +++ b/crates/oro-shim-bin/src/lib.rs @@ -0,0 +1,335 @@ +//! Creates shims for package bins on Windows. Basically a Rust port of +//! https://github.com/npm/cmd-shim. + +// The original project is licensed as follows: +// +// The ISC License +// +// Copyright (c) npm, Inc. and Contributors +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR +// IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +use std::path::Path; + +use once_cell::sync::Lazy; +use regex::Regex; + +static SHEBANG_REGEX: Lazy = Lazy::new(|| { + Regex::new(r"^#!\s*(?:/usr/bin/env\s+(?:-S\s+)?(?P(?:[^ \t=]+=[^ \t=]+\s+)*))?(?P[^ \t]+)(?P.*)$") + .unwrap() +}); + +static DOLLAR_EXPR_REGEX: Lazy = + Lazy::new(|| Regex::new(r"\$\{?(?P[^$@#?\- \t{}:]+)\}?").unwrap()); + +pub fn shim_bin(source: &Path, to: &Path) -> std::io::Result<()> { + // First, we blow away anything that already exists there. + // TODO: get rid of .expect()s? + let from = pathdiff::diff_paths(source, to.parent().expect("must have parent")) + .expect("paths should be diffable"); + cleanup_existing(to)?; + if let Ok(contents) = std::fs::read_to_string(source) { + let mut lines = contents.lines(); + if let Some(first_line) = lines.next() { + if let Some(captures) = SHEBANG_REGEX.captures(first_line.trim_end()) { + let vars = captures.name("vars").map(|m| m.as_str()); + let prog = captures.name("prog").map(|m| m.as_str()); + let args = captures.name("args").map(|m| m.as_str()); + return write_shim(&from, to, vars, prog, args); + } + } + } + write_shim(&from, to, None, None, None) +} + +fn cleanup_existing(to: &Path) -> std::io::Result<()> { + if let Ok(meta) = to.metadata() { + if meta.is_dir() { + std::fs::remove_dir_all(to)?; + } else { + std::fs::remove_file(to)?; + } + } + let cmd = to.with_extension("cmd"); + if let Ok(meta) = cmd.metadata() { + if meta.is_dir() { + std::fs::remove_dir_all(cmd)?; + } else { + std::fs::remove_file(cmd)?; + } + } + let ps1 = to.with_extension("ps1"); + if let Ok(meta) = ps1.metadata() { + if meta.is_dir() { + std::fs::remove_dir_all(ps1)?; + } else { + std::fs::remove_file(ps1)?; + } + } + Ok(()) +} + +fn write_shim( + from: &Path, + to: &Path, + vars: Option<&str>, + prog: Option<&str>, + args: Option<&str>, +) -> std::io::Result<()> { + write_cmd_shim(from, to, vars, prog, args)?; + write_sh_shim(from, to, vars, prog, args)?; + write_pwsh_shim(from, to, vars, prog, args)?; + Ok(()) +} + +fn write_cmd_shim( + from: &Path, + to: &Path, + vars: Option<&str>, + prog: Option<&str>, + args: Option<&str>, +) -> std::io::Result<()> { + let mut cmd = concat!( + "@ECHO off\r\n", + "GOTO start\r\n", + ":find_dp0\r\n", + "SET dp0=%~dp0\r\n", + "EXIT /b\r\n", + ":start\r\n", + "SETLOCAL\r\n", + "CALL :find_dp0\r\n" + ) + .to_string(); + + let target = format!( + "\"%dp0%\\{target}\" %*\r\n", + target = from.display().to_string().replace('/', "\\") + ); + if let Some(prog) = prog { + let args = if let Some(args) = args { + args.trim() + } else { + "" + }; + cmd.push_str(&convert_to_set_commands(vars.unwrap_or(""))); + cmd.push_str("\r\n"); + cmd.push_str(&format!("IF EXISTS \"%dp0%\\{prog}.exe\" (\r\n")); + cmd.push_str(&format!(" SET \"_prog=%dp0%\\{prog}.exe\"\r\n")); + cmd.push_str(") ELSE (\r\n"); + cmd.push_str(&format!( + " SET \"_prog={}\"\r\n", + prog.trim_start_matches('"').trim_end_matches('"') + )); + cmd.push_str(" SET PATHEXT=%PATHEXT:;.JS;=;%\r\n"); + cmd.push_str(")\r\n"); + cmd.push_str("\r\n"); + cmd.push_str("endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "); + cmd.push_str(&format!("\"%_prog%\" {args} \"%dp0%\\{target}\" %*\r\n",)); + } else { + cmd.push_str(&format!("\"%dp0%\\{target}\" %*\r\n",)); + } + + std::fs::write(to.with_extension("cmd"), cmd)?; + + Ok(()) +} + +fn write_sh_shim( + from: &Path, + to: &Path, + vars: Option<&str>, + prog: Option<&str>, + args: Option<&str>, +) -> std::io::Result<()> { + let mut sh = concat!( + "#!/bin/sh\n", + r#"basedir = $(dirname "$(echo "$0" | sed -e 's,\\,/,g')")"#, + "\n\n", + "case `uname` in\n", + " *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w \"$basedir\"`;;\n", + "esac\n\n" + ) + .to_string(); + + let args = args.unwrap_or(""); + let vars = vars.unwrap_or(""); + let target = from.display().to_string().replace('\\', "/"); + if let Some(prog) = prog { + let long_prog = format!("\"$basedir/{prog}\""); + let prog = prog.replace('\\', "/"); + sh.push_str(&format!("if [ -x {long_prog} ]; then\n")); + sh.push_str(&format!( + " exec {vars}{long_prog} {args} \"$basedir/{target}\" \"$@\"\n" + )); + sh.push_str("else \n"); + sh.push_str(&format!( + " exec {vars}{prog} {args} \"$basedir/{target}\" \"$@\"\n" + )); + sh.push_str("fi\n"); + } else { + sh.push_str(&format!("exec \"$basedir/{target}\" {args} \"$@\"\n")); + } + + std::fs::write(to, sh)?; + + Ok(()) +} + +fn write_pwsh_shim( + from: &Path, + to: &Path, + vars: Option<&str>, + prog: Option<&str>, + args: Option<&str>, +) -> std::io::Result<()> { + let mut pwsh = concat!( + "#!/usr/bin/env pwsh\n", + "$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent\n", + "\n", + "$exe=\"\"\n", + "if ($PSVersionTable.PSVersion -lt \"6.0\" -or $IsWindows) {\n", + " # Fix case when both the Windows and Linux builds of Node\n", + " # are installed in the same directory\n", + " $exe=\".exe\"\n", + "}\n" + ) + .to_string(); + + let args = args.unwrap_or(""); + let target = from.display().to_string().replace('\\', "/"); + if let Some(prog) = prog { + let long_prog = format!("\"$basedir/{prog}$exe\""); + let prog = format!("\"{}\"$exe", prog.replace('\\', "/")); + pwsh.push_str(&convert_to_env_commands(vars.unwrap_or(""))); + pwsh.push_str("$ret=0\n"); + pwsh.push_str(&format!("if (Test-Path {long_prog}) {{\n")); + pwsh.push_str(" # Support pipeline input\n"); + pwsh.push_str(" if ($MyInvocation.ExpectingInput) {\n"); + pwsh.push_str(&format!( + " $input | & {long_prog} {args} \"$basedir/{target}\" $args\n" + )); + pwsh.push_str(" } else {\n"); + pwsh.push_str(&format!( + " & {long_prog} {args} \"$basedir/{target}\" $args\n" + )); + pwsh.push_str(" }\n"); + pwsh.push_str(" $ret=$LASTEXITCODE\n"); + pwsh.push_str("} else {\n"); + pwsh.push_str(" # Support pipeline input\n"); + pwsh.push_str(" if ($MyInvocation.ExpectingInput) {\n"); + pwsh.push_str(&format!( + " $input | & {prog} {args} \"$basedir/{target}\" $args\n" + )); + pwsh.push_str(" } else {\n"); + pwsh.push_str(&format!( + " & {prog} {args} \"$basedir/{target}\" $args\n" + )); + pwsh.push_str(" }\n"); + pwsh.push_str(" $ret=$LASTEXITCODE\n"); + pwsh.push_str("}\n"); + pwsh.push_str("exit $ret\n"); + } else { + pwsh.push_str("# Support pipeline input\n"); + pwsh.push_str("if ($MyInvocation.ExpectingInput) {\n"); + pwsh.push_str(&format!(" $input | & \"$basedir/{target}\" $args\n")); + pwsh.push_str("} else {\n"); + pwsh.push_str(&format!(" & \"$basedir/{target}\" $args\n")); + pwsh.push_str("}\n"); + pwsh.push_str("exit $LASTEXITCODE\n"); + } + + std::fs::write(to.with_extension("ps1"), pwsh)?; + + Ok(()) +} + +fn convert_to_set_commands(variables: &str) -> String { + let mut var_declarations_as_batch = String::new(); + for var_str in variables.split_whitespace() { + let mut parts = var_str.splitn(2, '='); + if let (Some(key), Some(value)) = (parts.next(), parts.next()) { + var_declarations_as_batch.push_str(&convert_to_set_command(key, value)); + } + } + var_declarations_as_batch +} + +fn convert_to_set_command(key: &str, value: &str) -> String { + let key = key.trim(); + let value = value.trim(); + if key.is_empty() || value.is_empty() { + String::new() + } else { + format!("@SET {key}={}\r\n", replace_dollar_with_percent_pair(value)) + } +} + +fn replace_dollar_with_percent_pair(value: &str) -> String { + let mut result = String::new(); + let mut start_idx = 0; + for capture in DOLLAR_EXPR_REGEX.captures_iter(value) { + let mat = capture + .get(0) + .expect("If we had a capture, there should be a 0-match"); + result.push_str(&value[start_idx..mat.start()]); + result.push('%'); + result.push_str(&capture["var"]); + result.push('%'); + start_idx = mat.end(); + } + result.push_str(&value[start_idx..]); + result +} + +fn convert_to_env_commands(variables: &str) -> String { + let mut var_declarations_as_batch = String::new(); + for var_str in variables.split_whitespace() { + let mut parts = var_str.splitn(2, '='); + if let (Some(key), Some(value)) = (parts.next(), parts.next()) { + var_declarations_as_batch.push_str(&convert_to_env_command(key, value)); + } + } + var_declarations_as_batch +} + +fn convert_to_env_command(key: &str, value: &str) -> String { + let key = key.trim(); + let value = value.trim(); + if key.is_empty() || value.is_empty() { + String::new() + } else { + format!( + "$env:{key}=\"{}\"\n", + replace_with_string_interpolation(value) + ) + } +} + +fn replace_with_string_interpolation(value: &str) -> String { + let mut result = String::new(); + let mut start_idx = 0; + for capture in DOLLAR_EXPR_REGEX.captures_iter(value) { + let mat = capture + .get(0) + .expect("If we had a capture, there should be a 0-match"); + result.push_str(&value[start_idx..mat.start()]); + // This doesn't _necessarily_ have to be env:, but it's the most + // likely/sensible one, so we just go with it. + result.push_str("${env:"); + result.push_str(&capture["var"]); + result.push('}'); + start_idx = mat.end(); + } + result.push_str(&value[start_idx..]); + result.replace('\"', "`\"") +} diff --git a/crates/oro-shim-bin/tests/fixtures/from.env b/crates/oro-shim-bin/tests/fixtures/from.env new file mode 100644 index 00000000..482c45e1 --- /dev/null +++ b/crates/oro-shim-bin/tests/fixtures/from.env @@ -0,0 +1,2 @@ +#!/usr/bin/env node +console.log(/hi/) diff --git a/crates/oro-shim-bin/tests/fixtures/from.env.S b/crates/oro-shim-bin/tests/fixtures/from.env.S new file mode 100644 index 00000000..14041d21 --- /dev/null +++ b/crates/oro-shim-bin/tests/fixtures/from.env.S @@ -0,0 +1,2 @@ +#!/usr/bin/env -S node --expose_gc +gc() diff --git a/crates/oro-shim-bin/tests/fixtures/from.env.args b/crates/oro-shim-bin/tests/fixtures/from.env.args new file mode 100644 index 00000000..89627258 --- /dev/null +++ b/crates/oro-shim-bin/tests/fixtures/from.env.args @@ -0,0 +1,2 @@ +#!/usr/bin/env node --expose_gc +gc() diff --git a/crates/oro-shim-bin/tests/fixtures/from.env.multiple.variables b/crates/oro-shim-bin/tests/fixtures/from.env.multiple.variables new file mode 100644 index 00000000..11fcee00 --- /dev/null +++ b/crates/oro-shim-bin/tests/fixtures/from.env.multiple.variables @@ -0,0 +1 @@ +#!/usr/bin/env key=value key2=value2 node --flag-one --flag-two diff --git a/crates/oro-shim-bin/tests/fixtures/from.env.nospace b/crates/oro-shim-bin/tests/fixtures/from.env.nospace new file mode 100644 index 00000000..4095a390 --- /dev/null +++ b/crates/oro-shim-bin/tests/fixtures/from.env.nospace @@ -0,0 +1,2 @@ +#!/usr/bin/envnode +console.log(/hi/) diff --git a/crates/oro-shim-bin/tests/fixtures/from.env.variables b/crates/oro-shim-bin/tests/fixtures/from.env.variables new file mode 100644 index 00000000..60514bad --- /dev/null +++ b/crates/oro-shim-bin/tests/fixtures/from.env.variables @@ -0,0 +1 @@ +#!/usr/bin/env NODE_PATH=./lib:$NODE_PATH node diff --git a/crates/oro-shim-bin/tests/fixtures/from.exe b/crates/oro-shim-bin/tests/fixtures/from.exe new file mode 100644 index 00000000..86daf54c --- /dev/null +++ b/crates/oro-shim-bin/tests/fixtures/from.exe @@ -0,0 +1 @@ +exe diff --git a/crates/oro-shim-bin/tests/fixtures/from.sh b/crates/oro-shim-bin/tests/fixtures/from.sh new file mode 100644 index 00000000..0e712bb5 --- /dev/null +++ b/crates/oro-shim-bin/tests/fixtures/from.sh @@ -0,0 +1,2 @@ +#!/usr/bin/sh +echo hi diff --git a/crates/oro-shim-bin/tests/fixtures/from.sh.args b/crates/oro-shim-bin/tests/fixtures/from.sh.args new file mode 100644 index 00000000..3555a2fe --- /dev/null +++ b/crates/oro-shim-bin/tests/fixtures/from.sh.args @@ -0,0 +1,2 @@ +#!/usr/bin/sh -x +echo hi diff --git a/crates/oro-shim-bin/tests/shim_bin.rs b/crates/oro-shim-bin/tests/shim_bin.rs new file mode 100644 index 00000000..5e5c53f9 --- /dev/null +++ b/crates/oro-shim-bin/tests/shim_bin.rs @@ -0,0 +1,73 @@ +use std::path::PathBuf; + +fn fixtures() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") +} + +macro_rules! assert_fixture { + ($from:expr) => {{ + let shim_name = $from; + let tempdir = tempfile::tempdir_in(fixtures()).unwrap(); + let from = fixtures().join(&shim_name); + let to = tempdir.path().join("shim"); + oro_shim_bin::shim_bin(&from, &to).unwrap(); + insta::assert_snapshot!( + shim_name, + std::fs::read_to_string(&to).unwrap().replace('\r', "\\r") + ); + insta::assert_snapshot!( + format!("{shim_name}.ps1"), + std::fs::read_to_string(to.with_extension("ps1")) + .unwrap() + .replace('\r', "\\r") + ); + insta::assert_snapshot!( + format!("{shim_name}.cmd"), + std::fs::read_to_string(to.with_extension("cmd")) + .unwrap() + .replace('\r', "\\r") + ); + }}; +} + +#[test] +fn no_shebang() { + assert_fixture!("from.exe"); +} + +#[test] +fn env_shebang() { + assert_fixture!("from.env"); +} + +#[test] +fn env_shebang_with_args() { + assert_fixture!("from.env.args"); +} + +#[test] +fn env_shebang_vars() { + assert_fixture!("from.env.variables"); +} + +#[test] +fn explicit_shebang() { + assert_fixture!("from.sh"); +} + +#[test] +fn explicit_shebang_with_args() { + assert_fixture!("from.sh.args"); +} + +#[test] +fn multiple_variables() { + assert_fixture!("from.env.multiple.variables"); +} + +#[test] +fn shebang_with_env_s() { + assert_fixture!("from.env.S"); +} diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.S.cmd.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.S.cmd.snap new file mode 100644 index 00000000..6cb685be --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.S.cmd.snap @@ -0,0 +1,22 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"cmd\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +@ECHO off\r +GOTO start\r +:find_dp0\r +SET dp0=%~dp0\r +EXIT /b\r +:start\r +SETLOCAL\r +CALL :find_dp0\r +\r +IF EXISTS "%dp0%\node.exe" (\r + SET "_prog=%dp0%\node.exe"\r +) ELSE (\r + SET "_prog=node"\r + SET PATHEXT=%PATHEXT:;.JS;=;%\r +)\r +\r +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" --expose_gc "%dp0%\..\from.env.S" %*\r + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.S.ps1.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.S.ps1.snap new file mode 100644 index 00000000..1f07ed73 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.S.ps1.snap @@ -0,0 +1,33 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"ps1\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" --expose_gc "$basedir/../from.env.S" $args + } else { + & "$basedir/node$exe" --expose_gc "$basedir/../from.env.S" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node"$exe --expose_gc "$basedir/../from.env.S" $args + } else { + & "node"$exe --expose_gc "$basedir/../from.env.S" $args + } + $ret=$LASTEXITCODE +} +exit $ret + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.S.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.S.snap new file mode 100644 index 00000000..8fdfe791 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.S.snap @@ -0,0 +1,17 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(&to).unwrap().replace('\\r', \"\\\\r\")" +--- +#!/bin/sh +basedir = $(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" --expose_gc "$basedir/../from.env.S" "$@" +else + exec node --expose_gc "$basedir/../from.env.S" "$@" +fi + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.args.cmd.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.args.cmd.snap new file mode 100644 index 00000000..b4a7bd00 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.args.cmd.snap @@ -0,0 +1,22 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"cmd\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +@ECHO off\r +GOTO start\r +:find_dp0\r +SET dp0=%~dp0\r +EXIT /b\r +:start\r +SETLOCAL\r +CALL :find_dp0\r +\r +IF EXISTS "%dp0%\node.exe" (\r + SET "_prog=%dp0%\node.exe"\r +) ELSE (\r + SET "_prog=node"\r + SET PATHEXT=%PATHEXT:;.JS;=;%\r +)\r +\r +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" --expose_gc "%dp0%\..\from.env.args" %*\r + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.args.ps1.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.args.ps1.snap new file mode 100644 index 00000000..9b2f5e2c --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.args.ps1.snap @@ -0,0 +1,33 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"ps1\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" --expose_gc "$basedir/../from.env.args" $args + } else { + & "$basedir/node$exe" --expose_gc "$basedir/../from.env.args" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node"$exe --expose_gc "$basedir/../from.env.args" $args + } else { + & "node"$exe --expose_gc "$basedir/../from.env.args" $args + } + $ret=$LASTEXITCODE +} +exit $ret + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.args.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.args.snap new file mode 100644 index 00000000..8a9efcb3 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.args.snap @@ -0,0 +1,17 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(&to).unwrap().replace('\\r', \"\\\\r\")" +--- +#!/bin/sh +basedir = $(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" --expose_gc "$basedir/../from.env.args" "$@" +else + exec node --expose_gc "$basedir/../from.env.args" "$@" +fi + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.cmd.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.cmd.snap new file mode 100644 index 00000000..570b1296 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.cmd.snap @@ -0,0 +1,22 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"cmd\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +@ECHO off\r +GOTO start\r +:find_dp0\r +SET dp0=%~dp0\r +EXIT /b\r +:start\r +SETLOCAL\r +CALL :find_dp0\r +\r +IF EXISTS "%dp0%\node.exe" (\r + SET "_prog=%dp0%\node.exe"\r +) ELSE (\r + SET "_prog=node"\r + SET PATHEXT=%PATHEXT:;.JS;=;%\r +)\r +\r +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\from.env" %*\r + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.multiple.variables.cmd.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.multiple.variables.cmd.snap new file mode 100644 index 00000000..ad074f16 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.multiple.variables.cmd.snap @@ -0,0 +1,24 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"cmd\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +@ECHO off\r +GOTO start\r +:find_dp0\r +SET dp0=%~dp0\r +EXIT /b\r +:start\r +SETLOCAL\r +CALL :find_dp0\r +@SET key=value\r +@SET key2=value2\r +\r +IF EXISTS "%dp0%\node.exe" (\r + SET "_prog=%dp0%\node.exe"\r +) ELSE (\r + SET "_prog=node"\r + SET PATHEXT=%PATHEXT:;.JS;=;%\r +)\r +\r +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" --flag-one --flag-two "%dp0%\..\from.env.multiple.variables" %*\r + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.multiple.variables.ps1.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.multiple.variables.ps1.snap new file mode 100644 index 00000000..07486ad3 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.multiple.variables.ps1.snap @@ -0,0 +1,35 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"ps1\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$env:key="value" +$env:key2="value2" +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" --flag-one --flag-two "$basedir/../from.env.multiple.variables" $args + } else { + & "$basedir/node$exe" --flag-one --flag-two "$basedir/../from.env.multiple.variables" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node"$exe --flag-one --flag-two "$basedir/../from.env.multiple.variables" $args + } else { + & "node"$exe --flag-one --flag-two "$basedir/../from.env.multiple.variables" $args + } + $ret=$LASTEXITCODE +} +exit $ret + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.multiple.variables.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.multiple.variables.snap new file mode 100644 index 00000000..cd95bccb --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.multiple.variables.snap @@ -0,0 +1,17 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(&to).unwrap().replace('\\r', \"\\\\r\")" +--- +#!/bin/sh +basedir = $(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -x "$basedir/node" ]; then + exec key=value key2=value2 "$basedir/node" --flag-one --flag-two "$basedir/../from.env.multiple.variables" "$@" +else + exec key=value key2=value2 node --flag-one --flag-two "$basedir/../from.env.multiple.variables" "$@" +fi + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.ps1.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.ps1.snap new file mode 100644 index 00000000..5c59c407 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.ps1.snap @@ -0,0 +1,33 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"ps1\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../from.env" $args + } else { + & "$basedir/node$exe" "$basedir/../from.env" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node"$exe "$basedir/../from.env" $args + } else { + & "node"$exe "$basedir/../from.env" $args + } + $ret=$LASTEXITCODE +} +exit $ret + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.snap new file mode 100644 index 00000000..b42c04e5 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.snap @@ -0,0 +1,17 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(&to).unwrap().replace('\\r', \"\\\\r\")" +--- +#!/bin/sh +basedir = $(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -x "$basedir/node" ]; then + exec "$basedir/node" "$basedir/../from.env" "$@" +else + exec node "$basedir/../from.env" "$@" +fi + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.variables.cmd.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.variables.cmd.snap new file mode 100644 index 00000000..e9ee6151 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.variables.cmd.snap @@ -0,0 +1,23 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"cmd\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +@ECHO off\r +GOTO start\r +:find_dp0\r +SET dp0=%~dp0\r +EXIT /b\r +:start\r +SETLOCAL\r +CALL :find_dp0\r +@SET NODE_PATH=./lib:%NODE_PATH%\r +\r +IF EXISTS "%dp0%\node.exe" (\r + SET "_prog=%dp0%\node.exe"\r +) ELSE (\r + SET "_prog=node"\r + SET PATHEXT=%PATHEXT:;.JS;=;%\r +)\r +\r +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\from.env.variables" %*\r + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.variables.ps1.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.variables.ps1.snap new file mode 100644 index 00000000..54c3198b --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.variables.ps1.snap @@ -0,0 +1,34 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"ps1\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$env:NODE_PATH="./lib:${env:NODE_PATH}" +$ret=0 +if (Test-Path "$basedir/node$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/node$exe" "$basedir/../from.env.variables" $args + } else { + & "$basedir/node$exe" "$basedir/../from.env.variables" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "node"$exe "$basedir/../from.env.variables" $args + } else { + & "node"$exe "$basedir/../from.env.variables" $args + } + $ret=$LASTEXITCODE +} +exit $ret + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.variables.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.variables.snap new file mode 100644 index 00000000..66688924 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.env.variables.snap @@ -0,0 +1,17 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(&to).unwrap().replace('\\r', \"\\\\r\")" +--- +#!/bin/sh +basedir = $(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -x "$basedir/node" ]; then + exec NODE_PATH=./lib:$NODE_PATH "$basedir/node" "$basedir/../from.env.variables" "$@" +else + exec NODE_PATH=./lib:$NODE_PATH node "$basedir/../from.env.variables" "$@" +fi + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.exe.cmd.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.exe.cmd.snap new file mode 100644 index 00000000..1a3f8315 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.exe.cmd.snap @@ -0,0 +1,14 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"cmd\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +@ECHO off\r +GOTO start\r +:find_dp0\r +SET dp0=%~dp0\r +EXIT /b\r +:start\r +SETLOCAL\r +CALL :find_dp0\r +"%dp0%\..\from.exe" %*\r + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.exe.ps1.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.exe.ps1.snap new file mode 100644 index 00000000..e4328433 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.exe.ps1.snap @@ -0,0 +1,21 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"ps1\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +# Support pipeline input +if ($MyInvocation.ExpectingInput) { + $input | & "$basedir/../from.exe" $args +} else { + & "$basedir/../from.exe" $args +} +exit $LASTEXITCODE + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.exe.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.exe.snap new file mode 100644 index 00000000..fcc8bf34 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.exe.snap @@ -0,0 +1,13 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(&to).unwrap().replace('\\r', \"\\\\r\")" +--- +#!/bin/sh +basedir = $(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;; +esac + +exec "$basedir/../from.exe" "$@" + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.args.cmd.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.args.cmd.snap new file mode 100644 index 00000000..09a700dd --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.args.cmd.snap @@ -0,0 +1,22 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"cmd\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +@ECHO off\r +GOTO start\r +:find_dp0\r +SET dp0=%~dp0\r +EXIT /b\r +:start\r +SETLOCAL\r +CALL :find_dp0\r +\r +IF EXISTS "%dp0%\/usr/bin/sh.exe" (\r + SET "_prog=%dp0%\/usr/bin/sh.exe"\r +) ELSE (\r + SET "_prog=/usr/bin/sh"\r + SET PATHEXT=%PATHEXT:;.JS;=;%\r +)\r +\r +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" -x "%dp0%\..\from.sh.args" %*\r + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.args.ps1.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.args.ps1.snap new file mode 100644 index 00000000..2cfa31ff --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.args.ps1.snap @@ -0,0 +1,33 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"ps1\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir//usr/bin/sh$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir//usr/bin/sh$exe" -x "$basedir/../from.sh.args" $args + } else { + & "$basedir//usr/bin/sh$exe" -x "$basedir/../from.sh.args" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "/usr/bin/sh"$exe -x "$basedir/../from.sh.args" $args + } else { + & "/usr/bin/sh"$exe -x "$basedir/../from.sh.args" $args + } + $ret=$LASTEXITCODE +} +exit $ret + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.args.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.args.snap new file mode 100644 index 00000000..1730e6c5 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.args.snap @@ -0,0 +1,17 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(&to).unwrap().replace('\\r', \"\\\\r\")" +--- +#!/bin/sh +basedir = $(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -x "$basedir//usr/bin/sh" ]; then + exec "$basedir//usr/bin/sh" -x "$basedir/../from.sh.args" "$@" +else + exec /usr/bin/sh -x "$basedir/../from.sh.args" "$@" +fi + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.cmd.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.cmd.snap new file mode 100644 index 00000000..e7345eef --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.cmd.snap @@ -0,0 +1,22 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"cmd\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +@ECHO off\r +GOTO start\r +:find_dp0\r +SET dp0=%~dp0\r +EXIT /b\r +:start\r +SETLOCAL\r +CALL :find_dp0\r +\r +IF EXISTS "%dp0%\/usr/bin/sh.exe" (\r + SET "_prog=%dp0%\/usr/bin/sh.exe"\r +) ELSE (\r + SET "_prog=/usr/bin/sh"\r + SET PATHEXT=%PATHEXT:;.JS;=;%\r +)\r +\r +endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\from.sh" %*\r + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.ps1.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.ps1.snap new file mode 100644 index 00000000..bba5b5a6 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.ps1.snap @@ -0,0 +1,33 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(to.with_extension(\"ps1\")).unwrap().replace('\\r',\n \"\\\\r\")" +--- +#!/usr/bin/env pwsh +$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent + +$exe="" +if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) { + # Fix case when both the Windows and Linux builds of Node + # are installed in the same directory + $exe=".exe" +} +$ret=0 +if (Test-Path "$basedir//usr/bin/sh$exe") { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "$basedir//usr/bin/sh$exe" "$basedir/../from.sh" $args + } else { + & "$basedir//usr/bin/sh$exe" "$basedir/../from.sh" $args + } + $ret=$LASTEXITCODE +} else { + # Support pipeline input + if ($MyInvocation.ExpectingInput) { + $input | & "/usr/bin/sh"$exe "$basedir/../from.sh" $args + } else { + & "/usr/bin/sh"$exe "$basedir/../from.sh" $args + } + $ret=$LASTEXITCODE +} +exit $ret + diff --git a/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.snap b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.snap new file mode 100644 index 00000000..d6b2a7d4 --- /dev/null +++ b/crates/oro-shim-bin/tests/snapshots/shim_bin__from.sh.snap @@ -0,0 +1,17 @@ +--- +source: crates/node-maintainer/tests/shim_bin.rs +expression: "std::fs::read_to_string(&to).unwrap().replace('\\r', \"\\\\r\")" +--- +#!/bin/sh +basedir = $(dirname "$(echo "$0" | sed -e 's,\\,/,g')") + +case `uname` in + *CYGWIN*|*MINGW*|*MSYS*) basedir=`cygpath -w "$basedir"`;; +esac + +if [ -x "$basedir//usr/bin/sh" ]; then + exec "$basedir//usr/bin/sh" "$basedir/../from.sh" "$@" +else + exec /usr/bin/sh "$basedir/../from.sh" "$@" +fi + diff --git a/src/commands/restore.rs b/src/commands/restore.rs index ba1126b2..72328536 100644 --- a/src/commands/restore.rs +++ b/src/commands/restore.rs @@ -101,10 +101,12 @@ impl OroCommand for RestoreCmd { if !self.lockfile_only { self.prune(&emoji, &resolved_nm).await?; - self.extract(&emoji, &resolved_nm).await?; + if self.extract(&emoji, &resolved_nm).await? > 0 { + self.link_bins(&emoji, &resolved_nm).await?; + } } else { tracing::info!( - "{}Skipping prune and extract, only writing lockfile", + "{}Skipping installing node_modules/, only writing lockfile.", emoji.package() ); } @@ -165,7 +167,7 @@ impl RestoreCmd { Ok(resolved_nm) } - async fn prune(&self, emoji: &Emoji, maintainer: &NodeMaintainer) -> Result<()> { + async fn prune(&self, emoji: &Emoji, maintainer: &NodeMaintainer) -> Result { // Set up progress bar and timing stuff. let prune_time = std::time::Instant::now(); let prune_span = tracing::debug_span!("prune"); @@ -193,10 +195,10 @@ impl RestoreCmd { prune_time.elapsed().as_millis() as f32 / 1000.0 ); - Ok(()) + Ok(pruned) } - async fn extract(&self, emoji: &Emoji, maintainer: &NodeMaintainer) -> Result<()> { + async fn extract(&self, emoji: &Emoji, maintainer: &NodeMaintainer) -> Result { // Set up progress bar and timing stuff. let extract_time = std::time::Instant::now(); let extract_span = tracing::debug_span!("extract"); @@ -225,6 +227,19 @@ impl RestoreCmd { extract_time.elapsed().as_millis() as f32 / 1000.0 ); + Ok(extracted) + } + + async fn link_bins(&self, emoji: &Emoji, maintainer: &NodeMaintainer) -> Result<()> { + let link_time = std::time::Instant::now(); + let linked = maintainer.link_bins().await?; + tracing::info!( + "{}Linked {linked} package bin{} in {}s.", + emoji.link(), + if linked == 1 { "" } else { "s" }, + link_time.elapsed().as_millis() as f32 / 1000.0 + ); + Ok(()) } } @@ -259,12 +274,21 @@ impl Emoji { Self(use_emoji) } + const LINK: &'static str = "πŸ”— "; const PACKAGE: &'static str = "πŸ“¦ "; const MAGNIFYING_GLASS: &'static str = "πŸ” "; const BROOM: &'static str = "🧹 "; const WRITING: &'static str = "πŸ“ "; const TADA: &'static str = "πŸŽ‰ "; + fn link(&self) -> &'static str { + if self.0 { + Self::LINK + } else { + "" + } + } + fn package(&self) -> &'static str { if self.0 { Self::PACKAGE diff --git a/src/commands/view.rs b/src/commands/view.rs index 60b548e5..d4644327 100644 --- a/src/commands/view.rs +++ b/src/commands/view.rs @@ -137,6 +137,13 @@ impl OroCommand for ViewCmd { let bins = match bin { Bin::Str(_) => vec![name.clone().unwrap_or_else(|| String::from(""))], Bin::Hash(bins) => bins.keys().cloned().collect::>(), + Bin::Array(bins) => bins + .iter() + .filter_map(|bin| { + bin.file_name() + .map(|name| name.to_string_lossy().to_string()) + }) + .collect::>(), }; println!( "bins: {}\n",