├── .github └── workflows │ ├── cargo-build.yml │ ├── pull-request.yml │ └── windows-installer.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── installer ├── information.txt └── installer.iss ├── qpm.schema.json ├── rust-toolchain.toml ├── rustfmt.toml ├── src ├── commands │ ├── cache.rs │ ├── clear.rs │ ├── collapse.rs │ ├── config │ │ ├── cache.rs │ │ ├── mod.rs │ │ ├── ndkpath.rs │ │ ├── publish.rs │ │ ├── symlink.rs │ │ ├── timeout.rs │ │ └── token.rs │ ├── dependency.rs │ ├── install.rs │ ├── list │ │ ├── extra_properties.rs │ │ ├── mod.rs │ │ ├── packages.rs │ │ └── versions.rs │ ├── mod.rs │ ├── package │ │ ├── create.rs │ │ ├── edit.rs │ │ ├── edit_extra.rs │ │ └── mod.rs │ ├── publish │ │ └── mod.rs │ ├── qmod │ │ ├── edit.rs │ │ └── mod.rs │ └── restore.rs ├── data │ ├── config.rs │ ├── dependency │ │ ├── dependency_internal.rs │ │ ├── mod.rs │ │ └── shared_dependency.rs │ ├── file_repository.rs │ ├── mod.rs │ ├── mod_json.rs │ ├── package │ │ ├── compile_options.rs │ │ ├── mod.rs │ │ ├── package_config.rs │ │ └── shared_package_config.rs │ ├── qpackages.rs │ └── repo │ │ ├── local_provider.rs │ │ ├── mod.rs │ │ ├── multi_provider.rs │ │ └── qpm_provider.rs ├── main.rs ├── resolver │ ├── mod.rs │ ├── provider.rs │ └── semver.rs └── utils │ ├── git.rs │ ├── mod.rs │ ├── network.rs │ └── toggle.rs └── workspace.code-workspace /.github/workflows/cargo-build.yml: -------------------------------------------------------------------------------- 1 | # Runs on push to main, basically the "release" version since we don't really do releases (that's bad right) 2 | name: Cargo Build 3 | 4 | on: 5 | workflow_dispatch: 6 | push: 7 | branches: [main] 8 | paths-ignore: 9 | - 'README.md' 10 | - '**.json' 11 | - '**.yml' 12 | - 'LICENSE' 13 | - '!.github/workflows/cargo-build.yml' 14 | - 'installer/**' 15 | 16 | jobs: 17 | build: 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | os: [ubuntu-latest, windows-latest, macOS-latest] 22 | include: 23 | - os: ubuntu-latest 24 | file-name: qpm-rust 25 | prefix: linux 26 | 27 | - os: macOS-latest 28 | file-name: qpm-rust 29 | prefix: macos 30 | 31 | - os: windows-latest 32 | file-name: qpm-rust.exe 33 | prefix: windows 34 | 35 | steps: 36 | - uses: actions/checkout@v2 37 | 38 | - uses: actions-rs/toolchain@v1 39 | with: 40 | toolchain: nightly 41 | - uses: actions/cache@v2 42 | with: 43 | path: | 44 | ~/.cargo/bin/ 45 | ~/.cargo/registry/index/ 46 | ~/.cargo/registry/cache/ 47 | ~/.cargo/git/db/ 48 | target/ 49 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 50 | 51 | - name: Get libdbus if Ubuntu 52 | if: ${{ matrix.os == 'ubuntu-latest' }} 53 | run: | 54 | sudo apt-get install -y libdbus-1-dev 55 | 56 | - name: Cargo build 57 | uses: actions-rs/cargo@v1 58 | with: 59 | command: build 60 | args: --release 61 | 62 | - name: Upload executable 63 | uses: actions/upload-artifact@v2 64 | with: 65 | name: ${{ matrix.prefix }}-${{ matrix.file-name }} 66 | path: ./target/release/${{ matrix.file-name }} 67 | if-no-files-found: error 68 | -------------------------------------------------------------------------------- /.github/workflows/pull-request.yml: -------------------------------------------------------------------------------- 1 | # This workflow will be used to verify a pull request, this is to make sure that on pull requests it doesn't mess with the already available binary on the main workflow 2 | name: Pull Request Test 3 | 4 | on: 5 | pull_request: 6 | branches: [main] 7 | paths-ignore: 8 | - 'README.md' 9 | - '**.json' 10 | - '**.yml' 11 | - 'LICENSE' 12 | - '!.github/workflows/pull-request.yml' 13 | - 'installer/**' 14 | jobs: 15 | build: 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, windows-latest, macOS-latest] 20 | include: 21 | - os: ubuntu-latest 22 | file-name: qpm-rust 23 | prefix: linux 24 | 25 | - os: macOS-latest 26 | file-name: qpm-rust 27 | prefix: macos 28 | 29 | - os: windows-latest 30 | file-name: qpm-rust.exe 31 | prefix: windows 32 | 33 | steps: 34 | - uses: actions/checkout@v2 35 | 36 | - uses: actions-rs/toolchain@v1 37 | with: 38 | toolchain: nightly 39 | - uses: actions/cache@v2 40 | with: 41 | path: | 42 | ~/.cargo/bin/ 43 | ~/.cargo/registry/index/ 44 | ~/.cargo/registry/cache/ 45 | ~/.cargo/git/db/ 46 | target/ 47 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 48 | 49 | - name: Get libdbus if Ubuntu 50 | if: ${{ matrix.os == 'ubuntu-latest' }} 51 | run: | 52 | sudo apt-get install -y libdbus-1-dev 53 | 54 | - name: Cargo build 55 | uses: actions-rs/cargo@v1 56 | with: 57 | command: build 58 | args: --release 59 | 60 | - name: Upload executable 61 | uses: actions/upload-artifact@v2 62 | with: 63 | name: ${{ matrix.prefix }}-${{ matrix.file-name }} 64 | path: ./target/release/${{ matrix.file-name }} 65 | if-no-files-found: error 66 | -------------------------------------------------------------------------------- /.github/workflows/windows-installer.yml: -------------------------------------------------------------------------------- 1 | name: Windows Installer 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | paths-ignore: 7 | - 'README.md' 8 | - '**.json' 9 | - '**.yml' 10 | - 'LICENSE' 11 | - '!.github/workflows/windows-installer.yml' 12 | 13 | jobs: 14 | build: 15 | name: Build installer on windows 16 | runs-on: windows-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - uses: actions-rs/toolchain@v1 21 | with: 22 | toolchain: nightly 23 | - uses: actions/cache@v2 24 | with: 25 | path: | 26 | ~/.cargo/bin/ 27 | ~/.cargo/registry/index/ 28 | ~/.cargo/registry/cache/ 29 | ~/.cargo/git/db/ 30 | target/ 31 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 32 | 33 | - name: Download Inno Setup 34 | uses: suisei-cn/actions-download-file@v1 35 | with: 36 | url: https://jrsoftware.org/download.php/is.exe 37 | target: ../ 38 | 39 | - name: Install Inno Setup 40 | run: '../is.exe /VERYSILENT /NORESTART /ALLUSERS' 41 | 42 | - name: Cargo build 43 | uses: actions-rs/cargo@v1 44 | with: 45 | command: build 46 | args: --release 47 | 48 | - name: Compile Installer 49 | run: '& "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" /f installer/installer.iss' 50 | 51 | - name: Artifact Upload 52 | uses: actions/upload-artifact@v2 53 | with: 54 | name: qpm-rust-installer.exe 55 | path: ./installer/qpm-rust-installer.exe -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | # Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # file generated by testing the program, shouldn't be pushed 13 | *.json 14 | 15 | # this is the schema file, this can be pushed! 16 | !qpm.schema.json 17 | 18 | *.txt 19 | *.mk 20 | *.ps1 -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "qpm-rust" 3 | version = "0.1.0" 4 | authors = [ 5 | "Adam ? ", 6 | "RedBrumbler ", 7 | "Raphaël Thériault ", 8 | ] 9 | edition = "2021" 10 | 11 | [dependencies] 12 | serde = { version = "1.0", features = ["derive"] } 13 | serde_json = "1.0" 14 | clap = { version = "3.1", features = ["derive"]} 15 | reqwest = { version = "0.11", features = ["blocking", "json"] } 16 | semver = { version = "1.0", features = ["serde"] } 17 | cursed-semver-parser = { git = "https://github.com/raftario/cursed-semver-parser.git", features = [ 18 | "serde", 19 | ] } 20 | pubgrub = "0.2.1" 21 | owo-colors = "3" 22 | atomic_refcell = "0.1.8" 23 | dirs = "4.0.0" 24 | keyring = "1" 25 | duct = "0.13.5" 26 | zip = "0.6" 27 | remove_dir_all = "0.7.0" 28 | walkdir = "2.3.2" 29 | symlink = "0.1.0" 30 | fs_extra = "1.2.0" 31 | itertools = "0.10.3" 32 | 33 | [profile.release] 34 | opt-level = 3 35 | lto = true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2022 Adam ?, RedBrumbler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | a -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deprecation notice 2 | This version of qpm is deprecated, if you are looking for qpm for quest modding purposes you should get it from the new location at https://github.com/QuestPackageManager/QPM.CLI 3 | 4 | # QuestPackageManager-Rust 5 | 6 | QPM but rusty, this is a program that handles package downloading for quest modding, allowing modders to create packages to provide functionalities for mods. 7 | 8 | # Building the program 9 | 10 | First, make sure you have [Installed Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) 11 | 12 | Open a command line / Powershell window 13 | 14 | clone the repo 15 | 16 | ``` 17 | git clone https://github.com/RedBrumbler/QuestPackageManager-Rust.git 18 | ``` 19 | 20 | go into the folder 21 | 22 | ``` 23 | cd QuestPackageManager-Rust 24 | ``` 25 | 26 | run the build command 27 | 28 | ``` 29 | cargo build --release 30 | ``` 31 | 32 | the executable should now be found in `./target/release/qpm-rust` 33 | 34 | if you want to use it like this, add it to path or move it to a place of your choosing that's already added to path. 35 | 36 | # Downloading the program 37 | 38 | Download qpm-rust from the [latest github actions build](https://github.com/RedBrumbler/QuestPackageManager-Rust/actions/workflows/cargo-build.yml), or if you're on windows [Download the installer](https://github.com/RedBrumbler/QuestPackageManager-Rust/actions/workflows/windows-installer.yml) from the latest action since that's easier. then you can also disregard the next instructions unless you absolutely want to get the executable yourself. 39 | 40 | if nothing shows up, make sure you're logged in, if nothing still shows up we might have to manually make it generate a new version 41 | Make sure you select the appropriate platform for your OS! 42 | 43 | Now that you have this downloaded, you can unzip it and store it where you want it. I keep my qpm-rust executable in `S:/QPM-RUST` (irrelevant but just an example) 44 | 45 | Now you want to add the program to path so that you can run it from anywhere, your best bet is to just google how to do this for your platform. just make sure that after you add it to path you restart any terminals you had left open. 46 | 47 | Now to check if you installed it right you can run 48 | 49 | ``` 50 | qpm-rust --help 51 | ``` 52 | 53 | and you'll get a handy help message to get you on your way to using this program 54 | 55 | # Requirements for using the program properly 56 | 57 | To use the program properly with quest modding, you might also want to install the following programs, and make sure they are on path: 58 | - [Git](https://git-scm.com/downloads), used for downloading the repos that packages are stored on 59 | - [CMake](https://cmake.org/install/), generally used for compiling mods that use qpm-rust 60 | - [Ninja](https://ninja-build.org/), Used for building mods with cmake 61 | -------------------------------------------------------------------------------- /installer/information.txt: -------------------------------------------------------------------------------- 1 | qpm-rust is the rust version of qpm, a package manager for quest modding libraries -------------------------------------------------------------------------------- /installer/installer.iss: -------------------------------------------------------------------------------- 1 | ; Script generated by the Inno Setup Script Wizard. 2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! 3 | 4 | #define MyAppName "qpm-rust" 5 | #define MyAppVersion "0.1.0" 6 | #define MyAppPublisher "RedBrumbler" 7 | #define MyAppURL "https://github.com/RedBrumbler/QuestPackageManager-Rust" 8 | #define MyAppExeName "qpm-rust.exe" 9 | 10 | [Setup] 11 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. 12 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) 13 | AppId={{D942E320-514B-4137-998F-B5E0C3FD2EDB} 14 | AppName={#MyAppName} 15 | AppVersion={#MyAppVersion} 16 | ;AppVerName={#MyAppName} {#MyAppVersion} 17 | AppPublisher={#MyAppPublisher} 18 | AppPublisherURL={#MyAppURL} 19 | AppSupportURL={#MyAppURL} 20 | AppUpdatesURL={#MyAppURL} 21 | DefaultDirName={autopf}\{#MyAppName} 22 | DefaultGroupName={#MyAppName} 23 | DisableProgramGroupPage=yes 24 | LicenseFile=..\LICENSE 25 | InfoAfterFile=.\information.txt 26 | ; Uncomment the following line to run in non administrative install mode (install for current user only.) 27 | ;PrivilegesRequired=lowest 28 | PrivilegesRequiredOverridesAllowed=dialog 29 | OutputDir=.\ 30 | OutputBaseFilename=qpm-rust-installer 31 | Compression=lzma 32 | SolidCompression=yes 33 | WizardStyle=modern 34 | ArchitecturesInstallIn64BitMode=x64 35 | 36 | ; Taken from https://stackoverflow.com/a/46609047/11395424. Credit to author Wojciech Mleczek 37 | ; Adds qpm-rust to PATH on installation and remove on uninstallation 38 | [Code] 39 | const SystemEnvironmentKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment'; 40 | const UserEnvironmentKey = 'Environment'; 41 | function GetPathValue(var ResultStr: String): Boolean; 42 | begin 43 | if IsAdmin() 44 | then Result := RegQueryStringValue(HKEY_LOCAL_MACHINE, SystemEnvironmentKey, 'Path', ResultStr) 45 | else Result := RegQueryStringValue(HKEY_CURRENT_USER, UserEnvironmentKey, 'Path', ResultStr); 46 | end; 47 | function SetPathValue(Paths: string): Boolean; 48 | begin 49 | if IsAdmin() 50 | then Result := RegWriteStringValue(HKEY_LOCAL_MACHINE, SystemEnvironmentKey, 'Path', Paths) 51 | else Result := RegWriteStringValue(HKEY_CURRENT_USER, UserEnvironmentKey, 'Path', Paths) 52 | end; 53 | procedure EnvAddPath(Path: string); 54 | var 55 | Paths: string; 56 | begin 57 | { Retrieve current path (use empty string if entry not exists) } 58 | if not GetPathValue(Paths) 59 | then Paths := ''; 60 | { Skip if string already found in path } 61 | if Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';') > 0 then exit; 62 | { App string to the end of the path variable } 63 | Paths := Paths + ';'+ Path +';' 64 | { Overwrite (or create if missing) path environment variable } 65 | if SetPathValue(Paths) 66 | then Log(Format('The [%s] added to PATH: [%s]', [Path, Paths])) 67 | else Log(Format('Error while adding the [%s] to PATH: [%s]', [Path, Paths])); 68 | end; 69 | procedure EnvRemovePath(Path: string); 70 | var 71 | Paths: string; 72 | P: Integer; 73 | begin 74 | { Skip if registry entry not exists } 75 | if not GetPathValue(Paths) then 76 | exit; 77 | { Skip if string not found in path } 78 | P := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';'); 79 | if P = 0 then exit; 80 | { Update path variable } 81 | Delete(Paths, P - 1, Length(Path) + 1); 82 | { Overwrite path environment variable } 83 | if SetPathValue(Paths) 84 | then Log(Format('The [%s] removed from PATH: [%s]', [Path, Paths])) 85 | else Log(Format('Error while removing the [%s] from PATH: [%s]', [Path, Paths])); 86 | end; 87 | procedure CurStepChanged(CurStep: TSetupStep); 88 | begin 89 | if CurStep = ssPostInstall 90 | then EnvAddPath(ExpandConstant('{app}')); 91 | end; 92 | procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); 93 | begin 94 | if CurUninstallStep = usPostUninstall 95 | then EnvRemovePath(ExpandConstant('{app}')); 96 | end; 97 | 98 | [Languages] 99 | Name: "english"; MessagesFile: "compiler:Default.isl" 100 | 101 | [Files] 102 | Source: "..\target\release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion 103 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files 104 | 105 | [Icons] 106 | Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" 107 | 108 | [UninstallRun] 109 | 110 | [UninstallDelete] 111 | Type: filesandordirs; Name: "{app}" -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "nightly-2022-07-28" 3 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | group_imports = "StdExternalCrate" 3 | -------------------------------------------------------------------------------- /src/commands/cache.rs: -------------------------------------------------------------------------------- 1 | use std::io::{Read, Write}; 2 | 3 | use clap::Subcommand; 4 | use owo_colors::OwoColorize; 5 | use remove_dir_all::remove_dir_contents; 6 | use walkdir::WalkDir; 7 | 8 | use crate::data::{config::Config, package::PackageConfig}; 9 | 10 | #[derive(clap::Args, Debug, Clone)] 11 | 12 | pub struct Cache { 13 | /// Clear the cache 14 | #[clap(subcommand)] 15 | pub op: CacheOperation, 16 | } 17 | 18 | #[derive(Subcommand, Debug, Clone)] 19 | pub enum CacheOperation { 20 | /// Clear the cache 21 | Clear, 22 | /// Lists versions for each cached package 23 | List, 24 | /// Shows you the current cache path 25 | Path, 26 | /// Fixes some dependencies that use technically wrong include paths 27 | LegacyFix, 28 | } 29 | 30 | pub fn execute_cache_operation(operation: Cache) { 31 | match operation.op { 32 | CacheOperation::Clear => clear(), 33 | CacheOperation::List => list(), 34 | CacheOperation::Path => path(), 35 | CacheOperation::LegacyFix => legacy_fix(), 36 | } 37 | } 38 | 39 | fn clear() { 40 | let config = Config::read_combine(); 41 | let path = config.cache.unwrap(); 42 | remove_dir_contents(path).expect("Failed to remove cached folders"); 43 | } 44 | 45 | fn path() { 46 | let config = Config::read_combine(); 47 | println!( 48 | "Config path is: {}", 49 | config.cache.unwrap().display().bright_yellow() 50 | ); 51 | } 52 | 53 | fn list() { 54 | let config = Config::read_combine(); 55 | let path = config.cache.unwrap(); 56 | 57 | for dir in WalkDir::new(&path).max_depth(2).min_depth(1) { 58 | let unwrapped = dir.unwrap(); 59 | if unwrapped.depth() == 1 { 60 | println!( 61 | "package {}:", 62 | unwrapped.file_name().to_string_lossy().bright_red() 63 | ); 64 | } else { 65 | println!( 66 | " - {}", 67 | unwrapped.file_name().to_string_lossy().bright_green() 68 | ); 69 | } 70 | } 71 | } 72 | 73 | fn legacy_fix() { 74 | for entry in WalkDir::new(Config::read_combine().cache.unwrap()) 75 | .min_depth(2) 76 | .max_depth(2) 77 | { 78 | let path = entry.unwrap().into_path().join("src"); 79 | println!("{}", path.display()); 80 | let qpm_path = path.join("qpm.json"); 81 | if !qpm_path.exists() { 82 | continue; 83 | } 84 | let shared_path = path.join(PackageConfig::read_path(qpm_path).shared_dir); 85 | 86 | for entry in WalkDir::new(shared_path) { 87 | let entry_path = entry.unwrap().into_path(); 88 | if entry_path.is_file() { 89 | let mut file = match std::fs::File::open(&entry_path) { 90 | Ok(o) => o, 91 | Err(e) => panic!( 92 | "Opening file {} to read failed: {}", 93 | entry_path.display().bright_yellow(), 94 | e 95 | ), 96 | }; 97 | 98 | let mut buf: String = "".to_string(); 99 | match file.read_to_string(&mut buf) { 100 | Ok(_) => {} 101 | Err(_e) => { 102 | #[cfg(debug_assertions)] 103 | println!( 104 | "reading file {} to string failed: {}", 105 | entry_path.display().bright_yellow(), 106 | _e 107 | ); 108 | continue; 109 | } 110 | }; 111 | fs_extra::file::remove(&entry_path).unwrap_or_else(|_| { 112 | panic!( 113 | "removing file {} failed", 114 | entry_path.display().bright_yellow() 115 | ) 116 | }); 117 | let mut file = std::fs::File::create(&entry_path).unwrap_or_else(|_| { 118 | panic!( 119 | "opening file {} to write failed", 120 | entry_path.display().bright_yellow() 121 | ) 122 | }); 123 | file.write_all( 124 | buf.replace( 125 | "#include \"extern/beatsaber-hook/", 126 | "#include \"beatsaber-hook/", 127 | ) 128 | .as_bytes(), 129 | ) 130 | .unwrap_or_else(|_| { 131 | panic!( 132 | "writing file {} failed", 133 | entry_path.display().bright_yellow() 134 | ) 135 | }); 136 | } 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/commands/clear.rs: -------------------------------------------------------------------------------- 1 | use owo_colors::OwoColorize; 2 | use remove_dir_all::remove_dir_all; 3 | use walkdir::WalkDir; 4 | 5 | use crate::data::package::PackageConfig; 6 | 7 | pub fn execute_clear_operation() { 8 | remove_dependencies_dir(); 9 | std::fs::remove_file("qpm.shared.json").ok(); 10 | std::fs::remove_file("extern.cmake").ok(); 11 | std::fs::remove_file("qpm_defines.cmake").ok(); 12 | std::fs::remove_file("mod.json").ok(); 13 | } 14 | 15 | pub fn remove_dependencies_dir() { 16 | let package = PackageConfig::read(); 17 | let extern_path = std::path::Path::new(&package.dependencies_dir); 18 | 19 | if !extern_path.exists() { 20 | return; 21 | } 22 | 23 | let current_path = std::path::Path::new("."); 24 | 25 | 26 | let extern_path_canonical = extern_path.canonicalize().expect("Extern path not found"); 27 | 28 | // If extern is "" or ".." etc. or is a path that is an 29 | // ancestor of the current directory, fail fast 30 | if current_path.canonicalize().expect("No current path found? what?") == extern_path_canonical 31 | || current_path 32 | .ancestors() 33 | .any(|path| path.exists() && path.canonicalize().unwrap_or_else(|e| panic!("Ancestor path {e:?} not found?")) == extern_path_canonical) 34 | { 35 | panic!( 36 | "Current path {:?} would be deleted since extern path {:?} is an ancestor or empty", 37 | current_path.canonicalize().bright_yellow(), extern_path.bright_red() 38 | ); 39 | } 40 | 41 | 42 | for entry in WalkDir::new(extern_path_canonical).min_depth(1) { 43 | let path = entry.unwrap().into_path(); 44 | #[cfg(debug_assertions)] 45 | println!("Path: {}", path.display().bright_yellow()); 46 | if path.is_symlink() { 47 | if path.is_dir() { 48 | #[cfg(debug_assertions)] 49 | println!("Was symlink dir!"); 50 | if let Err(e) = symlink::remove_symlink_dir(&path) { 51 | println!( 52 | "Failed to remove symlink for directory {}: {}", 53 | path.display().bright_yellow(), 54 | e 55 | ); 56 | } 57 | } else if path.is_file() { 58 | #[cfg(debug_assertions)] 59 | println!("Was symlink file!"); 60 | if let Err(e) = symlink::remove_symlink_file(&path) { 61 | println!( 62 | "Failed to remove symlink for file {}: {}", 63 | path.display().bright_yellow(), 64 | e 65 | ); 66 | } 67 | } else { 68 | #[cfg(debug_assertions)] 69 | println!("Was broken symlink!"); 70 | if let Err(ed) = std::fs::remove_dir(&path) { 71 | if let Err(ef) = std::fs::remove_file(&path) { 72 | println!( 73 | "Failed to remove broken symlink for {}:\nAttempt 1 (dir):{}\nAttempt 2 (file):{}", 74 | path.display().bright_yellow(), 75 | ed, 76 | ef 77 | ); 78 | } 79 | } 80 | } 81 | } 82 | } 83 | 84 | remove_dir_all(&package.dependencies_dir).expect("Failed to remove cached folders"); 85 | } 86 | -------------------------------------------------------------------------------- /src/commands/collapse.rs: -------------------------------------------------------------------------------- 1 | use owo_colors::OwoColorize; 2 | 3 | use crate::data::package; 4 | 5 | pub fn execute_collapse_operation() { 6 | let package = package::PackageConfig::read(); 7 | let resolved = package.resolve(); 8 | for shared_package in resolved { 9 | println!( 10 | "{}: ({}) --> {} ({} restored dependencies)", 11 | &shared_package.config.info.id.bright_red(), 12 | /*&dep.dependency.version_range.bright_blue(),*/ "?".bright_blue(), 13 | &shared_package.config.info.version.bright_green(), 14 | shared_package 15 | .restored_dependencies 16 | .len() 17 | .to_string() 18 | .yellow() 19 | ); 20 | 21 | for shared_dep in shared_package.restored_dependencies.iter() { 22 | println!( 23 | " - {}: ({}) --> {}", 24 | &shared_dep.dependency.id, 25 | &shared_dep.dependency.version_range, 26 | &shared_dep.version 27 | ); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/config/cache.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{Args, Subcommand}; 4 | use owo_colors::OwoColorize; 5 | 6 | use crate::data::config::Config as AppConfig; 7 | 8 | #[derive(Args, Debug, Clone)] 9 | pub struct Cache { 10 | #[clap(subcommand)] 11 | pub op: CacheOperation, 12 | } 13 | 14 | #[derive(Subcommand, Debug, Clone)] 15 | pub enum CacheOperation { 16 | /// Gets or sets the path to place the QPM Cache 17 | Path(CacheSetPathOperation), 18 | } 19 | 20 | #[derive(Args, Debug, Clone)] 21 | pub struct CacheSetPathOperation { 22 | pub path: Option, 23 | } 24 | 25 | pub fn execute_cache_config_operation(config: &mut AppConfig, operation: Cache) -> bool { 26 | match operation.op { 27 | CacheOperation::Path(p) => { 28 | if let Some(path) = p.path { 29 | let path_data = path.as_path(); 30 | // if it's relative, that is bad, do not accept! 31 | if path_data.is_relative() { 32 | println!( 33 | "Path input {} is relative, this is not allowed! pass in absolute paths!", 34 | path.display().bright_yellow() 35 | ); 36 | // if it's a path to a file, that's not usable, do not accept! 37 | } else if path_data.is_file() { 38 | println!( 39 | "Path input {} is a file, this is not allowed! pass in a folder!", 40 | path.display().bright_yellow() 41 | ); 42 | } else { 43 | // if we can not create the folder, that is bad, do not accept! 44 | if let Err(err) = std::fs::create_dir_all(&path) { 45 | println!("Creating dir {} failed! does qpm have permission to create that directory?", path.display().bright_yellow()); 46 | println!("Not setting cache path due to: {}", err.bright_red()); 47 | return false; 48 | } 49 | 50 | // get temp file path 51 | let temp_path = path.join("temp.txt"); 52 | 53 | // check if we have write access 54 | if std::fs::File::create(&temp_path).is_ok() { 55 | std::fs::remove_file(&temp_path).expect("Couldn't remove created file"); 56 | println!("Set cache path to {}", path.display().bright_yellow()); 57 | println!( 58 | "\nDon't forget to clean up your old cache location if needed: {}", 59 | config.cache.clone().unwrap().display().bright_yellow() 60 | ); 61 | config.cache = Some(path); 62 | return true; 63 | } else { 64 | println!("Failed to set cache path to {}, since opening a test file there was not succesful", path.display().bright_yellow()); 65 | } 66 | } 67 | } else if let Some(path) = config.cache.as_ref() { 68 | println!( 69 | "Current configured cache path is {}", 70 | path.display().bright_yellow() 71 | ); 72 | } else { 73 | println!("Cache path is not configured!"); 74 | } 75 | } 76 | } 77 | 78 | false 79 | } 80 | -------------------------------------------------------------------------------- /src/commands/config/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::{Subcommand, Args}; 2 | 3 | mod cache; 4 | mod ndkpath; 5 | mod symlink; 6 | mod timeout; 7 | mod token; 8 | mod publish; 9 | 10 | use owo_colors::OwoColorize; 11 | 12 | use crate::data::config::Config as AppConfig; 13 | 14 | #[derive(Args, Debug, Clone)] 15 | 16 | pub struct Config { 17 | /// The operation to execute 18 | #[clap(subcommand)] 19 | pub op: ConfigOperation, 20 | /// use this flag to edit the local config instead of the global one 21 | #[clap(short, long)] 22 | pub local: bool, 23 | } 24 | 25 | #[derive(Subcommand, Debug, Clone)] 26 | 27 | pub enum ConfigOperation { 28 | /// Get or set the cache path 29 | Cache(cache::Cache), 30 | /// Enable or disable symlink usage 31 | Symlink(symlink::Symlink), 32 | /// Get or set the timeout for web requests 33 | Timeout(timeout::Timeout), 34 | /// Get or set the github token used for restore 35 | Token(token::Token), 36 | /// Print the location of the global config 37 | Location, 38 | /// Get or set the ndk path used in generation of build files 39 | NDKPath(ndkpath::NDKPath), 40 | /// Get or set the publish key used for publish 41 | Publish(publish::Key), 42 | } 43 | 44 | pub fn execute_config_operation(operation: Config) { 45 | let mut config = if operation.local { 46 | AppConfig::read_local() 47 | } else { 48 | AppConfig::read() 49 | }; 50 | 51 | let mut changed_any = false; 52 | match operation.op { 53 | ConfigOperation::Cache(c) => { 54 | changed_any = cache::execute_cache_config_operation(&mut config, c) 55 | } 56 | ConfigOperation::Symlink(s) => { 57 | changed_any = symlink::execute_symlink_config_operation(&mut config, s) 58 | } 59 | ConfigOperation::Timeout(t) => { 60 | changed_any = timeout::execute_timeout_config_operation(&mut config, t) 61 | } 62 | ConfigOperation::Token(t) => token::execute_token_config_operation(t), 63 | ConfigOperation::Location => println!( 64 | "Global Config is located at {}", 65 | AppConfig::global_config_path().display().bright_yellow() 66 | ), 67 | ConfigOperation::NDKPath(p) => { 68 | changed_any = ndkpath::execute_ndk_config_operation(&mut config, p) 69 | }, 70 | ConfigOperation::Publish(k) => publish::execute_key_config_operation(k), 71 | } 72 | 73 | if !changed_any { 74 | return; 75 | } 76 | 77 | if operation.local { 78 | config.write_local(); 79 | } else { 80 | config.write(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/commands/config/ndkpath.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args}; 2 | use owo_colors::OwoColorize; 3 | 4 | use crate::data::config::Config as AppConfig; 5 | 6 | #[derive(Args, Debug, Clone)] 7 | pub struct NDKPath { 8 | /// The path to set for the ndk path 9 | pub ndk_path: Option, 10 | } 11 | 12 | pub fn execute_ndk_config_operation(config: &mut AppConfig, operation: NDKPath) -> bool { 13 | if let Some(path) = operation.ndk_path { 14 | println!("Set ndk path to {}!", path.bright_yellow()); 15 | config.ndk_path = Some(path); 16 | true 17 | } else if let Some(path) = &config.ndk_path { 18 | println!("Current configured ndk path is: {}", path.bright_yellow()); 19 | false 20 | } else { 21 | println!("No ndk path was configured!"); 22 | false 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/commands/config/publish.rs: -------------------------------------------------------------------------------- 1 | use clap::Args; 2 | use owo_colors::OwoColorize; 3 | 4 | use crate::data::config::get_publish_keyring; 5 | 6 | #[derive(Args, Debug, Clone)] 7 | pub struct Key { 8 | pub key: Option, 9 | #[clap(long)] 10 | pub delete: bool, 11 | } 12 | 13 | pub fn execute_key_config_operation(operation: Key) { 14 | if operation.delete && get_publish_keyring().get_password().is_ok() { 15 | get_publish_keyring() 16 | .delete_password() 17 | .expect("Removing publish key failed"); 18 | println!("Deleted publish key from config, it will no longer be used"); 19 | return; 20 | } else if operation.delete { 21 | println!("There was no publish key configured, did not delete it"); 22 | return; 23 | } 24 | 25 | if let Some(key) = operation.key { 26 | // write key 27 | get_publish_keyring().set_password(&key).expect("Failed to set publish key"); 28 | println!("Configured a publish key! This will now be used for future qpm publish calls"); 29 | } else { 30 | // read token, possibly unused so prepend with _ to prevent warnings 31 | if let Ok(_key) = get_publish_keyring().get_password() { 32 | #[cfg(debug_assertions)] 33 | println!("Configured publish key: {}", _key.bright_yellow()); 34 | #[cfg(not(debug_assertions))] 35 | println!( 36 | "In release builds you {} view the configured publish key!", 37 | "cannot".bright_red() 38 | ); 39 | } else { 40 | println!("No publish key was configured, or getting the publish key failed!"); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/commands/config/symlink.rs: -------------------------------------------------------------------------------- 1 | use clap::{Subcommand, Args}; 2 | use owo_colors::OwoColorize; 3 | 4 | use crate::data::config::Config as AppConfig; 5 | 6 | #[derive(Subcommand, Debug, Clone)] 7 | pub enum SymlinkOperation { 8 | /// Enable symlink usage 9 | Enable, 10 | /// Disable symlink usage 11 | Disable, 12 | } 13 | 14 | #[derive(Args, Debug, Clone)] 15 | 16 | pub struct Symlink { 17 | #[clap(subcommand)] 18 | pub op: Option, 19 | } 20 | 21 | pub fn execute_symlink_config_operation(config: &mut AppConfig, operation: Symlink) -> bool { 22 | // value is given 23 | if let Some(symlink) = operation.op { 24 | match symlink { 25 | SymlinkOperation::Enable => { 26 | set_symlink_usage(config, true); 27 | } 28 | SymlinkOperation::Disable => { 29 | set_symlink_usage(config, false); 30 | } 31 | } 32 | return true; 33 | } else if let Some(symlink) = config.symlink.as_ref() { 34 | println!( 35 | "Current configured symlink usage is set to: {}", 36 | symlink.bright_yellow() 37 | ); 38 | } else { 39 | println!("Symlink usage is not configured!"); 40 | } 41 | 42 | false 43 | } 44 | 45 | fn set_symlink_usage(config: &mut AppConfig, value: bool) { 46 | println!("Set symlink usage to {}", value.bright_yellow()); 47 | config.symlink = Some(value); 48 | } 49 | -------------------------------------------------------------------------------- /src/commands/config/timeout.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args}; 2 | use owo_colors::OwoColorize; 3 | 4 | use crate::data::config::Config as AppConfig; 5 | 6 | #[derive(Args, Debug, Clone)] 7 | pub struct Timeout { 8 | pub timeout: Option, 9 | } 10 | 11 | pub fn execute_timeout_config_operation(config: &mut AppConfig, operation: Timeout) -> bool { 12 | if let Some(timeout) = operation.timeout { 13 | println!("Set timeout to {}!", timeout.bright_yellow()); 14 | config.timeout = Some(timeout); 15 | true 16 | } else if let Some(timeout) = config.timeout { 17 | println!( 18 | "Current configured timeout is set to: {}", 19 | timeout.bright_yellow() 20 | ); 21 | false 22 | } else { 23 | println!("Timeout is not configured!"); 24 | false 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/commands/config/token.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args}; 2 | use owo_colors::OwoColorize; 3 | 4 | use crate::data::config::get_keyring; 5 | 6 | #[derive(Args, Debug, Clone)] 7 | pub struct Token { 8 | pub token: Option, 9 | #[clap(long)] 10 | pub delete: bool, 11 | } 12 | 13 | pub fn execute_token_config_operation(operation: Token) { 14 | if operation.delete && get_keyring().get_password().is_ok() { 15 | get_keyring() 16 | .delete_password() 17 | .expect("Removing password failed"); 18 | println!("Deleted github token from config, it will no longer be used"); 19 | return; 20 | } else if operation.delete { 21 | println!("There was no github token configured, did not delete it"); 22 | return; 23 | } 24 | 25 | if let Some(token) = operation.token { 26 | // write token 27 | get_keyring() 28 | .set_password(&token) 29 | .expect("Storing token failed!"); 30 | println!("Configured a github token! This will now be used in qpm restore"); 31 | } else { 32 | // read token, possibly unused so prepend with _ to prevent warnings 33 | if let Ok(_token) = get_keyring().get_password() { 34 | #[cfg(debug_assertions)] 35 | println!("Configured github token: {}", _token.bright_yellow()); 36 | #[cfg(not(debug_assertions))] 37 | println!( 38 | "In release builds you {} view the configured github token, a token was configured though!", 39 | "cannot".bright_red() 40 | ); 41 | } else { 42 | println!("No token was configured, or getting the token failed!"); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/commands/dependency.rs: -------------------------------------------------------------------------------- 1 | use clap::{Subcommand, Args}; 2 | use owo_colors::OwoColorize; 3 | use semver::VersionReq; 4 | 5 | use crate::data::{dependency, package::PackageConfig}; 6 | 7 | #[derive(Args, Debug, Clone)] 8 | pub struct Dependency { 9 | #[clap(subcommand)] 10 | pub op: DependencyOperation, 11 | } 12 | 13 | #[derive(Subcommand, Debug, Clone)] 14 | pub enum DependencyOperation { 15 | /// Add a dependency 16 | Add(DependencyOperationAddArgs), 17 | /// Remove a dependency 18 | Remove(DependencyOperationRemoveArgs), 19 | } 20 | 21 | #[derive(Args, Debug, Clone)] 22 | pub struct DependencyOperationAddArgs { 23 | /// Id of the dependency as listed on qpackages 24 | pub id: String, 25 | 26 | /// optional version of the dependency that you want to add 27 | #[clap(short, long)] 28 | pub version: Option, 29 | 30 | /// Additional data for the dependency (as a valid json object) 31 | #[clap(long)] 32 | pub additional_data: Option, 33 | } 34 | 35 | #[derive(Args, Debug, Clone)] 36 | pub struct DependencyOperationRemoveArgs { 37 | /// Id of the dependency as listed on qpackages 38 | pub id: String, 39 | } 40 | 41 | pub fn execute_dependency_operation(operation: Dependency) { 42 | match operation.op { 43 | DependencyOperation::Add(a) => add_dependency(a), 44 | DependencyOperation::Remove(r) => remove_dependency(r), 45 | } 46 | } 47 | 48 | fn add_dependency(dependency_args: DependencyOperationAddArgs) { 49 | if dependency_args.id == "yourmom" { 50 | println!("The dependency was too big to add, we can't add this one!"); 51 | return; 52 | } 53 | 54 | let versions = crate::data::qpackages::get_versions(&dependency_args.id).expect("No version found for dependency"); 55 | 56 | if versions.is_empty() { 57 | println!( 58 | "Package {} does not seem to exist qpackages, please make sure you spelled it right, and that it's an actual package!", 59 | dependency_args.id.bright_green() 60 | ); 61 | return; 62 | } 63 | 64 | let version = match dependency_args.version { 65 | Option::Some(v) => v, 66 | // if no version given, use ^latest instead, should've specified a version idiot 67 | Option::None => { 68 | semver::VersionReq::parse(&format!("^{}", versions.first().unwrap().version)).unwrap() 69 | } 70 | }; 71 | 72 | let additional_data = match &dependency_args.additional_data { 73 | Option::Some(d) => serde_json::from_str(d).expect("Deserializing additional data failed"), 74 | Option::None => dependency::AdditionalDependencyData::default(), 75 | }; 76 | 77 | put_dependency(&dependency_args.id, version, &additional_data); 78 | } 79 | 80 | fn put_dependency( 81 | id: &str, 82 | version: VersionReq, 83 | additional_data: &dependency::AdditionalDependencyData, 84 | ) { 85 | println!( 86 | "Adding dependency with id {} and version {}", 87 | id.bright_red(), 88 | version.bright_blue() 89 | ); 90 | 91 | let mut package = crate::data::package::PackageConfig::read(); 92 | let dep = dependency::Dependency { 93 | id: id.to_string(), 94 | version_range: version, 95 | additional_data: additional_data.clone(), 96 | }; 97 | package.add_dependency(dep); 98 | package.write(); 99 | } 100 | 101 | fn remove_dependency(dependency_args: DependencyOperationRemoveArgs) { 102 | let mut package = PackageConfig::read(); 103 | package.remove_dependency(&dependency_args.id); 104 | package.write(); 105 | } 106 | -------------------------------------------------------------------------------- /src/commands/install.rs: -------------------------------------------------------------------------------- 1 | use std::{io::Write, path::PathBuf}; 2 | 3 | use clap::Args; 4 | 5 | use crate::data::{ 6 | config::Config, 7 | file_repository::FileRepository, 8 | package::{PackageConfig, SharedPackageConfig}, 9 | }; 10 | 11 | #[derive(Args, Debug, Clone)] 12 | pub struct InstallOperation { 13 | pub binary_path: Option, 14 | pub debug_binary_path: Option, 15 | 16 | #[clap(long)] 17 | pub cmake_build: Option, 18 | // pub additional_folders: Vec // todo 19 | } 20 | 21 | pub fn execute_install_operation(install: InstallOperation) { 22 | println!("Publishing package to local file repository"); 23 | let package = PackageConfig::read(); 24 | let shared_package = SharedPackageConfig::from_package(&package); 25 | 26 | // create used dirs 27 | std::fs::create_dir_all("src").expect("Failed to create directory"); 28 | std::fs::create_dir_all("include").expect("Failed to create directory"); 29 | std::fs::create_dir_all(&shared_package.config.shared_dir).expect("Failed to create directory"); 30 | 31 | // write the ndk path to a file if available 32 | let config = Config::read_combine(); 33 | if let Some(ndk_path) = config.ndk_path { 34 | let mut file = std::fs::File::create("ndkpath.txt").expect("Failed to create ndkpath.txt"); 35 | file.write_all(ndk_path.as_bytes()) 36 | .expect("Failed to write out ndkpath.txt"); 37 | } 38 | 39 | shared_package.write(); 40 | 41 | let mut binary_path = install.binary_path; 42 | let mut debug_binary_path = install.debug_binary_path; 43 | 44 | let header_only = package.info.additional_data.headers_only.unwrap_or(false); 45 | #[cfg(debug_assertions)] 46 | println!("Header only: {}", header_only); 47 | 48 | if !header_only { 49 | if binary_path.is_none() && install.cmake_build.unwrap_or(true) { 50 | binary_path = Some( 51 | PathBuf::from(format!("./build/{}", shared_package.config.get_so_name())) 52 | .canonicalize() 53 | .unwrap(), 54 | ); 55 | } 56 | 57 | if debug_binary_path.is_none() && install.cmake_build.unwrap_or(true) { 58 | debug_binary_path = Some( 59 | PathBuf::from(format!( 60 | "./build/debug/{}", 61 | shared_package.config.get_so_name() 62 | )) 63 | .canonicalize() 64 | .unwrap(), 65 | ); 66 | } 67 | } 68 | 69 | if let Some(p) = &debug_binary_path { 70 | if !p.exists() { 71 | println!("Could not find debug binary {p:?}, skipping") 72 | } 73 | } 74 | 75 | if let Some(p) = &binary_path { 76 | if !p.exists() { 77 | println!("Could not find binary {p:?}, skipping") 78 | } 79 | } 80 | 81 | let mut repo = FileRepository::read(); 82 | repo.add_artifact( 83 | shared_package, 84 | PathBuf::from(".") 85 | .canonicalize() 86 | .expect("Unable to canocalize path"), 87 | binary_path, 88 | debug_binary_path, 89 | ); 90 | repo.write(); 91 | } 92 | -------------------------------------------------------------------------------- /src/commands/list/extra_properties.rs: -------------------------------------------------------------------------------- 1 | pub fn execute_extra_properties_list() { 2 | println!("TODO: print all extra properties"); 3 | // is this really needed? qpm package edit --help and qpm dependency edit --help should already give enough info? 4 | } 5 | -------------------------------------------------------------------------------- /src/commands/list/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::{Subcommand, Args}; 2 | 3 | mod extra_properties; 4 | mod packages; 5 | mod versions; 6 | pub type Package = versions::Package; 7 | 8 | #[derive(Subcommand, Debug, Clone)] 9 | 10 | pub enum ListOption { 11 | /// List the extra properties that are supported 12 | ExtraProperties, 13 | /// List the available packages on qpackages.com 14 | Packages, 15 | /// List the versions for a specific package 16 | Versions(Package), 17 | } 18 | 19 | #[derive(Args, Debug, Clone)] 20 | 21 | pub struct ListOperation { 22 | /// What you want to list 23 | #[clap(subcommand)] 24 | pub op: ListOption, 25 | } 26 | 27 | pub fn execute_list_operation(operation: ListOperation) { 28 | match operation.op { 29 | ListOption::ExtraProperties => extra_properties::execute_extra_properties_list(), 30 | ListOption::Packages => packages::execute_packages_list(), 31 | ListOption::Versions(p) => versions::execute_versions_list(p), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/commands/list/packages.rs: -------------------------------------------------------------------------------- 1 | use owo_colors::OwoColorize; 2 | 3 | pub fn execute_packages_list() { 4 | let ids = crate::data::qpackages::get_packages(); 5 | if !ids.is_empty() { 6 | println!( 7 | "Found {} packages on qpackages.com", 8 | ids.len().bright_yellow() 9 | ); 10 | let mut idx = 0; 11 | for id in ids.iter() { 12 | println!("{}", id); 13 | idx += 1; 14 | if idx % 5 == 0 { 15 | println!(); 16 | idx = 0; 17 | } 18 | } 19 | } else { 20 | println!("qpackages.com returned 0 packages, is something wrong?"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/commands/list/versions.rs: -------------------------------------------------------------------------------- 1 | use clap::Args; 2 | use owo_colors::OwoColorize; 3 | 4 | #[derive(Args, Debug, Clone)] 5 | pub struct Package { 6 | pub package: String, 7 | #[clap(short, long)] 8 | pub latest: bool, 9 | } 10 | 11 | pub fn execute_versions_list(package: Package) { 12 | let versions = crate::data::qpackages::get_versions(&package.package); 13 | if package.latest { 14 | println!( 15 | "The latest version for package {} is {}", 16 | package.package.bright_red(), 17 | versions 18 | .expect("Getting version failed!") 19 | .get(0) 20 | .expect("Getting first version failed!") 21 | .version 22 | .to_string() 23 | .bright_green() 24 | ); 25 | } else if let Some(package_versions) = &versions { 26 | println!( 27 | "Package {} has {} versions on qpackages.com:", 28 | package.package.bright_red(), 29 | versions.as_ref().unwrap().len().bright_yellow() 30 | ); 31 | for package_version in package_versions.iter().rev() { 32 | println!(" - {}", package_version.version.to_string().bright_green()); 33 | } 34 | } else { 35 | println!( 36 | "Package {} either did not exist or has no versions on qpackages.com", 37 | package.package.bright_red() 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod cache; 2 | pub mod clear; 3 | pub mod collapse; 4 | pub mod config; 5 | pub mod dependency; 6 | pub mod list; 7 | pub mod package; 8 | pub mod publish; 9 | pub mod qmod; 10 | pub mod restore; 11 | pub mod install; -------------------------------------------------------------------------------- /src/commands/package/create.rs: -------------------------------------------------------------------------------- 1 | use std::path::Path; 2 | 3 | use clap::{Args}; 4 | use owo_colors::OwoColorize; 5 | use semver::Version; 6 | 7 | use crate::data::{ 8 | dependency::{AdditionalDependencyData, Dependency}, 9 | package::{AdditionalPackageData, PackageConfig, PackageInfo}, 10 | }; 11 | #[derive(Args, Debug, Clone)] 12 | 13 | pub struct PackageOperationCreateArgs { 14 | /// The name of the package 15 | pub name: String, 16 | /// The version of the package 17 | pub version: Version, 18 | /// Specify an id, else lowercase will be used 19 | #[clap(long = "id")] 20 | pub id: Option, 21 | /// Branch name of a Github repo. Only used when a valid github url is provided 22 | #[clap(long = "branchName")] 23 | pub branch_name: Option, 24 | /// Specify that this package is headers only and does not contain a .so or .a file 25 | #[clap(long = "headersOnly")] 26 | pub headers_only: Option, 27 | /// Specify that this package is static linking 28 | #[clap(long = "staticLinking")] 29 | pub static_linking: Option, 30 | /// Specify the download link for a release .so or .a file 31 | #[clap(long = "soLink")] 32 | pub so_link: Option, 33 | /// Specify the download link for a debug .so or .a files (usually from the obj folder) 34 | #[clap(long = "debugSoLink")] 35 | pub debug_so_link: Option, 36 | /// Override the downloaded .so or .a filename with this name instead. 37 | #[clap(long = "overrideSoName")] 38 | pub override_so_name: Option, 39 | } 40 | 41 | pub fn package_create_operation(create_parameters: PackageOperationCreateArgs) { 42 | if PackageConfig::check() { 43 | println!( 44 | "{}", 45 | "Package already existed, not creating a new package!".bright_red() 46 | ); 47 | println!("Did you try to make a package in the same directory as another, or did you not use a clean folder?"); 48 | return; 49 | } 50 | 51 | let additional_data = AdditionalPackageData { 52 | branch_name: create_parameters.branch_name, 53 | headers_only: create_parameters.headers_only, 54 | static_linking: create_parameters.static_linking, 55 | so_link: create_parameters.so_link, 56 | debug_so_link: create_parameters.debug_so_link, 57 | override_so_name: create_parameters.override_so_name, 58 | ..Default::default() 59 | }; 60 | 61 | // id is optional so we need to check if it's defined, else use the name to lowercase 62 | let id = match create_parameters.id { 63 | Option::Some(s) => s, 64 | Option::None => create_parameters.name.to_lowercase(), 65 | }; 66 | 67 | let package_info = PackageInfo { 68 | id, 69 | name: create_parameters.name, 70 | url: None, 71 | version: create_parameters.version, 72 | additional_data, 73 | }; 74 | 75 | let package = PackageConfig { 76 | info: package_info, 77 | shared_dir: Path::new("shared").to_owned(), 78 | dependencies_dir: Path::new("extern").to_owned(), 79 | dependencies: Vec::::default(), 80 | additional_data: AdditionalDependencyData::default(), 81 | }; 82 | 83 | package.write(); 84 | } 85 | -------------------------------------------------------------------------------- /src/commands/package/edit.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args}; 2 | use semver::Version; 3 | 4 | use crate::data::{package::{PackageConfig, SharedPackageConfig}, repo::DependencyRepository}; 5 | 6 | #[derive(Args, Debug, Clone)] 7 | 8 | pub struct EditArgs { 9 | ///Edit the id property of the package 10 | #[clap(long)] 11 | pub id: Option, 12 | ///Edit the name property of the package 13 | #[clap(long)] 14 | pub name: Option, 15 | ///Edit the url property of the package 16 | #[clap(long)] 17 | pub url: Option, 18 | ///Edit the version property of the package 19 | #[clap(long)] 20 | pub version: Option, 21 | } 22 | 23 | pub fn package_edit_operation(edit_parameters: EditArgs, repo: &impl DependencyRepository) { 24 | let mut package = PackageConfig::read(); 25 | let mut any_changed = false; 26 | if let Some(id) = edit_parameters.id { 27 | package_set_id(&mut package, id); 28 | any_changed = true; 29 | } 30 | if let Some(name) = edit_parameters.name { 31 | package_set_name(&mut package, name); 32 | any_changed = true; 33 | } 34 | if let Some(url) = edit_parameters.url { 35 | package_set_url(&mut package, url); 36 | any_changed = true; 37 | } 38 | if let Some(version) = edit_parameters.version { 39 | package_set_version(&mut package, version); 40 | any_changed = true; 41 | } 42 | 43 | if any_changed { 44 | package.write(); 45 | let mut shared_package = SharedPackageConfig::read(); 46 | shared_package.config = package; 47 | shared_package.write(); 48 | 49 | // HACK: Not sure if this is a proper way of doing this but it seems logical 50 | shared_package.write_define_cmake(); 51 | shared_package.write_extern_cmake(repo); 52 | } 53 | } 54 | 55 | fn package_set_id(package: &mut PackageConfig, id: String) { 56 | println!("Setting package id: {}", id); 57 | package.info.id = id; 58 | } 59 | 60 | fn package_set_name(package: &mut PackageConfig, name: String) { 61 | println!("Setting package name: {}", name); 62 | package.info.name = name; 63 | } 64 | 65 | fn package_set_url(package: &mut PackageConfig, url: String) { 66 | println!("Setting package url: {}", url); 67 | package.info.url = Option::Some(url); 68 | } 69 | 70 | fn package_set_version(package: &mut PackageConfig, version: Version) { 71 | println!("Setting package version: {}", version); 72 | package.info.version = version; 73 | } 74 | -------------------------------------------------------------------------------- /src/commands/package/edit_extra.rs: -------------------------------------------------------------------------------- 1 | use clap::{Args, Subcommand}; 2 | 3 | use crate::{ 4 | data::{package::{PackageConfig, SharedPackageConfig}, repo::DependencyRepository}, 5 | utils::toggle::Toggle, 6 | }; 7 | 8 | #[derive(Args, Debug, Clone)] 9 | 10 | pub struct EditExtraArgs { 11 | /// Change the branch name in additional data 12 | #[clap(long = "branchName")] 13 | pub branch_name: Option, 14 | 15 | /// Change the headers only bool in additional data, pass enable or disable 16 | #[clap(long = "headersOnly")] 17 | pub headers_only: Option, 18 | 19 | /// Make the package be statically linked, 0 for false, 1 for true 20 | #[clap(long = "staticLinking")] 21 | pub static_linking: Option, 22 | 23 | /// Provide a so link for downloading the regular .so file 24 | #[clap(long = "soLink")] 25 | pub so_link: Option, 26 | 27 | /// Provide a debug so link for downloading the debug .so file 28 | #[clap(long = "debugSoLink")] 29 | pub debug_so_link: Option, 30 | 31 | /// Provide an additional file to add to the extra files list, prepend with - to remove an entry 32 | #[clap(long = "extraFiles")] 33 | pub extra_files: Option, 34 | 35 | /// Provide an overridden name for the .so file 36 | #[clap(long = "overrideSoName")] 37 | pub override_so_name: Option, 38 | 39 | /// Provide a link to the mod 40 | #[clap(long = "modLink")] 41 | pub mod_link: Option, 42 | 43 | /// If this package is defined in a repo with more packages in subfolders, this is where you specify the subfolder to be used 44 | #[clap(long = "subFolder")] 45 | pub sub_folder: Option, 46 | 47 | /// Additional options for compilation and edits to compilation related files. 48 | #[clap(subcommand)] 49 | pub compile_options: Option, 50 | } 51 | 52 | #[derive(Subcommand, Debug, Clone)] 53 | 54 | pub enum EditExtraOptions { 55 | /// Additional options for compilation and edits to compilation related files. 56 | CompileOptions(CompileOptionsEditArgs), 57 | } 58 | 59 | #[derive(Args, Debug, Clone)] 60 | 61 | pub struct CompileOptionsEditArgs { 62 | /// Additional include paths to add, relative to the extern directory. Prefix with a '-' to remove that entry 63 | #[clap(long = "includePaths")] 64 | pub include_paths: Option, 65 | /// Additional system include paths to add, relative to the extern directory. Prefix with a '-' to remove that entry 66 | #[clap(long = "systemIncludes")] 67 | pub system_includes: Option, 68 | /// Additional C++ features to add. Prefix with a '-' to remove that entry 69 | #[clap(long = "cppFeatures")] 70 | pub cpp_features: Option, 71 | /// Additional C++ flags to add. Prefix with a '-' to remove that entry 72 | #[clap(long = "cppFlags")] 73 | pub cpp_flags: Option, 74 | /// Additional C flags to add. Prefix with a '-' to remove that entry 75 | #[clap(long = "cFlags")] 76 | pub c_flags: Option, 77 | } 78 | 79 | pub fn package_edit_extra_operation(edit_parameters: EditExtraArgs, repo: &impl DependencyRepository) { 80 | let mut package = PackageConfig::read(); 81 | let mut any_changed = false; 82 | if let Some(branch_name) = edit_parameters.branch_name { 83 | package_edit_extra_branch_name(&mut package, branch_name); 84 | any_changed = true; 85 | } 86 | if let Some(headers_only) = edit_parameters.headers_only { 87 | package_edit_extra_headers_only(&mut package, headers_only.into()); 88 | any_changed = true; 89 | } 90 | if let Some(static_linking) = edit_parameters.static_linking { 91 | package_edit_extra_static_linking(&mut package, static_linking.into()); 92 | any_changed = true; 93 | } 94 | if let Some(so_link) = edit_parameters.so_link { 95 | package_edit_extra_so_link(&mut package, so_link); 96 | any_changed = true; 97 | } 98 | if let Some(extra_files) = edit_parameters.extra_files { 99 | package_edit_extra_extra_files(&mut package, extra_files); 100 | any_changed = true; 101 | } 102 | if let Some(debug_so_link) = edit_parameters.debug_so_link { 103 | package_edit_extra_debug_so_link(&mut package, debug_so_link); 104 | any_changed = true; 105 | } 106 | if let Some(mod_link) = edit_parameters.mod_link { 107 | package_edit_extra_mod_link(&mut package, mod_link); 108 | any_changed = true; 109 | } 110 | if let Some(override_so_name) = edit_parameters.override_so_name { 111 | package_edit_extra_override_so_name(&mut package, override_so_name); 112 | any_changed = true; 113 | } 114 | if let Some(sub_folder) = edit_parameters.sub_folder { 115 | package_edit_extra_sub_folder(&mut package, sub_folder); 116 | any_changed = true; 117 | } 118 | 119 | if any_changed { 120 | package.write(); 121 | let mut shared_package = SharedPackageConfig::read(); 122 | shared_package.config = package; 123 | shared_package.write(); 124 | 125 | // HACK: Not sure if this is a proper way of doing this but it seems logical 126 | shared_package.write_define_cmake(); 127 | shared_package.write_extern_cmake(repo); 128 | } 129 | } 130 | 131 | pub fn package_edit_extra_branch_name(package: &mut PackageConfig, branch_name: String) { 132 | println!("Setting branch name: {:#?}", branch_name); 133 | package.info.additional_data.branch_name = Some(branch_name); 134 | } 135 | 136 | pub fn package_edit_extra_headers_only(package: &mut PackageConfig, headers_only: bool) { 137 | println!("Setting headers_only: {:#?}", headers_only); 138 | package.info.additional_data.headers_only = Some(headers_only); 139 | } 140 | 141 | pub fn package_edit_extra_static_linking(package: &mut PackageConfig, static_linking: bool) { 142 | println!("Setting static_linking: {:#?}", static_linking); 143 | package.info.additional_data.static_linking = Some(static_linking); 144 | } 145 | 146 | pub fn package_edit_extra_so_link(package: &mut PackageConfig, so_link: String) { 147 | println!("Setting so_link: {:#?}", so_link); 148 | package.info.additional_data.so_link = Some(so_link); 149 | } 150 | 151 | pub fn package_edit_extra_mod_link(package: &mut PackageConfig, mod_link: String) { 152 | println!("Setting mod_link: {:#?}", mod_link); 153 | package.info.additional_data.mod_link = Some(mod_link); 154 | } 155 | 156 | pub fn package_edit_extra_extra_files(package: &mut PackageConfig, extra_file: String) { 157 | println!("Setting extra_file: {}", extra_file); 158 | match extra_file.chars().next().unwrap() { 159 | '-' => { 160 | // remove 161 | package_edit_extra_remove_extra_files(package, extra_file[1..].to_string()); 162 | } 163 | _ => { 164 | // add 165 | package_edit_extra_add_extra_files(package, extra_file); 166 | } 167 | } 168 | } 169 | 170 | pub fn package_edit_extra_remove_extra_files(package: &mut PackageConfig, extra_file: String) { 171 | if let Some(extra_files) = &mut package.info.additional_data.extra_files { 172 | if let Some(idx) = extra_files.iter().position(|f| f == &extra_file) { 173 | extra_files.remove(idx); 174 | } 175 | } 176 | } 177 | 178 | pub fn package_edit_extra_add_extra_files(package: &mut PackageConfig, extra_file: String) { 179 | if let Some(extra_files) = &mut package.info.additional_data.extra_files { 180 | if !extra_files.iter().any(|f| f == &extra_file) { 181 | extra_files.push(extra_file); 182 | } 183 | } else { 184 | package.info.additional_data.extra_files = Some(vec![extra_file]); 185 | } 186 | } 187 | 188 | pub fn package_edit_extra_debug_so_link(package: &mut PackageConfig, debug_so_link: String) { 189 | println!("Setting debug_so_link: {:#?}", debug_so_link); 190 | package.info.additional_data.debug_so_link = Some(debug_so_link); 191 | } 192 | 193 | pub fn package_edit_extra_override_so_name(package: &mut PackageConfig, override_so_name: String) { 194 | println!("Setting override_so_name: {:#?}", override_so_name); 195 | package.info.additional_data.override_so_name = Some(override_so_name); 196 | } 197 | 198 | pub fn package_edit_extra_sub_folder(package: &mut PackageConfig, sub_folder: String) { 199 | println!("Setting sub_folder: {:#?}", sub_folder); 200 | package.info.additional_data.sub_folder = Some(sub_folder); 201 | } 202 | -------------------------------------------------------------------------------- /src/commands/package/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::{Subcommand, Args}; 2 | 3 | use crate::data::repo::multi_provider::MultiDependencyProvider; 4 | 5 | mod create; 6 | mod edit; 7 | mod edit_extra; 8 | 9 | #[derive(Args, Debug, Clone)] 10 | 11 | pub struct Package { 12 | #[clap(subcommand)] 13 | pub op: PackageOperation, 14 | } 15 | 16 | #[derive(Subcommand, Debug, Clone)] 17 | 18 | pub enum PackageOperation { 19 | /// Create a package 20 | Create(create::PackageOperationCreateArgs), 21 | /// Edit various properties of the package 22 | Edit(edit::EditArgs), 23 | /// Edit extra supported properties of the package 24 | EditExtra(edit_extra::EditExtraArgs), 25 | } 26 | 27 | pub fn execute_package_operation(operation: Package) { 28 | match operation.op { 29 | PackageOperation::Create(c) => create::package_create_operation(c), 30 | PackageOperation::Edit(e) => edit::package_edit_operation(e, &MultiDependencyProvider::useful_default_new()), 31 | PackageOperation::EditExtra(ee) => edit_extra::package_edit_extra_operation(ee, &MultiDependencyProvider::useful_default_new()), 32 | } 33 | } 34 | 35 | /* Check if all these are supported here: 36 | - branchName (System.String): Branch name of a Github repo. Only used when a valid github url is provided - Supported in: package, dependency 37 | - headersOnly (System.Boolean): Specify that this package is headers only and does not contain a .so or .a file - Supported in: package 38 | - staticLinking (System.Boolean): Specify that this package is static linking - Supported in: package 39 | - soLink (System.String): Specify the download link for a release .so or .a file - Supported in: package 40 | - debugSoLink (System.String): Specify the download link for a debug .so or .a files (usually from the obj folder) - Supported in: package 41 | - extraFiles (System.String[]): Specify any additional files to be downloaded - Supported in: package, dependency 42 | - overrideSoName (System.String): Override the downloaded .so or .a filename with this name instead. - Supported in: package 43 | - subfolder (System.String): Subfolder for this particular package in the provided repository, relative to root of the repo. - Supported in: package 44 | - compileOptions (QPM.Commands.SupportedPropertiesCommand+CompileOptionsProperty): Additional options for compilation and edits to compilation related files. - Supported in: package 45 | Type: QPM.Commands.SupportedPropertiesCommand+CompileOptionsProperty 46 | - includePaths - OPTIONAL (System.String[]): Additional include paths to add, relative to the extern directory. 47 | - systemIncludes - OPTIONAL (System.String[]): Additional system include paths to add, relative to the extern directory. 48 | - cppFeatures - OPTIONAL (System.String[]): Additional C++ features to add. 49 | - cppFlags - OPTIONAL (System.String[]): Additional C++ flags to add. 50 | - cFlags - OPTIONAL (System.String[]): Additional C flags to add. 51 | 52 | - localPath (System.String): Copy a dependency from a location that is local to this root path instead of from a remote url - Supported in: dependency 53 | - useRelease (System.Boolean): Specify if a dependency should download a release .so or .a file. Defaults to false - Supported in: dependency 54 | 55 | NOTE: Styles are not used by anybody, deprecate! 56 | - styles (QPM.Commands.SupportedPropertiesCommand+StyleProperty[]): Provide various download links of differing styles. Styles are appended to module names. - Supported in: package 57 | - style (System.String): Specify the style to use. - Supported in: dependency 58 | Type: QPM.Commands.SupportedPropertiesCommand+StyleProperty 59 | - name - REQUIRED (System.String): The name of the style. 60 | - soLink - OPTIONAL (System.String): The release downloadable so link for this style. Must exist if being used as release. 61 | - debugSoLink - OPTIONAL (System.String): The debug downloadable so link for this style. Must exist if being used as debug. 62 | 63 | */ 64 | -------------------------------------------------------------------------------- /src/commands/publish/mod.rs: -------------------------------------------------------------------------------- 1 | use clap::Args; 2 | use crate::data::config::get_publish_keyring; 3 | 4 | #[derive(Args, Debug, Clone)] 5 | 6 | pub struct Publish { 7 | /// the authorization header to use for publishing, if present 8 | pub publish_auth: Option, 9 | } 10 | 11 | use owo_colors::OwoColorize; 12 | 13 | use crate::data::package::SharedPackageConfig; 14 | pub fn execute_publish_operation(auth: &Publish) { 15 | let package = SharedPackageConfig::read(); 16 | if package.config.info.url.is_none() { 17 | println!("Package without url can not be published!"); 18 | return; 19 | } 20 | 21 | // check if all dependencies are available off of qpackages 22 | for dependency in package.config.dependencies.iter() { 23 | match dependency.get_shared_package() { 24 | Option::Some(_s) => {} 25 | Option::None => { 26 | println!( 27 | "dependency {} was not available on qpackages in the given version range", 28 | &dependency.id 29 | ); 30 | panic!( 31 | "make sure {} exists for this dependency", 32 | &dependency.version_range 33 | ); 34 | } 35 | }; 36 | } 37 | 38 | // check if all required dependencies are in the restored dependencies, and if they satisfy the version ranges 39 | for dependency in package.config.dependencies.iter() { 40 | // if we can not find any dependency that matches ID and version satisfies given range, then we are missing a dep 41 | if let Some(el) = package 42 | .restored_dependencies 43 | .iter() 44 | .find(|el| el.dependency.id == dependency.id) 45 | { 46 | // if version doesn't match range, panic 47 | if !dependency.version_range.matches(&el.version) { 48 | panic!( 49 | "Restored dependency {} version ({}) does not satisfy stated range ({})", 50 | dependency.id.bright_red(), 51 | el.version.to_string().bright_green(), 52 | dependency.version_range.to_string().bright_blue() 53 | ); 54 | } 55 | } 56 | } 57 | 58 | // check if url is set to download headers 59 | if package.config.info.url.is_none() { 60 | panic!("info.url is null, please make sure to init this with the base link to your repo, e.g. '{}'", "https://github.com/RedBrumbler/QuestPackageManager-Rust".bright_yellow()); 61 | } 62 | // check if this is header only, if it's not header only check if the so_link is set, if not, panic 63 | if !package 64 | .config 65 | .info 66 | .additional_data 67 | .headers_only 68 | .unwrap_or(false) 69 | && package.config.info.additional_data.so_link.is_none() 70 | { 71 | panic!("soLink is not set in the package config, but this package is not header only, please make sure to either add the soLink or to make the package header only."); 72 | } 73 | 74 | // TODO: Implement a check that gets the repo and checks if the shared folder and subfolder exists, if not it throws an error and won't let you publish 75 | 76 | if let Some(key) = &auth.publish_auth { 77 | package.publish(&key); 78 | } else { 79 | // Empty strings are None, you shouldn't be able to publish with a None 80 | let publish_key = get_publish_keyring(); 81 | package.publish(&publish_key.get_password().expect("Unable to get stored publish key!")); 82 | } 83 | 84 | 85 | println!( 86 | "Package {} v{} published!", 87 | package.config.info.id, package.config.info.version 88 | ); 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/commands/qmod/edit.rs: -------------------------------------------------------------------------------- 1 | use clap::Args; 2 | use semver::Version; 3 | 4 | use crate::data::mod_json::ModJson; 5 | 6 | /// Some properties are not editable through the qmod edit command, these properties are either editable through the package, or not at all 7 | #[derive(Args, Debug, Clone)] 8 | 9 | pub struct EditQmodJsonOperationArgs { 10 | /// The schema version this mod was made for, ex. '0.1.1' 11 | #[clap(long = "qpversion")] 12 | pub schema_version: Option, 13 | /// Author of the mod, ex. 'RedBrumbler' 14 | #[clap(long)] 15 | pub author: Option, 16 | /// Optional slot for if you ported a mod, ex. 'Fern' 17 | #[clap(long)] 18 | pub porter: Option, 19 | /// id of the package the mod is for, ex. 'com.beatgames.beatsaber' 20 | #[clap(long = "packageID")] 21 | pub package_id: Option, 22 | /// Version of the package, ex. '1.1.0' 23 | #[clap(long = "packageVersion")] 24 | pub package_version: Option, 25 | /// description for the mod, ex. 'The best mod to exist ever!' 26 | #[clap(long)] 27 | pub description: Option, 28 | /// optional cover image filename, ex. 'cover.png' 29 | #[clap(long = "coverImage")] 30 | pub cover_image: Option, 31 | } 32 | 33 | pub fn execute_qmod_edit_operation(edit_parameters: EditQmodJsonOperationArgs) { 34 | let mut json = ModJson::read(ModJson::get_template_path()); 35 | 36 | if let Some(schema_version) = edit_parameters.schema_version { 37 | json.schema_version = schema_version; 38 | } 39 | if let Some(author) = edit_parameters.author { 40 | json.author = author; 41 | } 42 | if let Some(porter) = edit_parameters.porter { 43 | if porter == "clear" { 44 | json.porter = None; 45 | } else { 46 | json.porter = Some(porter); 47 | } 48 | } 49 | if let Some(package_id) = edit_parameters.package_id { 50 | json.package_id = Some(package_id); 51 | } 52 | if let Some(package_version) = edit_parameters.package_version { 53 | json.package_version = Some(package_version); 54 | } 55 | if let Some(description) = edit_parameters.description { 56 | if description == "clear" { 57 | json.description = None; 58 | } else { 59 | json.description = Some(description); 60 | } 61 | } 62 | if let Some(cover_image) = edit_parameters.cover_image { 63 | if cover_image == "clear" { 64 | json.cover_image = None; 65 | } else { 66 | json.cover_image = Some(cover_image); 67 | } 68 | } 69 | 70 | json.write(ModJson::get_template_path()); 71 | } 72 | -------------------------------------------------------------------------------- /src/commands/qmod/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{Args, Subcommand}; 4 | use semver::Version; 5 | 6 | mod edit; 7 | 8 | use crate::data::{ 9 | mod_json::{ModJson, PreProcessingData}, 10 | package::{PackageConfig, SharedPackageConfig}, 11 | }; 12 | 13 | #[derive(Args, Debug, Clone)] 14 | 15 | pub struct Qmod { 16 | #[clap(subcommand)] 17 | pub op: QmodOperation, 18 | } 19 | 20 | /// Some properties are not editable through the qmod create command, these properties are either editable through the package, or not at all 21 | #[derive(Args, Debug, Clone)] 22 | 23 | pub struct CreateQmodJsonOperationArgs { 24 | /// The schema version this mod was made for, ex. '0.1.1' 25 | #[clap(long = "qpversion")] 26 | pub schema_version: Option, 27 | /// Author of the mod, ex. 'RedBrumbler' 28 | #[clap(long)] 29 | pub author: Option, 30 | /// Optional slot for if you ported a mod, ex. 'Fern' 31 | #[clap(long)] 32 | pub porter: Option, 33 | /// id of the package the mod is for, ex. 'com.beatgames.beatsaber' 34 | #[clap(long = "packageID")] 35 | pub package_id: Option, 36 | /// Version of the package, ex. '1.1.0' 37 | #[clap(long = "packageVersion")] 38 | pub package_version: Option, 39 | /// description for the mod, ex. 'The best mod to exist ever!' 40 | #[clap(long)] 41 | pub description: Option, 42 | /// optional cover image filename, ex. 'cover.png' 43 | #[clap(long = "coverImage")] 44 | pub cover_image: Option, 45 | #[clap(long = "isLibrary")] 46 | pub is_library: Option, 47 | } 48 | 49 | #[derive(Args, Debug, Clone)] 50 | 51 | pub struct BuildQmodOperationArgs { 52 | #[clap(long = "isLibrary")] 53 | pub is_library: Option, 54 | 55 | /// 56 | /// Tells QPM to exclude mods from being listed as copied mod or libs dependencies 57 | /// 58 | #[clap(long = "exclude_libs")] 59 | pub exclude_libs: Option>, 60 | 61 | /// 62 | /// Tells QPM to include mods from being listed as copied mod or libs dependencies 63 | /// Does not work with `exclude_libs` combined 64 | /// 65 | #[clap(long = "include_libs")] 66 | pub include_libs: Option>, 67 | } 68 | 69 | #[derive(Subcommand, Debug, Clone)] 70 | #[allow(clippy::large_enum_variant)] 71 | pub enum QmodOperation { 72 | /// Create a "mod.template.json" that you can pre-fill with certain values that will be used to then generate your final mod.json when you run 'qpm qmod build' 73 | /// 74 | /// Some properties are not settable through the qmod create command, these properties are either editable through the package, or not at all 75 | Create(CreateQmodJsonOperationArgs), 76 | /// This will parse the `mod.template.json` and process it, then finally export a `mod.json` for packaging and deploying. 77 | Build(BuildQmodOperationArgs), 78 | /// Edit your mod.template.json from the command line, mostly intended for edits on github actions 79 | /// 80 | /// Some properties are not editable through the qmod edit command, these properties are either editable through the package, or not at all 81 | Edit(edit::EditQmodJsonOperationArgs), 82 | } 83 | 84 | pub fn execute_qmod_operation(operation: Qmod) { 85 | match operation.op { 86 | QmodOperation::Create(q) => execute_qmod_create_operation(q), 87 | QmodOperation::Build(b) => execute_qmod_build_operation(b), 88 | QmodOperation::Edit(e) => edit::execute_qmod_edit_operation(e), 89 | } 90 | } 91 | 92 | fn execute_qmod_create_operation(create_parameters: CreateQmodJsonOperationArgs) { 93 | let schema_version = match create_parameters.schema_version { 94 | Option::Some(s) => s, 95 | Option::None => Version::new(1, 0, 0), 96 | }; 97 | 98 | let json = ModJson { 99 | schema_version, 100 | name: "${mod_name}".to_string(), 101 | id: "${mod_id}".to_string(), 102 | author: create_parameters 103 | .author 104 | .unwrap_or_else(|| "---".to_string()), 105 | porter: create_parameters.porter, 106 | version: "${version}".to_string(), 107 | package_id: create_parameters.package_id, 108 | package_version: create_parameters.package_version, 109 | description: Some( 110 | create_parameters 111 | .description 112 | .unwrap_or_else(|| "${mod_id}, version ${version}!".to_string()), 113 | ), 114 | cover_image: create_parameters.cover_image, 115 | is_library: create_parameters.is_library, 116 | dependencies: Default::default(), 117 | mod_files: Default::default(), 118 | library_files: Default::default(), 119 | file_copies: Default::default(), 120 | copy_extensions: Default::default(), 121 | }; 122 | 123 | json.write(PathBuf::from(ModJson::get_template_name())); 124 | } 125 | 126 | // This will parse the `qmod.template.json` and process it, then finally export a `qmod.json` for packaging and deploying. 127 | fn execute_qmod_build_operation(build_parameters: BuildQmodOperationArgs) { 128 | assert!(std::path::Path::new("mod.template.json").exists(), 129 | "No mod.template.json found in the current directory, set it up please :) Hint: use \"qmod create\""); 130 | 131 | println!("Generating mod.json file from template..."); 132 | let package = PackageConfig::read(); 133 | let shared_package = SharedPackageConfig::from_package(&package); 134 | 135 | let mut mod_json: ModJson = shared_package.into(); 136 | 137 | // Parse template mod.template.json 138 | let preprocess_data = PreProcessingData { 139 | version: package.info.version.to_string(), 140 | mod_id: package.info.id, 141 | mod_name: package.info.name, 142 | }; 143 | 144 | let mut existing_json = ModJson::read_and_preprocess(&preprocess_data); 145 | if let Some(is_library) = build_parameters.is_library { 146 | existing_json.is_library = Some(is_library); 147 | } 148 | 149 | // if it's a library, append to libraryFiles, else to modFiles 150 | if existing_json.is_library.unwrap_or(false) { 151 | existing_json.library_files.append(&mut mod_json.mod_files); 152 | } else { 153 | existing_json.mod_files.append(&mut mod_json.mod_files); 154 | } 155 | 156 | existing_json 157 | .dependencies 158 | .append(&mut mod_json.dependencies); 159 | existing_json 160 | .library_files 161 | .append(&mut mod_json.library_files); 162 | 163 | if let Some(excluded) = build_parameters.exclude_libs { 164 | let exclude_filter = |lib_name: &String| -> bool { 165 | // returning false means don't include 166 | // don't include anything that is excluded 167 | if excluded.iter().any(|s| lib_name == s) { 168 | return false; 169 | } 170 | 171 | true 172 | }; 173 | 174 | existing_json.mod_files.retain(exclude_filter); 175 | existing_json.library_files.retain(exclude_filter); 176 | } else if let Some(included) = build_parameters.include_libs { 177 | let include_filter = |lib_name: &String| -> bool { 178 | // returning false means don't include 179 | // only include anything that is specified included 180 | if included.iter().any(|s| lib_name == s) { 181 | return true; 182 | } 183 | 184 | false 185 | }; 186 | 187 | existing_json.mod_files.retain(include_filter); 188 | existing_json.library_files.retain(include_filter); 189 | } 190 | 191 | // handled by preprocessing 192 | // existing_json.id = mod_json.id; 193 | // existing_json.version = mod_json.version; 194 | 195 | // Write mod.json 196 | existing_json.write(PathBuf::from(ModJson::get_result_name())); 197 | } 198 | -------------------------------------------------------------------------------- /src/commands/restore.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use crate::data::{ 4 | config::Config, 5 | package::{PackageConfig, SharedPackageConfig}, repo::multi_provider::MultiDependencyProvider, 6 | }; 7 | 8 | pub fn execute_restore_operation() { 9 | println!("package should be restoring"); 10 | let package = PackageConfig::read(); 11 | let shared_package = SharedPackageConfig::from_package(&package); 12 | 13 | // create used dirs 14 | std::fs::create_dir_all("src").expect("Failed to create directory"); 15 | std::fs::create_dir_all("include").expect("Failed to create directory"); 16 | std::fs::create_dir_all(&shared_package.config.shared_dir).expect("Failed to create directory"); 17 | 18 | // write the ndk path to a file if available 19 | let config = Config::read_combine(); 20 | if let Some(ndk_path) = config.ndk_path { 21 | let mut file = std::fs::File::create("ndkpath.txt").expect("Failed to create ndkpath.txt"); 22 | file.write_all(ndk_path.as_bytes()) 23 | .expect("Failed to write out ndkpath.txt"); 24 | } 25 | 26 | shared_package.write(); 27 | if std::path::Path::new(&shared_package.config.dependencies_dir).exists() { 28 | // HACK: qpm rust is fast enough to where removing the folder and then remaking it is doable 29 | super::clear::remove_dependencies_dir(); 30 | } 31 | shared_package.restore(&MultiDependencyProvider::useful_default_new()); 32 | } 33 | -------------------------------------------------------------------------------- /src/data/config.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | 5 | #[derive(Serialize, Deserialize, Clone, Debug)] 6 | #[serde(rename_all = "camelCase")] 7 | pub struct Config { 8 | #[serde(skip_serializing_if = "Option::is_none")] 9 | pub symlink: Option, 10 | #[serde(skip_serializing_if = "Option::is_none")] 11 | pub cache: Option, 12 | #[serde(skip_serializing_if = "Option::is_none")] 13 | pub timeout: Option, 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | pub ndk_path: Option 16 | } 17 | 18 | impl Default for Config { 19 | #[inline] 20 | fn default() -> Config { 21 | Config { 22 | symlink: Some(true), 23 | cache: Some(dirs::data_dir().unwrap().join("QPM-Rust").join("cache")), 24 | timeout: Some(5000), 25 | ndk_path: None, 26 | } 27 | } 28 | } 29 | 30 | impl Config { 31 | /// always gets the global config 32 | pub fn read() -> Config { 33 | let path = Config::global_config_path(); 34 | std::fs::create_dir_all(Config::global_config_dir()).expect("Failed to make config folder"); 35 | 36 | if let Ok(file) = std::fs::File::open(path) { 37 | // existed 38 | serde_json::from_reader(file).expect("Deserializing global config failed") 39 | } else { 40 | // didn't exist 41 | Config { 42 | ..Default::default() 43 | } 44 | } 45 | } 46 | 47 | pub fn read_local() -> Config { 48 | let path = "qpm.settings.json"; 49 | if let Ok(file) = std::fs::File::open(path) { 50 | // existed 51 | serde_json::from_reader(file).expect(&format!("Deserializing {} failed", path)) 52 | } else { 53 | // didn't exist 54 | Config { 55 | symlink: None, 56 | cache: None, 57 | timeout: None, 58 | ndk_path: None, 59 | } 60 | } 61 | } 62 | 63 | /// combines the values of the global config with whatever is written in a local qpm.settings.json 64 | pub fn read_combine() -> Config { 65 | let mut config = Config::read(); 66 | 67 | // read a local qpm.settings.json to 68 | let local_path = "qpm.settings.json"; 69 | if let Ok(file) = std::fs::File::open(local_path) { 70 | 71 | 72 | let local_config: Config = 73 | serde_json::from_reader(file).expect("Deserializing package failed"); 74 | 75 | if local_config.symlink.is_some() { 76 | config.symlink = local_config.symlink; 77 | } 78 | if local_config.cache.is_some() { 79 | config.cache = local_config.cache; 80 | } 81 | if local_config.timeout.is_some() { 82 | config.timeout = local_config.timeout; 83 | } 84 | if local_config.ndk_path.is_some() { 85 | config.ndk_path = local_config.ndk_path; 86 | } 87 | } 88 | 89 | config 90 | } 91 | 92 | pub fn write(&self) { 93 | let path = Config::global_config_path(); 94 | 95 | std::fs::create_dir_all(Config::global_config_dir()).expect("Failed to make config folder"); 96 | let file = std::fs::File::create(path).expect("create failed"); 97 | serde_json::to_writer_pretty(file, &self).expect("Serialization failed"); 98 | 99 | println!("Saved Config!"); 100 | } 101 | 102 | pub fn write_local(&self) { 103 | std::fs::create_dir_all(Config::global_config_dir()).expect("Failed to make config folder"); 104 | let path = "qpm.settings.json"; 105 | let file = std::fs::File::create(path).expect("create failed"); 106 | 107 | serde_json::to_writer_pretty(file, &self).expect("Serialization failed"); 108 | println!("Saved Config!"); 109 | } 110 | 111 | pub fn global_config_path() -> PathBuf { 112 | Config::global_config_dir().join("qpm.settings.json") 113 | } 114 | 115 | pub fn global_config_dir() -> PathBuf { 116 | dirs::config_dir().unwrap().join("QPM-Rust") 117 | } 118 | } 119 | 120 | #[inline] 121 | pub fn get_keyring() -> keyring::Entry { 122 | keyring::Entry::new("qpm", "github") 123 | } 124 | #[inline] 125 | pub fn get_publish_keyring() -> keyring::Entry { 126 | keyring::Entry::new("qpm", "publish") 127 | } 128 | -------------------------------------------------------------------------------- /src/data/dependency/dependency_internal.rs: -------------------------------------------------------------------------------- 1 | use semver::VersionReq; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use super::AdditionalDependencyData; 5 | use crate::data::{package::SharedPackageConfig, qpackages}; 6 | 7 | #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] 8 | #[serde(rename_all = "camelCase")] 9 | pub struct Dependency { 10 | pub id: String, 11 | #[serde(deserialize_with = "cursed_semver_parser::deserialize")] 12 | pub version_range: VersionReq, 13 | pub additional_data: AdditionalDependencyData, 14 | } 15 | 16 | /* 17 | #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq, Default)] 18 | #[serde(rename_all = "camelCase")] 19 | pub struct AdditionalDependencyData { 20 | /// Copy a dependency from a location that is local to this root path instead of from a remote url 21 | #[serde(skip_serializing_if = "Option::is_none")] 22 | pub local_path: Option, 23 | 24 | /// Whether or not the package is header only 25 | #[serde(skip_serializing_if = "Option::is_none")] 26 | pub headers_only: Option, 27 | 28 | /// Whether or not the package is statically linked 29 | #[serde(skip_serializing_if = "Option::is_none")] 30 | pub static_linking: Option, 31 | 32 | /// Whether to use the release or debug .so for linking 33 | #[serde(skip_serializing_if = "Option::is_none")] 34 | pub use_release: Option, 35 | 36 | /// the link to the so file 37 | #[serde(skip_serializing_if = "Option::is_none")] 38 | pub so_link: Option, 39 | 40 | /// the link to the debug .so file 41 | #[serde(skip_serializing_if = "Option::is_none")] 42 | pub debug_so_link: Option, 43 | 44 | /// the overridden so file name 45 | #[serde(skip_serializing_if = "Option::is_none")] 46 | pub override_so_name: Option, 47 | 48 | /// the link to the qmod 49 | #[serde(skip_serializing_if = "Option::is_none")] 50 | pub mod_link: Option, 51 | 52 | /// Branch name of a Github repo. Only used when a valid github url is provided 53 | #[serde(skip_serializing_if = "Option::is_none")] 54 | pub branch_name: Option, 55 | 56 | /// Specify any additional files to be downloaded 57 | #[serde(skip_serializing_if = "Option::is_none")] 58 | pub extra_files: Option>, 59 | 60 | /// Whether or not the dependency is private and should be used in restore 61 | #[serde( 62 | skip_serializing_if = "Option::is_none", 63 | rename(serialize = "private", deserialize = "private") 64 | )] 65 | pub is_private: Option, 66 | } 67 | 68 | */ 69 | /* 70 | impl Default for AdditionalDependencyData { 71 | fn default() -> Self { 72 | Self { 73 | local_path: None, 74 | headers_only: None, 75 | static_linking: None, 76 | use_release: None, 77 | so_link: None, 78 | debug_so_link: None, 79 | override_so_name: None, 80 | mod_link: None, 81 | branch_name: None, 82 | extra_files: None, 83 | is_private: None, 84 | } 85 | } 86 | } 87 | */ 88 | /* 89 | impl AdditionalDependencyData { 90 | pub fn merge(&mut self, other: AdditionalDependencyData) { 91 | if self.branch_name.is_none() { 92 | if let Some(other_branch_name) = &other.branch_name { 93 | self.branch_name = Some(other_branch_name.clone()); 94 | } 95 | } 96 | 97 | if let (Some(extra_files), Some(other_extra_files)) = 98 | (&mut self.extra_files, &other.extra_files) 99 | { 100 | extra_files.append(&mut other_extra_files.clone()); 101 | } else if self.extra_files.is_none() { 102 | if let Some(other_extra_files) = &other.extra_files { 103 | self.extra_files = Some(other_extra_files.clone()); 104 | } 105 | } 106 | 107 | if self.local_path.is_none() { 108 | if let Some(other_local_path) = &other.local_path { 109 | self.local_path = Some(other_local_path.clone()); 110 | } 111 | } 112 | 113 | if let (Some(is_private), Some(other_is_private)) = (&self.is_private, &other.is_private) { 114 | self.is_private = Some(*is_private || *other_is_private); 115 | } else if self.is_private.is_none() { 116 | if let Some(other_is_private) = &other.is_private { 117 | self.is_private = Some(*other_is_private); 118 | } 119 | } 120 | } 121 | 122 | pub fn merge_package(&mut self, other: AdditionalPackageData) { 123 | if let Some(static_linking) = other.static_linking { 124 | self.static_linking = Some(static_linking); 125 | } 126 | 127 | if self.mod_link.is_none() { 128 | self.mod_link = other.mod_link; 129 | } 130 | } 131 | } 132 | */ 133 | 134 | impl Dependency { 135 | pub fn get_shared_package(&self) -> Option { 136 | let versions = qpackages::get_versions(&self.id).expect("Unable to get versions"); 137 | for v in versions.iter() { 138 | if self.version_range.matches(&v.version) { 139 | return qpackages::get_shared_package(&self.id, &v.version); 140 | } 141 | } 142 | 143 | Option::None 144 | } 145 | } 146 | 147 | /* 148 | impl From for AdditionalDependencyData { 149 | fn from(package_data: AdditionalPackageData) -> Self { 150 | serde_json::from_str(&serde_json::to_string(&package_data).unwrap()).unwrap() 151 | } 152 | } 153 | */ 154 | -------------------------------------------------------------------------------- /src/data/dependency/mod.rs: -------------------------------------------------------------------------------- 1 | mod dependency_internal; 2 | /// A dependency is a library that you want to use for your mod 3 | pub type Dependency = dependency_internal::Dependency; 4 | /// Additional dependency data can be used to configure a dependency a certain way to make it work the way you want it to 5 | /// 6 | /// Really it's the same as AdditionalPackageData though 7 | pub type AdditionalDependencyData = crate::data::package::AdditionalPackageData; 8 | 9 | mod shared_dependency; 10 | /// A shared dependency is a dependency that, when someone wants to use your lib as a dependency, is used for more dependency resolution. 11 | /// 12 | /// It is also used to check out how it was configured when you generated the library 13 | pub type SharedDependency = shared_dependency::SharedDependency; 14 | -------------------------------------------------------------------------------- /src/data/dependency/shared_dependency.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::Cursor, 4 | path::{Path, PathBuf}, 5 | }; 6 | 7 | use fs_extra::{dir::copy as copy_directory, file::copy as copy_file}; 8 | use owo_colors::OwoColorize; 9 | use remove_dir_all::remove_dir_all; 10 | use semver::{Version, VersionReq}; 11 | use serde::{Deserialize, Serialize}; 12 | use zip::ZipArchive; 13 | 14 | use super::Dependency; 15 | use crate::{ 16 | data::{ 17 | config::Config, 18 | package::{PackageConfig, SharedPackageConfig}, 19 | }, 20 | utils::{git, network::get_agent}, 21 | }; 22 | 23 | #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] 24 | #[serde(rename_all = "camelCase")] 25 | pub struct SharedDependency { 26 | pub dependency: Dependency, 27 | pub version: Version, 28 | } 29 | 30 | impl SharedDependency { 31 | pub fn get_so_name(&self) -> String { 32 | self.dependency 33 | .additional_data 34 | .override_so_name 35 | .clone() 36 | .unwrap_or(format!( 37 | "lib{}_{}.{}", 38 | self.dependency.id, 39 | self.version.to_string().replace('.', "_"), 40 | if self 41 | .dependency 42 | .additional_data 43 | .static_linking 44 | .unwrap_or(false) 45 | { 46 | "a" 47 | } else { 48 | "so" 49 | }, 50 | )) 51 | } 52 | 53 | pub fn cache(&self, shared_package: &SharedPackageConfig) { 54 | // Check if already cached 55 | // if true, don't download repo / header files 56 | // else cache to tmp folder in package id folder @ cache path 57 | // git repo -> git clone w/ or without github token 58 | // not git repo (no github.com) -> assume it's a zip 59 | // !! HANDLE SUBFOLDER FROM TMP, OR IF NO SUBFOLDER JUST RENAME TMP TO SRC !! 60 | // -- now we have the header files -- 61 | // Check if .so files are downloaded, if not: 62 | // Download release .so and possibly debug .so to libs folder, if from github use token if available 63 | // Now it should be cached! 64 | 65 | println!( 66 | "Checking cache for dependency {} {}", 67 | self.dependency.id.bright_red(), 68 | self.version.bright_green() 69 | ); 70 | let config = Config::read_combine(); 71 | let base_path = config 72 | .cache 73 | .unwrap() 74 | .join(&self.dependency.id) 75 | .join(self.version.to_string()); 76 | 77 | let src_path = base_path.join("src"); 78 | let lib_path = base_path.join("lib"); 79 | let tmp_path = base_path.join("tmp"); 80 | 81 | let so_path = lib_path.join(shared_package.config.get_so_name()); 82 | let debug_so_path = lib_path.join(format!("debug_{}", shared_package.config.get_so_name())); 83 | 84 | // Downloads the repo / zip file into src folder w/ subfolder taken into account 85 | if !src_path.exists() { 86 | // if the tmp path exists, but src doesn't, that's a failed cache, delete it and try again! 87 | if tmp_path.exists() { 88 | remove_dir_all(&tmp_path).expect("Failed to remove existing tmp folder"); 89 | } 90 | 91 | // src did not exist, this means that we need to download the repo/zip file from packageconfig.info.url 92 | std::fs::create_dir_all(&src_path.parent().unwrap()) 93 | .expect("Failed to create lib path"); 94 | let url = shared_package.config.info.url.as_ref().unwrap(); 95 | if url.contains("github.com") { 96 | // github url! 97 | git::clone( 98 | url.clone(), 99 | shared_package 100 | .config 101 | .info 102 | .additional_data 103 | .branch_name 104 | .as_ref(), 105 | &tmp_path, 106 | ); 107 | } else { 108 | // not a github url, assume it's a zip 109 | let response = get_agent().get(url).send().unwrap(); 110 | 111 | let buffer = Cursor::new(response.bytes().unwrap()); 112 | // Extract to tmp folder 113 | ZipArchive::new(buffer).unwrap().extract(&tmp_path).unwrap(); 114 | } 115 | // the only way the above if else would break is if someone put a link to a zip file on github in the url slot 116 | // if you are reading this and think of doing that so I have to fix this, fuck you 117 | 118 | let from_path = 119 | if let Some(sub_folder) = &shared_package.config.info.additional_data.sub_folder { 120 | // the package exists in a subfolder of the downloaded thing, just move the subfolder to src 121 | tmp_path.join(sub_folder) 122 | } else { 123 | // the downloaded thing IS the package, just rename the folder to src 124 | tmp_path.clone() 125 | }; 126 | 127 | if from_path.exists() { 128 | // only log this on debug builds 129 | #[cfg(debug_assertions)] 130 | println!( 131 | "from: {}\nto: {}", 132 | from_path.display().bright_yellow(), 133 | src_path.display().bright_yellow() 134 | ); 135 | 136 | if src_path.exists() { 137 | let mut line = String::new(); 138 | println!( 139 | "Confirm deletion of folder {}: (y/N)", 140 | src_path.display().bright_yellow() 141 | ); 142 | let _ = std::io::stdin().read_line(&mut line).unwrap(); 143 | if line.starts_with('y') || line.starts_with('Y') { 144 | remove_dir_all(&src_path).expect("Failed to remove existing src folder"); 145 | } 146 | } 147 | // HACK: renaming seems to work, idk if it works for actual subfolders? 148 | std::fs::rename(&from_path, &src_path).expect("Failed to move folder"); 149 | } else { 150 | panic!("Failed to restore folder for this dependency\nif you have a token configured check if it's still valid\nIf it is, check if you can manually reach the repo"); 151 | } 152 | 153 | // clear up tmp folder if it still exists 154 | if tmp_path.exists() { 155 | std::fs::remove_dir_all(tmp_path).expect("Failed to remove tmp folder"); 156 | } 157 | let package_path = src_path.join("qpm.json"); 158 | let downloaded_package = PackageConfig::read_path(package_path); 159 | 160 | // check if downloaded config is the same version as expected, if not, panic 161 | if downloaded_package.info.version != self.version { 162 | panic!( 163 | "Downloaded package ({}) version ({}) does not match expected version ({})!", 164 | self.dependency.id.bright_red(), 165 | downloaded_package.info.version.to_string().bright_green(), 166 | self.version.to_string().bright_green(), 167 | ) 168 | } 169 | } 170 | 171 | if !lib_path.exists() { 172 | std::fs::create_dir_all(&lib_path).expect("Failed to create lib path"); 173 | // libs didn't exist or the release object didn't exist, we need to download from packageconfig.info.additional_data.so_link and packageconfig.info.additional_data.debug_so_link 174 | if !so_path.exists() || File::open(&so_path).is_err() { 175 | if let Some(so_link) = &shared_package.config.info.additional_data.so_link { 176 | // so_link existed, download 177 | if so_link.contains("github.com") { 178 | // github url! 179 | git::get_release(so_link, &so_path); 180 | } else { 181 | let mut response = get_agent() 182 | .get(so_link) 183 | .send() 184 | .expect("Unable to download so file"); 185 | 186 | // other dl link, assume it's a raw lib file download 187 | let mut file = 188 | std::fs::File::create(so_path).expect("create so file failed"); 189 | 190 | response 191 | .copy_to(&mut file) 192 | .expect("Failed to write out downloaded bytes"); 193 | } 194 | } 195 | } 196 | 197 | if !debug_so_path.exists() || File::open(&debug_so_path).is_err() { 198 | if let Some(debug_so_link) = 199 | &shared_package.config.info.additional_data.debug_so_link 200 | { 201 | // debug_so_link existed, download 202 | if debug_so_link.contains("github.com") { 203 | // github url! 204 | git::get_release(debug_so_link, &debug_so_path); 205 | } else { 206 | // other dl link, assume it's a raw lib file download 207 | let mut response = get_agent() 208 | .get(debug_so_link) 209 | .send() 210 | .expect("Unable to download debug so file"); 211 | 212 | let mut file = 213 | std::fs::File::create(debug_so_path).expect("create so file failed"); 214 | 215 | response 216 | .copy_to(&mut file) 217 | .expect("Failed to write out downloaded bytes"); 218 | } 219 | } 220 | } 221 | } 222 | } 223 | 224 | pub fn restore_from_cache(&self, also_lib: bool, shared_package: &SharedPackageConfig) { 225 | // restore from cached files, give error on fail (nonexistent?) 226 | if Config::read_combine().symlink.unwrap_or(false) { 227 | self.restore_from_cache_symlink(also_lib, shared_package); 228 | } else { 229 | self.restore_from_cache_copy(also_lib, shared_package); 230 | } 231 | } 232 | 233 | pub fn collect_to_copy( 234 | &self, 235 | also_lib: bool, 236 | shared_package: &SharedPackageConfig, 237 | ) -> Vec<(PathBuf, PathBuf)> { 238 | // TODO: Look into improving the way it gets all the things to copy 239 | // low priority since this also works 240 | let config = Config::read_combine(); 241 | let package = PackageConfig::read(); 242 | 243 | let base_path = config 244 | .cache 245 | .unwrap() 246 | .join(&self.dependency.id) 247 | .join(self.version.to_string()); 248 | let src_path = base_path.join("src"); 249 | let libs_path = base_path.join("lib"); 250 | let dependencies_path = Path::new(&package.dependencies_dir); 251 | std::fs::create_dir_all(dependencies_path).unwrap(); 252 | let dependencies_path = dependencies_path.canonicalize().unwrap().join("includes"); 253 | let local_path = dependencies_path.join(&self.dependency.id); 254 | let mut to_copy = Vec::new(); 255 | if also_lib { 256 | let use_release = if self.dependency.additional_data.use_release.is_some() { 257 | self.dependency.additional_data.use_release.unwrap() 258 | } else if let Some(local_dep) = package 259 | .dependencies 260 | .iter() 261 | .find(|el| el.id == self.dependency.id) 262 | { 263 | local_dep.additional_data.use_release.unwrap_or(false) 264 | } else { 265 | false 266 | }; 267 | 268 | let prefix = if !use_release { "debug_" } else { "" }; 269 | 270 | let suffix = if let Some(override_so_name) = shared_package 271 | .config 272 | .info 273 | .additional_data 274 | .override_so_name 275 | .clone() 276 | { 277 | override_so_name 278 | } else { 279 | format!( 280 | "lib{}_{}.so", 281 | self.dependency.id, 282 | self.version.to_string().replace('.', "_") 283 | ) 284 | }; 285 | 286 | let mut so_name: String = format!("{}{}", prefix, suffix); 287 | 288 | // if not headers only, copy over .so file 289 | if shared_package 290 | .config 291 | .info 292 | .additional_data 293 | .headers_only 294 | .is_none() 295 | || !shared_package 296 | .config 297 | .info 298 | .additional_data 299 | .headers_only 300 | .unwrap() 301 | { 302 | let mut lib_so_path = libs_path.join(&so_name); 303 | // if it doesn't exist, use it without debug 304 | if !lib_so_path.exists() { 305 | #[cfg(debug_assertions)] 306 | println!( 307 | "Path {} did not exist, editing to remove debug_", 308 | lib_so_path.display().bright_yellow() 309 | ); 310 | 311 | so_name = so_name.replace("debug_", ""); 312 | lib_so_path = libs_path.join(&so_name); 313 | } 314 | 315 | let local_so_path = Path::new(&package.dependencies_dir) 316 | .canonicalize() 317 | .unwrap() 318 | .join("libs") 319 | .join(&so_name.replace("debug_", "")); 320 | // from to 321 | to_copy.push((lib_so_path, local_so_path)); 322 | } 323 | } 324 | // copy shared / include over 325 | let cache_shared_path = src_path.join(&shared_package.config.shared_dir); 326 | let shared_path = local_path.join(&shared_package.config.shared_dir); 327 | to_copy.push((cache_shared_path, shared_path)); 328 | 329 | if let Some(extra_files) = &self.dependency.additional_data.extra_files { 330 | for entry in extra_files.iter() { 331 | let cache_entry_path = src_path.join(entry); 332 | let entry_path = local_path.join(entry); 333 | to_copy.push((cache_entry_path, entry_path)); 334 | } 335 | } 336 | 337 | let local_shared_package = SharedPackageConfig::read(); 338 | if let Some(dep) = local_shared_package 339 | .config 340 | .dependencies 341 | .iter() 342 | .find(|el| el.id == self.dependency.id) 343 | { 344 | if let Some(extra_files) = &dep.additional_data.extra_files { 345 | for entry in extra_files.iter() { 346 | let cache_entry_path = src_path.join(entry); 347 | let entry_path = local_path.join(entry); 348 | to_copy.push((cache_entry_path, entry_path)); 349 | } 350 | } 351 | } 352 | 353 | to_copy 354 | } 355 | 356 | pub fn restore_from_cache_symlink(&self, also_lib: bool, shared_package: &SharedPackageConfig) { 357 | let to_copy = self.collect_to_copy(also_lib, shared_package); 358 | // sort out issues with the symlinking, stuff is being symlinked weirdly 359 | for (from, to) in to_copy.iter() { 360 | #[cfg(debug_assertions)] 361 | println!( 362 | "symlinking\nfrom {}\nto {}", 363 | from.display().bright_yellow(), 364 | to.display().bright_yellow() 365 | ); 366 | 367 | // make sure to parent dir exists! 368 | std::fs::create_dir_all(to.parent().unwrap()).expect("Failed to create parent folder"); 369 | if let Err(e) = symlink::symlink_auto(&from, &to) { 370 | #[cfg(windows)] 371 | println!("Failed to create symlink: {}\nfalling back to copy, did the link already exist, or did you not enable windows dev mode?\nTo disable this warning (and default to copy), use the command {}", e.bright_red(), "qpm config symlink disable".bright_yellow()); 372 | #[cfg(not(windows))] 373 | println!("Failed to create symlink: {}\nfalling back to copy, did the link already exist?\nTo disable this warning (and default to copy), use the command {}", e.bright_red(), "qpm config symlink disable".bright_yellow()); 374 | 375 | if from.is_dir() { 376 | let mut options = fs_extra::dir::CopyOptions::new(); 377 | options.overwrite = true; 378 | options.copy_inside = true; 379 | options.content_only = true; 380 | options.skip_exist = true; 381 | copy_directory(&from, &to, &options).unwrap_or_else(|_| { 382 | panic!("Failed to copy directory! From {:#?} To {:#?}", &from, &to) 383 | }); // ignore warning, let it raise the error for more details. 384 | } else if from.is_file() { 385 | // we can get the parent beccause this is a file path 386 | std::fs::create_dir_all(&to.parent().unwrap()) 387 | .expect("Failed to create containing directory"); 388 | std::fs::copy(&from, &to).expect("Failed to copy file!"); 389 | } 390 | } 391 | } 392 | } 393 | 394 | pub fn restore_from_cache_copy(&self, also_lib: bool, shared_package: &SharedPackageConfig) { 395 | // get the files to copy 396 | let to_copy = self.collect_to_copy(also_lib, shared_package); 397 | for (from_str, to_str) in to_copy.iter() { 398 | let from = Path::new(&from_str); 399 | let to = Path::new(&to_str); 400 | 401 | #[cfg(debug_assertions)] 402 | println!( 403 | "copying\nfrom {}\nto {}", 404 | from.display().bright_yellow(), 405 | to.display().bright_yellow() 406 | ); 407 | 408 | // make sure to parent dir exists! 409 | std::fs::create_dir_all(to.parent().unwrap()).expect("Failed to create parent folder"); 410 | // if dir, make sure it exists 411 | if !from.exists() { 412 | println!("The file or folder\n\t'{}'\ndid not exist! what happened to the cache? you should probably run {} to make sure everything is in order...", from.display().bright_yellow(), "qpm cache clear".bright_yellow()); 413 | } else if from.is_dir() { 414 | std::fs::create_dir_all(&to).expect("Failed to create destination folder"); 415 | let mut options = fs_extra::dir::CopyOptions::new(); 416 | options.overwrite = true; 417 | options.copy_inside = true; 418 | options.content_only = true; 419 | // copy it over 420 | copy_directory(&from, &to, &options).expect("Failed to copy directory!"); 421 | } else if from.is_file() { 422 | // if it's a file, copy that over instead 423 | let mut options = fs_extra::file::CopyOptions::new(); 424 | options.overwrite = true; 425 | copy_file(&from, &to, &options).expect("Failed to copy file!"); 426 | } 427 | } 428 | } 429 | } 430 | 431 | impl From for SharedDependency { 432 | fn from(shared_package: SharedPackageConfig) -> Self { 433 | let package_config = PackageConfig::read(); 434 | let version_range = if let Some(orig) = package_config 435 | .dependencies 436 | .iter() 437 | .find(|el| el.id == shared_package.config.info.id) 438 | { 439 | orig.version_range.clone() 440 | } else { 441 | VersionReq::parse(&format!("^{}", shared_package.config.info.version)).unwrap() 442 | }; 443 | 444 | SharedDependency { 445 | dependency: Dependency { 446 | id: shared_package.config.info.id.to_string(), 447 | version_range, 448 | additional_data: shared_package.config.info.additional_data, 449 | }, 450 | version: shared_package.config.info.version, 451 | } 452 | } 453 | } 454 | -------------------------------------------------------------------------------- /src/data/file_repository.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fs, 4 | io::{Read, Write}, 5 | path::PathBuf, 6 | }; 7 | 8 | use fs_extra::{dir::copy as copy_directory, file::copy as copy_file}; 9 | use owo_colors::OwoColorize; 10 | use remove_dir_all::remove_dir_all; 11 | use semver::Version; 12 | use serde::{Deserialize, Serialize}; 13 | 14 | use super::package::SharedPackageConfig; 15 | use crate::data::{config::Config, package::PackageConfig}; 16 | 17 | // TODO: Somehow make a global singleton of sorts/cached instance to share across places 18 | // like resolver 19 | #[derive(Serialize, Deserialize, Clone, Debug, Default)] 20 | pub struct FileRepository { 21 | pub artifacts: HashMap>, 22 | } 23 | 24 | impl FileRepository { 25 | pub fn get_artifacts_from_id( 26 | &self, 27 | id: &str, 28 | ) -> Option<&HashMap> { 29 | self.artifacts.get(id) 30 | } 31 | 32 | pub fn get_artifact(&self, id: &str, version: &Version) -> Option<&SharedPackageConfig> { 33 | match self.artifacts.get(id) { 34 | Some(artifacts) => artifacts.get(version), 35 | None => None, 36 | } 37 | } 38 | 39 | pub fn add_artifact( 40 | &mut self, 41 | package: SharedPackageConfig, 42 | project_folder: PathBuf, 43 | binary_path: Option, 44 | debug_binary_path: Option, 45 | ) { 46 | if !self.artifacts.contains_key(&package.config.info.id) { 47 | self.artifacts 48 | .insert(package.config.info.id.clone(), HashMap::new()); 49 | } 50 | 51 | Self::add_to_cache(&package, project_folder, binary_path, debug_binary_path); 52 | 53 | let id_artifacts = self.artifacts.get_mut(&package.config.info.id).unwrap(); 54 | 55 | id_artifacts.insert(package.config.info.version.clone(), package); 56 | } 57 | 58 | fn copy_to_cache(a: &PathBuf, b: &PathBuf) { 59 | if a.is_dir() { 60 | fs::create_dir_all(&b) 61 | .unwrap_or_else(|e| panic!("Failed to create {b:?} path. Cause {e:?}")); 62 | } else { 63 | let parent = b.parent().unwrap(); 64 | fs::create_dir_all(parent) 65 | .unwrap_or_else(|e| panic!("Failed to create {parent:?} path. Cause {e:?}")); 66 | } 67 | 68 | let result = if a.is_dir() { 69 | let mut options = fs_extra::dir::CopyOptions::new(); 70 | options.overwrite = true; 71 | options.copy_inside = true; 72 | options.content_only = true; 73 | // copy it over 74 | copy_directory(a, b, &options) 75 | } else { 76 | // if it's a file, copy that over instead 77 | let mut options = fs_extra::file::CopyOptions::new(); 78 | options.overwrite = true; 79 | copy_file(a, b, &options) 80 | }; 81 | 82 | result.unwrap_or_else(|e| panic!("Unable to copy from {:?} to {:?}. Cause {e:?}", a, b)); 83 | } 84 | 85 | fn add_to_cache( 86 | package: &SharedPackageConfig, 87 | project_folder: PathBuf, 88 | binary_path: Option, 89 | debug_binary_path: Option, 90 | ) { 91 | println!( 92 | "Adding cache for local dependency {} {}", 93 | package.config.info.id.bright_red(), 94 | package.config.info.version.bright_green() 95 | ); 96 | let config = Config::read_combine(); 97 | let cache_path = config 98 | .cache 99 | .unwrap() 100 | .join(&package.config.info.id) 101 | .join(package.config.info.version.to_string()); 102 | 103 | let src_path = cache_path.join("src"); 104 | 105 | let tmp_path = cache_path.join("tmp"); 106 | 107 | // Downloads the repo / zip file into src folder w/ subfolder taken into account 108 | 109 | // if the tmp path exists, but src doesn't, that's a failed cache, delete it and try again! 110 | if tmp_path.exists() { 111 | remove_dir_all(&tmp_path).expect("Failed to remove existing tmp folder"); 112 | } 113 | 114 | if src_path.exists() { 115 | remove_dir_all(&src_path).expect("Failed to remove existing src folder"); 116 | } 117 | 118 | fs::create_dir_all(&src_path).expect("Failed to create lib path"); 119 | 120 | if binary_path.is_some() || debug_binary_path.is_some() { 121 | let lib_path = cache_path.join("lib"); 122 | let so_path = lib_path.join(package.config.get_so_name()); 123 | let debug_so_path = lib_path.join(format!("debug_{}", package.config.get_so_name())); 124 | 125 | if let Some(binary_path_unwrapped) = &binary_path { 126 | Self::copy_to_cache(binary_path_unwrapped, &so_path); 127 | } 128 | 129 | if let Some(debug_binary_path_unwrapped) = &debug_binary_path { 130 | Self::copy_to_cache(debug_binary_path_unwrapped, &debug_so_path); 131 | } 132 | } 133 | 134 | let original_shared_path = project_folder.join(&package.config.shared_dir); 135 | let original_package_file_path = project_folder.join("qpm.json"); 136 | 137 | Self::copy_to_cache( 138 | &original_shared_path, 139 | &src_path.join(&package.config.shared_dir), 140 | ); 141 | Self::copy_to_cache(&original_package_file_path, &src_path.join("qpm.json")); 142 | 143 | let package_path = src_path.join("qpm.json"); 144 | let downloaded_package = PackageConfig::read_path(package_path); 145 | 146 | // check if downloaded config is the same version as expected, if not, panic 147 | if downloaded_package.info.version != package.config.info.version { 148 | panic!( 149 | "Downloaded package ({}) version ({}) does not match expected version ({})!", 150 | package.config.info.id.bright_red(), 151 | downloaded_package.info.version.to_string().bright_green(), 152 | package.config.info.version.to_string().bright_green(), 153 | ) 154 | } 155 | } 156 | 157 | /// always gets the global config 158 | pub fn read() -> Self { 159 | let path = Self::global_file_repository_path(); 160 | std::fs::create_dir_all(Self::global_repository_dir()) 161 | .expect("Failed to make config folder"); 162 | 163 | if let Ok(mut file) = std::fs::File::open(path) { 164 | // existed 165 | let mut config_str = String::new(); 166 | file.read_to_string(&mut config_str) 167 | .expect("Reading data failed"); 168 | 169 | serde_json::from_str::(&config_str).expect("Deserializing package failed") 170 | } else { 171 | // didn't exist 172 | Self { 173 | ..Default::default() 174 | } 175 | } 176 | } 177 | 178 | pub fn write(&self) { 179 | let config = serde_json::to_string_pretty(&self).expect("Serialization failed"); 180 | let path = Self::global_file_repository_path(); 181 | 182 | std::fs::create_dir_all(Self::global_repository_dir()) 183 | .expect("Failed to make config folder"); 184 | let mut file = std::fs::File::create(path).expect("create failed"); 185 | file.write_all(config.as_bytes()).expect("write failed"); 186 | println!("Saved local repository Config!"); 187 | } 188 | 189 | pub fn global_file_repository_path() -> PathBuf { 190 | Self::global_repository_dir().join("qpm.repository.json") 191 | } 192 | 193 | pub fn global_repository_dir() -> PathBuf { 194 | dirs::config_dir().unwrap().join("QPM-Rust") 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/data/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod dependency; 3 | pub mod mod_json; 4 | pub mod package; 5 | pub mod qpackages; 6 | pub mod file_repository; 7 | pub mod repo; -------------------------------------------------------------------------------- /src/data/mod_json.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{BufReader, Read}, 3 | path::PathBuf, collections::HashSet, 4 | }; 5 | 6 | use semver::{Version, VersionReq}; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::data::{ 10 | dependency::{Dependency, SharedDependency}, 11 | package::SharedPackageConfig, 12 | }; 13 | 14 | // TODO: Idea for later, maybe some kind of config that stores defaults for the different fields, like description and author? 15 | #[derive(Serialize, Deserialize, Clone, Debug)] 16 | #[serde(rename_all = "camelCase")] 17 | #[serde(default)] // skip missing fields 18 | pub struct ModJson { 19 | /// The Questpatcher version this mod.json was made for 20 | #[serde(rename(serialize = "_QPVersion", deserialize = "_QPVersion"))] 21 | pub schema_version: Version, 22 | /// Name of the mod 23 | pub name: String, 24 | /// ID of the mod 25 | pub id: String, 26 | /// Author of the mod 27 | pub author: String, 28 | /// Optional slot for if you ported a mod 29 | #[serde(skip_serializing_if = "Option::is_none")] 30 | pub porter: Option, 31 | /// Mod version 32 | pub version: String, 33 | /// id of the package the mod is for, ex. com.beatgaems.beatsaber 34 | #[serde(skip_serializing_if = "Option::is_none")] 35 | pub package_id: Option, 36 | /// Version of the package, ex. 1.1.0 37 | #[serde(skip_serializing_if = "Option::is_none")] 38 | pub package_version: Option, 39 | /// description for the mod 40 | #[serde(skip_serializing_if = "Option::is_none")] 41 | pub description: Option, 42 | /// optional cover image filename 43 | #[serde(skip_serializing_if = "Option::is_none")] 44 | pub cover_image: Option, 45 | /// whether or not this qmod is a library or not 46 | #[serde(skip_serializing_if = "Option::is_none")] 47 | pub is_library: Option, 48 | /// list of downloadable dependencies 49 | pub dependencies: Vec, 50 | /// list of files that go in the package's mod folder 51 | pub mod_files: Vec, 52 | /// list of files that go in the package's libs folder 53 | pub library_files: Vec, 54 | /// list of files that will be copied on the quest 55 | pub file_copies: Vec, 56 | /// list of copy extensions registered for this specific mod 57 | pub copy_extensions: Vec, 58 | } 59 | 60 | impl Default for ModJson { 61 | fn default() -> Self { 62 | Self { 63 | schema_version: Version::new(1, 0, 0), 64 | name: Default::default(), 65 | id: Default::default(), 66 | author: Default::default(), 67 | porter: Default::default(), 68 | version: Default::default(), 69 | package_id: Default::default(), 70 | package_version: Default::default(), 71 | description: Default::default(), 72 | cover_image: Default::default(), 73 | is_library: Default::default(), 74 | dependencies: Default::default(), 75 | mod_files: Default::default(), 76 | library_files: Default::default(), 77 | file_copies: Default::default(), 78 | copy_extensions: Default::default(), 79 | } 80 | } 81 | } 82 | 83 | #[derive(Serialize, Deserialize, Clone, Debug, Default)] 84 | #[serde(rename_all = "camelCase")] 85 | pub struct ModDependency { 86 | /// the version requirement for this dependency 87 | #[serde(deserialize_with = "cursed_semver_parser::deserialize")] 88 | #[serde(rename = "version")] 89 | pub version_range: VersionReq, 90 | /// the id of this dependency 91 | pub id: String, 92 | /// the download link for this dependency, must satisfy id and version range! 93 | #[serde(skip_serializing_if = "Option::is_none")] 94 | #[serde(rename = "downloadIfMissing")] 95 | pub mod_link: Option, 96 | } 97 | 98 | #[derive(Serialize, Deserialize, Clone, Debug, Default)] 99 | #[serde(rename_all = "camelCase")] 100 | pub struct FileCopy { 101 | /// name of the file in the qmod 102 | pub name: String, 103 | /// place where to put it (full path) 104 | pub destination: String, 105 | } 106 | 107 | #[derive(Serialize, Deserialize, Clone, Debug, Default)] 108 | #[serde(rename_all = "camelCase")] 109 | pub struct CopyExtension { 110 | /// the extension to register for 111 | pub extension: String, 112 | /// the destination folder these files should be going to 113 | pub destination: String, 114 | } 115 | 116 | pub struct PreProcessingData { 117 | pub version: String, 118 | pub mod_id: String, 119 | pub mod_name: String, 120 | } 121 | 122 | impl ModJson { 123 | pub fn get_template_name() -> &'static str { 124 | "mod.template.json" 125 | } 126 | 127 | pub fn get_result_name() -> &'static str { 128 | "mod.json" 129 | } 130 | 131 | pub fn get_template_path() -> std::path::PathBuf { 132 | std::path::PathBuf::new() 133 | .join(&Self::get_template_name()) 134 | .canonicalize() 135 | .unwrap() 136 | } 137 | 138 | pub fn read_and_preprocess(preprocess_data: &PreProcessingData) -> Self { 139 | let mut file = 140 | std::fs::File::open(Self::get_template_name()).expect("Opening mod.json failed"); 141 | 142 | // Get data 143 | let mut json = String::new(); 144 | file.read_to_string(&mut json).expect("Reading data failed"); 145 | 146 | // Pre process 147 | let processsed = Self::preprocess(json, preprocess_data); 148 | 149 | serde_json::from_str(&processsed).expect("Deserializing package failed") 150 | } 151 | 152 | fn preprocess(s: String, preprocess_data: &PreProcessingData) -> String { 153 | s.replace("${version}", preprocess_data.version.as_str()) 154 | .replace("${mod_id}", preprocess_data.mod_id.as_str()) 155 | .replace("${mod_name}", preprocess_data.mod_name.as_str()) 156 | } 157 | 158 | pub fn read(path: PathBuf) -> ModJson { 159 | let file = std::fs::File::open(path).expect("Opening mod.json failed"); 160 | let reader = BufReader::new(file); 161 | 162 | serde_json::from_reader(reader).expect("Deserializing package failed") 163 | } 164 | 165 | pub fn write(&self, path: PathBuf) { 166 | let file = std::fs::File::create(path).expect("create failed"); 167 | serde_json::to_writer_pretty(file, self).expect("Write failed"); 168 | } 169 | } 170 | 171 | impl From for ModJson { 172 | fn from(mut shared_package: SharedPackageConfig) -> Self { 173 | let local_deps = &shared_package.config.dependencies; 174 | 175 | // Only bundle mods that are not specifically excluded in qpm.json or if they're not header-only 176 | shared_package.restored_dependencies.retain(|dep| { 177 | let local_dep_opt = local_deps.iter().find(|local_dep| local_dep.id == dep.dependency.id); 178 | 179 | if let Some(local_dep) = local_dep_opt { 180 | // if force included/excluded, return early 181 | if let Some(force_included) = local_dep.additional_data.include_qmod { 182 | return force_included; 183 | } 184 | } 185 | 186 | // or if header only is false 187 | !dep.dependency.additional_data.headers_only.unwrap_or(false) 188 | }); 189 | 190 | // List of dependencies we are directly referencing in qpm.json 191 | let direct_dependencies: HashSet = shared_package.config.dependencies.iter().map(|f| f.id.clone()).collect(); 192 | 193 | 194 | // downloadable mods links n stuff 195 | // mods that are header-only but provide qmods can be added as deps 196 | let mods: Vec = shared_package 197 | .restored_dependencies 198 | .iter() 199 | // Removes any dependency without a qmod link 200 | .filter(|dep| 201 | // Must be directly referenced in qpm.json 202 | direct_dependencies.contains(&dep.dependency.id) && 203 | 204 | dep.dependency.additional_data.mod_link.is_some()) 205 | .map(|dep| dep.clone().into()) 206 | .collect(); 207 | 208 | 209 | // The rest of the mods to handle are not qmods, they are .so or .a mods 210 | // actual direct lib deps 211 | let libs = shared_package 212 | .restored_dependencies 213 | .iter() 214 | // We could just query the bmbf core mods list on GH? 215 | // https://github.com/BMBF/resources/blob/master/com.beatgames.beatsaber/core-mods.json 216 | // but really the only lib that never is copied over is the modloader, the rest is either a downloaded qmod or just a copied lib 217 | // even core mods should technically be added via download 218 | .filter(|lib| 219 | // Must be directly referenced in qpm.json 220 | direct_dependencies.contains(&lib.dependency.id) && 221 | 222 | // keep if header only is false, or if not defined 223 | !lib.dependency.additional_data.headers_only.unwrap_or(false) && 224 | 225 | // Modloader should never be included 226 | lib.dependency.id != "modloader" && 227 | 228 | // don't include static deps 229 | !lib.dependency.additional_data.static_linking.unwrap_or(false) && 230 | 231 | // Only keep libs that aren't downloadable 232 | !mods.iter().any(|dep| lib.dependency.id == dep.id)) 233 | .map(|dep| dep.get_so_name()) 234 | .collect::>(); 235 | 236 | Self { 237 | schema_version: Version::new(1, 0, 0), 238 | name: shared_package.config.info.name.clone(), 239 | id: shared_package.config.info.id.clone(), 240 | author: Default::default(), 241 | porter: None, 242 | version: shared_package.config.info.version.to_string(), 243 | package_id: None, 244 | package_version: None, 245 | description: None, 246 | cover_image: None, 247 | is_library: None, 248 | dependencies: mods, 249 | mod_files: vec![shared_package.config.get_so_name()], 250 | library_files: libs, 251 | file_copies: Default::default(), 252 | copy_extensions: Default::default(), 253 | } 254 | } 255 | } 256 | 257 | impl From for ModDependency { 258 | fn from(dep: Dependency) -> Self { 259 | Self { 260 | id: dep.id, 261 | version_range: dep.version_range, 262 | mod_link: dep.additional_data.mod_link, 263 | } 264 | } 265 | } 266 | 267 | impl From for ModDependency { 268 | fn from(dep: SharedDependency) -> Self { 269 | dep.dependency.into() 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /src/data/package/compile_options.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | /// - compileOptions (QPM.Commands.SupportedPropertiesCommand+CompileOptionsProperty): Additional options for compilation and edits to compilation related files. - Supported in: package 4 | /// Type: QPM.Commands.SupportedPropertiesCommand+CompileOptionsProperty 5 | /// - includePaths - OPTIONAL (System.String[]): Additional include paths to add, relative to the extern directory. 6 | /// - systemIncludes - OPTIONAL (System.String[]): Additional system include paths to add, relative to the extern directory. 7 | /// - cppFeatures - OPTIONAL (System.String[]): Additional C++ features to add. 8 | /// - cppFlags - OPTIONAL (System.String[]): Additional C++ flags to add. 9 | /// - cFlags - OPTIONAL (System.String[]): Additional C flags to add. 10 | #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct CompileOptions { 13 | /// Additional include paths to add, relative to the extern directory. 14 | #[serde(skip_serializing_if = "Option::is_none")] 15 | pub include_paths: Option>, 16 | 17 | /// Additional system include paths to add, relative to the extern directory. 18 | #[serde(skip_serializing_if = "Option::is_none")] 19 | pub system_includes: Option>, 20 | 21 | /// Additional C++ features to add. 22 | #[serde(skip_serializing_if = "Option::is_none")] 23 | pub cpp_features: Option>, 24 | 25 | /// Additional C++ flags to add. 26 | #[serde(skip_serializing_if = "Option::is_none")] 27 | pub cpp_flags: Option>, 28 | 29 | /// Additional C flags to add. 30 | #[serde(skip_serializing_if = "Option::is_none")] 31 | pub c_flags: Option>, 32 | } 33 | -------------------------------------------------------------------------------- /src/data/package/mod.rs: -------------------------------------------------------------------------------- 1 | mod compile_options; 2 | pub type CompileOptions = compile_options::CompileOptions; 3 | 4 | mod package_config; 5 | pub type PackageConfig = package_config::PackageConfig; 6 | pub type PackageInfo = package_config::PackageInfo; 7 | pub type AdditionalPackageData = package_config::AdditionalPackageData; 8 | 9 | mod shared_package_config; 10 | pub type SharedPackageConfig = shared_package_config::SharedPackageConfig; 11 | -------------------------------------------------------------------------------- /src/data/package/package_config.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use owo_colors::OwoColorize; 4 | use semver::Version; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | use super::{CompileOptions, SharedPackageConfig}; 8 | use crate::data::dependency::{AdditionalDependencyData, Dependency}; 9 | #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] 10 | #[serde(rename_all = "camelCase")] 11 | pub struct PackageConfig { 12 | pub shared_dir: PathBuf, 13 | pub dependencies_dir: PathBuf, 14 | pub info: PackageInfo, 15 | pub dependencies: Vec, 16 | pub additional_data: AdditionalDependencyData, 17 | } 18 | 19 | #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq)] 20 | #[serde(rename_all = "camelCase")] 21 | pub struct PackageInfo { 22 | pub name: String, 23 | pub id: String, 24 | pub version: Version, 25 | pub url: Option, 26 | pub additional_data: AdditionalPackageData, 27 | } 28 | 29 | #[derive(Serialize, Deserialize, Clone, Debug, Hash, Eq, PartialEq, Default)] 30 | #[serde(rename_all = "camelCase")] 31 | pub struct AdditionalPackageData { 32 | /// Copy a dependency from a location that is local to this root path instead of from a remote url 33 | /// Technically just a dependency field 34 | #[serde(skip_serializing_if = "Option::is_none")] 35 | pub local_path: Option, 36 | 37 | /// By default if empty, true 38 | /// If false, this mod dependency will NOT be included in the generated mod.json 39 | /// Technically just a dependency field 40 | #[serde(skip_serializing_if = "Option::is_none")] 41 | pub include_qmod: Option, 42 | 43 | /// Whether or not the package is header only 44 | #[serde(skip_serializing_if = "Option::is_none")] 45 | pub headers_only: Option, 46 | 47 | /// Whether or not the package is statically linked 48 | #[serde(skip_serializing_if = "Option::is_none")] 49 | pub static_linking: Option, 50 | 51 | /// Whether to use the release or debug .so for linking 52 | /// Technically just a dependency field 53 | #[serde(skip_serializing_if = "Option::is_none")] 54 | pub use_release: Option, 55 | 56 | /// the link to the so file 57 | #[serde(skip_serializing_if = "Option::is_none")] 58 | pub so_link: Option, 59 | 60 | /// the link to the debug .so file 61 | #[serde(skip_serializing_if = "Option::is_none")] 62 | pub debug_so_link: Option, 63 | 64 | /// the overridden so file name 65 | #[serde(skip_serializing_if = "Option::is_none")] 66 | pub override_so_name: Option, 67 | 68 | /// the link to the qmod 69 | #[serde(skip_serializing_if = "Option::is_none")] 70 | pub mod_link: Option, 71 | 72 | /// Branch name of a Github repo. Only used when a valid github url is provided 73 | #[serde(skip_serializing_if = "Option::is_none")] 74 | pub branch_name: Option, 75 | 76 | /// Specify any additional files to be downloaded 77 | #[serde(skip_serializing_if = "Option::is_none")] 78 | pub extra_files: Option>, 79 | 80 | /// Whether or not the dependency is private and should be used in restore 81 | /// Technically just a dependency field 82 | #[serde( 83 | skip_serializing_if = "Option::is_none", 84 | rename(serialize = "private", deserialize = "private") 85 | )] 86 | pub is_private: Option, 87 | 88 | /// Additional Compile options to be used with this package 89 | #[serde(skip_serializing_if = "Option::is_none")] 90 | pub compile_options: Option, 91 | 92 | /// Sub folder to use from the downloaded repo / zip, so one repo can contain multiple packages 93 | #[serde(skip_serializing_if = "Option::is_none")] 94 | pub sub_folder: Option, 95 | } 96 | 97 | impl PackageConfig { 98 | pub fn write(&self) { 99 | let file = std::fs::File::create("qpm.json").expect("create failed"); 100 | serde_json::to_writer_pretty(file, &self).expect("Serialization failed"); 101 | println!("Package {} Written!", self.info.id); 102 | } 103 | 104 | pub fn check() -> bool { 105 | std::path::Path::new("qpm.json").exists() 106 | } 107 | 108 | pub fn read_path(filepath: PathBuf) -> PackageConfig { 109 | let file = std::fs::File::open(filepath).expect("Opening qpm.json failed"); 110 | serde_json::from_reader(file).expect("Deserializing package failed") 111 | } 112 | 113 | pub fn read() -> PackageConfig { 114 | let file = match std::fs::File::open("qpm.json") { 115 | Ok(o) => o, 116 | Err(_) => { 117 | panic!("Could not find qpm.json in local folder, are you in the correct directory? Maybe try {}", "qpm package create".bright_yellow()); 118 | } 119 | }; 120 | serde_json::from_reader(file).expect("Deserializing package failed") 121 | } 122 | 123 | pub fn add_dependency(&mut self, dependency: Dependency) { 124 | let dep = self.get_dependency(&dependency.id); 125 | match dep { 126 | Option::Some(_d) => { 127 | println!( 128 | "Not adding dependency {} because it already existed", 129 | &dependency.id 130 | ); 131 | } 132 | Option::None => { 133 | self.dependencies.push(dependency); 134 | } 135 | } 136 | } 137 | 138 | pub fn get_dependency(&mut self, id: &str) -> Option<&mut Dependency> { 139 | for (idx, dependency) in self.dependencies.iter().enumerate() { 140 | if dependency.id.eq(id) { 141 | return self.dependencies.get_mut(idx); 142 | } 143 | } 144 | 145 | Option::default() 146 | } 147 | 148 | pub fn remove_dependency(&mut self, id: &str) { 149 | for (idx, dependency) in self.dependencies.iter().enumerate() { 150 | if dependency.id.eq(id) { 151 | println!("removed dependency {}", id); 152 | self.dependencies.remove(idx); 153 | return; 154 | } 155 | } 156 | 157 | println!("Not removing dependency {} because it did not exist", id); 158 | } 159 | 160 | pub fn resolve(&self) -> impl Iterator + '_ { 161 | crate::resolver::resolve(self) 162 | } 163 | 164 | pub fn get_module_id(&self) -> String { 165 | let name = self.get_so_name(); 166 | if self.info.additional_data.static_linking.unwrap_or(false) { 167 | name[3..name.len() - 2].to_string() 168 | } else { 169 | name[3..name.len() - 3].to_string() 170 | } 171 | } 172 | 173 | pub fn get_so_name(&self) -> String { 174 | self.info 175 | .additional_data 176 | .override_so_name 177 | .clone() 178 | .unwrap_or(format!( 179 | "lib{}_{}.{}", 180 | self.info.id, 181 | self.info.version.to_string().replace('.', "_"), 182 | if self.additional_data.static_linking.unwrap_or(false) { 183 | "a" 184 | } else { 185 | "so" 186 | }, 187 | )) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/data/package/shared_package_config.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{Read, Write}, 3 | vec, 4 | }; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use std::fmt::Write as _; 9 | 10 | use crate::data::{qpackages, repo::DependencyRepository}; 11 | /// Fern: Adds line ending after each element 12 | /// thanks raft 13 | macro_rules! concatln { 14 | ($s:expr $(, $ss:expr)*) => { 15 | concat!($s $(, "\n", $ss)*) 16 | } 17 | } 18 | 19 | use super::PackageConfig; 20 | use crate::data::dependency::SharedDependency; 21 | #[derive(Serialize, Deserialize, Clone, Debug)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct SharedPackageConfig { 24 | /// The packageconfig that is stored in qpm.json 25 | pub config: PackageConfig, 26 | /// The dependencies as given by self.config.resolve() 27 | pub restored_dependencies: Vec, 28 | } 29 | 30 | impl SharedPackageConfig { 31 | pub fn read() -> SharedPackageConfig { 32 | let mut file = 33 | std::fs::File::open("qpm.shared.json").expect("Opening qpm.shared.json failed"); 34 | let mut qpm_package = String::new(); 35 | file.read_to_string(&mut qpm_package) 36 | .expect("Reading data failed"); 37 | 38 | serde_json::from_str::(&qpm_package) 39 | .expect("Deserializing package failed") 40 | } 41 | 42 | pub fn write(&self) { 43 | let qpm_package = serde_json::to_string_pretty(&self).expect("Serialization failed"); 44 | 45 | let mut file = std::fs::File::create("qpm.shared.json").expect("create failed"); 46 | file.write_all(qpm_package.as_bytes()) 47 | .expect("write failed"); 48 | println!("Package {} Written!", self.config.info.id); 49 | } 50 | 51 | pub fn publish(&self, auth: &str) { 52 | // ggez 53 | qpackages::publish_package(self, auth); 54 | } 55 | 56 | pub fn from_package(package: &PackageConfig) -> SharedPackageConfig { 57 | let shared_iter = package.resolve(); 58 | 59 | SharedPackageConfig { 60 | config: package.clone(), 61 | restored_dependencies: shared_iter 62 | // this is not needed right? 63 | //.collect::>() 64 | //.iter() 65 | .map(|cfg| cfg.into()) 66 | .collect::>(), 67 | } 68 | } 69 | 70 | pub fn restore(&self, repo: &impl DependencyRepository) { 71 | // TODO: Support restoring file repository dependencies 72 | for to_restore in self.restored_dependencies.iter() { 73 | // if the shared dep is contained within the direct dependencies, link against that, always copy headers! 74 | let shared_package = repo.get_shared_package_from_dependency(to_restore).unwrap_or_else(|| panic!("Could not find package {}", to_restore.dependency.id)); 75 | 76 | to_restore.cache(&shared_package); 77 | to_restore.restore_from_cache( 78 | self.config 79 | .dependencies 80 | .iter() 81 | .any(|dep| dep.id == to_restore.dependency.id), 82 | &shared_package 83 | ); 84 | } 85 | 86 | self.write_extern_cmake(repo); 87 | self.write_define_cmake(); 88 | } 89 | 90 | pub fn write_extern_cmake(&self, repo: &impl DependencyRepository) { 91 | let mut extern_cmake_file = 92 | std::fs::File::create("extern.cmake").expect("Failed to create extern cmake file"); 93 | let mut result = concatln!( 94 | "# YOU SHOULD NOT MANUALLY EDIT THIS FILE, QPM WILL VOID ALL CHANGES", 95 | "# always added", 96 | "target_include_directories(${COMPILE_ID} PRIVATE ${EXTERN_DIR}/includes)", 97 | "target_include_directories(${COMPILE_ID} SYSTEM PRIVATE ${EXTERN_DIR}/includes/libil2cpp/il2cpp/libil2cpp)", 98 | "\n# includes and compile options added by other libraries\n" 99 | ).to_string(); 100 | 101 | let mut any = false; 102 | for shared_dep in self.restored_dependencies.iter() { 103 | let shared_package = repo.get_shared_package_from_dependency(shared_dep).expect("Unable to get shared package"); 104 | let package_id = shared_package.config.info.id; 105 | 106 | if let Some(compile_options) = 107 | shared_package.config.info.additional_data.compile_options 108 | { 109 | any = true; 110 | // TODO: Must ${{COMPILE_ID}} be changed to {package_id}? 111 | 112 | if let Some(include_dirs) = compile_options.include_paths { 113 | for dir in include_dirs.iter() { 114 | writeln!(result, "target_include_directories(${{COMPILE_ID}} PRIVATE ${{EXTERN_DIR}}/includes/{}/{})", package_id, dir).unwrap(); 115 | } 116 | } 117 | 118 | if let Some(system_include_dirs) = compile_options.system_includes { 119 | for dir in system_include_dirs.iter() { 120 | writeln!(result, "target_include_directories(${{COMPILE_ID}} SYSTEM PRIVATE ${{EXTERN_DIR}}/includes/{}/{})", package_id, dir).unwrap(); 121 | } 122 | } 123 | 124 | let mut features: Vec = vec![]; 125 | 126 | if let Some(cpp_features) = compile_options.cpp_features { 127 | features.append(&mut cpp_features.clone()); 128 | } 129 | 130 | for feature in features.iter() { 131 | writeln!(result, 132 | "target_compile_features(${{COMPILE_ID}} PRIVATE {})", 133 | feature 134 | ).unwrap(); 135 | } 136 | 137 | let mut flags: Vec = vec![]; 138 | 139 | if let Some(cpp_flags) = compile_options.cpp_flags { 140 | flags.append(&mut cpp_flags.clone()); 141 | } 142 | 143 | if let Some(c_flags) = compile_options.c_flags { 144 | flags.append(&mut c_flags.clone()); 145 | } 146 | 147 | for flag in flags.iter() { 148 | writeln!( 149 | result, 150 | "target_compile_options(${{COMPILE_ID}} PRIVATE {})", 151 | flag 152 | ).unwrap(); 153 | } 154 | } 155 | 156 | if let Some(extra_files) = &shared_dep.dependency.additional_data.extra_files { 157 | for path_str in extra_files.iter() { 158 | let path = std::path::PathBuf::new().join(&format!( 159 | "extern/includes/{}/{}", 160 | &shared_dep.dependency.id, path_str 161 | )); 162 | let extern_path = std::path::PathBuf::new().join(&format!( 163 | "includes/{}/{}", 164 | &shared_dep.dependency.id, path_str 165 | )); 166 | if path.is_file() { 167 | write!(result, 168 | "add_library(${{COMPILE_ID}} SHARED ${{EXTERN_DIR}}/{})", 169 | extern_path.display() 170 | ).unwrap(); 171 | } else { 172 | let listname = format!( 173 | "{}_{}_extra", 174 | path_str 175 | .replace('/', "_") 176 | .replace('\\', "_") 177 | .replace('-', "_"), 178 | shared_dep.dependency.id.replace('-', "_") 179 | ); 180 | 181 | writeln!(result, 182 | "RECURSE_FILES({}_c ${{EXTERN_DIR}}/{}/*.c)", 183 | listname, 184 | extern_path.display() 185 | ).unwrap(); 186 | 187 | writeln!(result, 188 | "RECURSE_FILES({}_cpp ${{EXTERN_DIR}}/{}/*.cpp)", 189 | listname, 190 | extern_path.display() 191 | ).unwrap(); 192 | 193 | writeln!(result, 194 | "target_sources(${{COMPILE_ID}} PRIVATE ${{{}_c}})", 195 | listname 196 | ).unwrap(); 197 | 198 | writeln!( 199 | result, 200 | "target_sources(${{COMPILE_ID}} PRIVATE ${{{}_cpp}})", 201 | listname 202 | ).unwrap(); 203 | } 204 | } 205 | } 206 | 207 | if let Some(dep) = self 208 | .config 209 | .dependencies 210 | .iter() 211 | .find(|el| el.id == shared_dep.dependency.id) 212 | { 213 | if let Some(extra_files) = &dep.additional_data.extra_files { 214 | for path_str in extra_files.iter() { 215 | let path = std::path::PathBuf::new() 216 | .join(&format!("extern/includes/{}/{}", &dep.id, path_str)); 217 | let extern_path = std::path::PathBuf::new().join(&format!( 218 | "includes/{}/{}", 219 | &shared_dep.dependency.id, path_str 220 | )); 221 | if path.is_file() { 222 | write!(result, 223 | "add_library(${{COMPILE_ID}} SHARED ${{EXTERN_DIR}}/{})", 224 | extern_path.display() 225 | ).unwrap(); 226 | } else { 227 | let listname = format!( 228 | "{}_{}_local_extra", 229 | path_str 230 | .replace('/', "_") 231 | .replace('\\', "_") 232 | .replace('-', "_"), 233 | shared_dep.dependency.id.replace('-', "_") 234 | ); 235 | 236 | writeln!( 237 | result, 238 | "RECURSE_FILES({}_c ${{EXTERN_DIR}}/{}/*.c)", 239 | listname, 240 | extern_path.display() 241 | ).unwrap(); 242 | 243 | writeln!( 244 | result, 245 | "RECURSE_FILES({}_cpp ${{EXTERN_DIR}}/{}/*.cpp)", 246 | listname, 247 | extern_path.display() 248 | ).unwrap(); 249 | 250 | writeln!( 251 | result, 252 | "target_sources(${{COMPILE_ID}} PRIVATE ${{{}_c}})", 253 | listname 254 | ).unwrap(); 255 | 256 | writeln!( 257 | result, 258 | "target_sources(${{COMPILE_ID}} PRIVATE ${{{}_cpp}})", 259 | listname 260 | ).unwrap(); 261 | } 262 | } 263 | } 264 | } 265 | } 266 | 267 | if !any { 268 | result.push_str("# Sadly, there were none with extra include dirs\n"); 269 | } 270 | 271 | result.push_str(concatln!( 272 | "\n# libs dir -> stores .so or .a files (or symlinked!)", 273 | "target_link_directories(${COMPILE_ID} PRIVATE ${EXTERN_DIR}/libs)", 274 | "RECURSE_FILES(so_list ${EXTERN_DIR}/libs/*.so)", 275 | "RECURSE_FILES(a_list ${EXTERN_DIR}/libs/*.a)\n", 276 | "# every .so or .a that needs to be linked, put here!", 277 | "# I don't believe you need to specify if a lib is static or not, poggers!", 278 | "target_link_libraries(${COMPILE_ID} PRIVATE\n\t${so_list}\n\t${a_list}\n)\n" 279 | )); 280 | 281 | extern_cmake_file 282 | .write_all(result.as_bytes()) 283 | .expect("Failed to write out extern cmake file"); 284 | } 285 | 286 | pub fn write_define_cmake(&self) { 287 | let mut defines_cmake_file = std::fs::File::create("qpm_defines.cmake") 288 | .expect("Failed to create defines cmake file"); 289 | 290 | defines_cmake_file 291 | .write_all(self.make_defines_string().as_bytes()) 292 | .expect("Failed to write out own define make string"); 293 | } 294 | 295 | pub fn make_defines_string(&self) -> String { 296 | // TODO: use additional_data.compile_options here or in the extern cmake file ? include dirs are set there at least 297 | let mut result: String = concatln!( 298 | "# YOU SHOULD NOT MANUALLY EDIT THIS FILE, QPM WILL VOID ALL CHANGES", 299 | "# Version defines, pretty useful" 300 | ) 301 | .to_string(); 302 | 303 | writeln!( 304 | result, 305 | "\nset(MOD_VERSION \"{}\")", 306 | self.config.info.version 307 | ).unwrap(); 308 | result.push_str("# take the mod name and just remove spaces, that will be MOD_ID, if you don't like it change it after the include of this file\n"); 309 | writeln!( 310 | result, 311 | "set(MOD_ID \"{}\")\n", 312 | self.config.info.name.replace(' ', "") 313 | ).unwrap(); 314 | result.push_str("# derived from override .so name or just id_version\n"); 315 | 316 | writeln!( 317 | result, 318 | "set(COMPILE_ID \"{}\")", 319 | self.config.get_module_id() 320 | ).unwrap(); 321 | 322 | result.push_str( 323 | "# derived from whichever codegen package is installed, will default to just codegen\n", 324 | ); 325 | 326 | writeln!( 327 | result, 328 | "set(CODEGEN_ID \"{}\")\n", 329 | if let Some(codegen_dep) = self 330 | .restored_dependencies 331 | .iter() 332 | .find(|dep| dep.dependency.id.contains("codegen")) 333 | { 334 | // found a codegen 335 | &codegen_dep.dependency.id 336 | } else { 337 | "codegen" 338 | } 339 | ).unwrap(); 340 | 341 | result.push_str("# given from qpm, automatically updated from qpm.json\n"); 342 | 343 | writeln!( 344 | result, 345 | "set(EXTERN_DIR_NAME \"{}\")", 346 | self.config.dependencies_dir.display() 347 | ).unwrap(); 348 | writeln!( 349 | result, 350 | "set(SHARED_DIR_NAME \"{}\")\n", 351 | self.config.shared_dir.display() 352 | ).unwrap(); 353 | 354 | result.push_str(concatln!( 355 | "# if no target given, use Debug", 356 | "if (NOT DEFINED CMAKE_BUILD_TYPE)", 357 | "\tset(CMAKE_BUILD_TYPE \"Debug\")", 358 | "endif()\n" 359 | )); 360 | result.push_str(concatln!( 361 | "\n# defines used in ninja / cmake ndk builds", 362 | "if (NOT DEFINED CMAKE_ANDROID_NDK)", 363 | "\tif (EXISTS \"${CMAKE_CURRENT_LIST_DIR}/ndkpath.txt\")", 364 | "\t\tfile (STRINGS \"ndkpath.txt\" CMAKE_ANDROID_NDK)", 365 | "\telse()", 366 | "\t\tif(EXISTS $ENV{ANDROID_NDK_HOME})", 367 | "\t\t\tset(CMAKE_ANDROID_NDK $ENV{ANDROID_NDK_HOME})", 368 | "\t\telseif(EXISTS $ENV{ANDROID_NDK_LATEST_HOME})", 369 | "\t\t\tset(CMAKE_ANDROID_NDK $ENV{ANDROID_NDK_LATEST_HOME})", 370 | "\t\tendif()", 371 | "\tendif()", 372 | "endif()", 373 | 374 | "if (NOT DEFINED CMAKE_ANDROID_NDK)", 375 | "\tmessage(Big time error buddy, no NDK)", 376 | "endif()", 377 | "message(Using NDK ${CMAKE_ANDROID_NDK})", 378 | "string(REPLACE \"\\\\\" \"/\" CMAKE_ANDROID_NDK ${CMAKE_ANDROID_NDK})", 379 | "\nset(ANDROID_PLATFORM 24)", 380 | "set(ANDROID_ABI arm64-v8a)", 381 | "set(ANDROID_STL c++_static)", 382 | "set(ANDROID_USE_LEGACY_TOOLCHAIN_FILE OFF)", 383 | "\nset(CMAKE_TOOLCHAIN_FILE ${CMAKE_ANDROID_NDK}/build/cmake/android.toolchain.cmake)" 384 | )); 385 | result.push_str(concatln!( 386 | "\n# define used for external data, mostly just the qpm dependencies", 387 | "set(EXTERN_DIR ${CMAKE_CURRENT_SOURCE_DIR}/${EXTERN_DIR_NAME})", 388 | "set(SHARED_DIR ${CMAKE_CURRENT_SOURCE_DIR}/${SHARED_DIR_NAME})" 389 | )); 390 | result.push_str(concatln!( 391 | "\n# get files by filter recursively", 392 | "MACRO(RECURSE_FILES return_list filter)", 393 | "\tFILE(GLOB_RECURSE new_list ${filter})", 394 | "\tSET(file_list \"\")", 395 | "\tFOREACH(file_path ${new_list})", 396 | "\t\tSET(file_list ${file_list} ${file_path})", 397 | "\tENDFOREACH()", 398 | "\tLIST(REMOVE_DUPLICATES file_list)", 399 | "\tSET(${return_list} ${file_list})", 400 | "ENDMACRO()" 401 | )); 402 | 403 | result 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/data/qpackages.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::LazyLock as Lazy}; 2 | 3 | use atomic_refcell::AtomicRefCell; 4 | use reqwest::{blocking::Response, StatusCode}; 5 | use semver::Version; 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::{data::{package::SharedPackageConfig}, utils::network::get_agent}; 9 | static API_URL: &str = "https://qpackages.com"; 10 | 11 | static VERSIONS_CACHE: Lazy>>> = 12 | Lazy::new(Default::default); 13 | static SHARED_PACKAGE_CACHE: Lazy>> = 14 | Lazy::new(Default::default); 15 | 16 | 17 | 18 | #[derive(Serialize, Deserialize, Clone, Debug, Hash, PartialEq, Eq)] 19 | #[allow(non_snake_case)] 20 | #[serde(rename_all = "camelCase")] 21 | pub struct PackageVersion { 22 | pub id: String, 23 | pub version: Version, 24 | } 25 | 26 | // true if 404 27 | fn is_404_or_panic(res: &Result) -> bool { 28 | if let Err(e) = res { 29 | 30 | if let Some(status) = e.status() { 31 | if status == 404u16 { 32 | return true; 33 | } 34 | 35 | panic!("Received error code {:?} with response {:?}", status, &res) 36 | } 37 | 38 | panic!("Unable to send request {}", dbg!(&e)); 39 | } 40 | 41 | false 42 | } 43 | 44 | /// Requests the appriopriate package info from qpackage.com 45 | pub fn get_versions(id: &str) -> Option> { 46 | let url = format!("{}/{}?limit=0", API_URL, id); 47 | 48 | if let Some(entry) = VERSIONS_CACHE.borrow().get(&url) { 49 | return Some(entry.clone()); 50 | } 51 | 52 | let response = get_agent() 53 | .get(&url) 54 | .send(); 55 | 56 | if is_404_or_panic(&response) { 57 | return None; 58 | } 59 | 60 | let versions: Vec = response 61 | .expect("Request to qpackages.com failed") 62 | .json() 63 | .expect("Into json failed"); 64 | 65 | 66 | VERSIONS_CACHE.borrow_mut().insert(url, versions.clone()); 67 | 68 | 69 | Some(versions) 70 | } 71 | 72 | pub fn get_shared_package(id: &str, ver: &Version) -> Option { 73 | let url = format!("{}/{}/{}", API_URL, id, ver); 74 | 75 | if let Some(entry) = SHARED_PACKAGE_CACHE.borrow().get(&url) { 76 | return Some(entry.clone()); 77 | } 78 | 79 | let response = get_agent() 80 | .get(&url) 81 | .send(); 82 | 83 | let shared_package: SharedPackageConfig = response 84 | .expect("Request to qpackages.com failed") 85 | .json() 86 | .expect("Into json failed"); 87 | 88 | SHARED_PACKAGE_CACHE 89 | .borrow_mut() 90 | .insert(url, shared_package.clone()); 91 | Some(shared_package) 92 | } 93 | 94 | pub fn get_packages() -> Vec { 95 | get_agent() 96 | .get(API_URL) 97 | .send() 98 | .expect("Request to qpackages.com failed") 99 | .json() 100 | .expect("Into json failed") 101 | } 102 | 103 | pub fn publish_package(package: &SharedPackageConfig, auth: &str) { 104 | let url = format!( 105 | "{}/{}/{}", 106 | API_URL, &package.config.info.id, &package.config.info.version 107 | ); 108 | 109 | let resp = get_agent() 110 | .post(&url) 111 | .header("Authorization", auth) 112 | .json(&package) 113 | .send() 114 | .expect("Request to qpackages.com failed"); 115 | 116 | if resp.status() == StatusCode::UNAUTHORIZED { 117 | panic!("Could not publish to {}: Unauthorized! Did you provide the correct key?", API_URL); 118 | } 119 | resp.error_for_status().expect("Response not OK!"); 120 | } 121 | -------------------------------------------------------------------------------- /src/data/repo/local_provider.rs: -------------------------------------------------------------------------------- 1 | use semver::Version; 2 | 3 | 4 | use crate::data::{file_repository::FileRepository, qpackages::PackageVersion}; 5 | 6 | use super::DependencyRepository; 7 | 8 | impl DependencyRepository for FileRepository { 9 | fn get_versions(&self, id: &str) -> Option> { 10 | self.get_artifacts_from_id(id).map(|artifacts| { 11 | artifacts 12 | .keys() 13 | .map(|version| PackageVersion { 14 | id: id.to_string(), 15 | version: version.clone(), 16 | }) 17 | .collect() 18 | }) 19 | } 20 | 21 | fn get_shared_package( 22 | &self, 23 | id: &str, 24 | version: &Version, 25 | ) -> Option { 26 | self.get_artifact(id, version).cloned() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/data/repo/mod.rs: -------------------------------------------------------------------------------- 1 | use super::{package::SharedPackageConfig, qpackages::{PackageVersion}, dependency::SharedDependency}; 2 | 3 | 4 | pub mod local_provider; 5 | pub mod qpm_provider; 6 | pub mod multi_provider; 7 | 8 | pub trait DependencyRepository { 9 | fn get_versions(&self, id: &str) -> Option>; 10 | fn get_shared_package( 11 | &self, 12 | id: &str, 13 | version: &semver::Version, 14 | ) -> Option; 15 | 16 | fn get_shared_package_from_dependency(&self, shared_package: &SharedDependency) -> Option where Self: Sized { 17 | self.get_shared_package(&shared_package.dependency.id, &shared_package.version) 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/data/repo/multi_provider.rs: -------------------------------------------------------------------------------- 1 | use itertools::Itertools; 2 | 3 | use crate::data::{package::{SharedPackageConfig}, file_repository::FileRepository, qpackages::PackageVersion}; 4 | 5 | use super::{DependencyRepository, qpm_provider::QPMRepository}; 6 | 7 | 8 | pub fn default_repositories() -> Vec> { 9 | // TODO: Make file repository cached 10 | let file_repository = Box::new(FileRepository::read()); 11 | let qpm_repository = Box::new(QPMRepository::new()); 12 | vec![file_repository, qpm_repository] 13 | } 14 | 15 | pub struct MultiDependencyProvider { 16 | repositories: Vec>, 17 | } 18 | 19 | impl MultiDependencyProvider { 20 | // Repositories sorted in order 21 | pub fn new( repositories: Vec>) -> Self { 22 | Self { repositories } 23 | } 24 | 25 | pub fn useful_default_new() -> Self { 26 | MultiDependencyProvider::new(default_repositories()) 27 | } 28 | } 29 | 30 | /// 31 | /// Merge multiple repositories into one 32 | /// Allow fetching from multiple backends 33 | /// 34 | impl DependencyRepository for MultiDependencyProvider { 35 | // get versions of all repositories 36 | fn get_versions(&self, id: &str) -> Option> { 37 | // double flat map???? rust weird 38 | let result: Vec = self 39 | .repositories 40 | .iter() 41 | .flat_map(|r| r.get_versions(id)) 42 | .flatten() 43 | .unique() 44 | .collect(); 45 | 46 | if result.is_empty() { 47 | return None; 48 | } 49 | 50 | 51 | Some(result) 52 | } 53 | 54 | // get package from the first repository that has it 55 | fn get_shared_package( 56 | &self, 57 | id: &str, 58 | version: &semver::Version, 59 | ) -> Option { 60 | self.repositories 61 | .iter() 62 | .find_map(|r| r.get_shared_package(id, version)) 63 | } 64 | } -------------------------------------------------------------------------------- /src/data/repo/qpm_provider.rs: -------------------------------------------------------------------------------- 1 | use semver::Version; 2 | 3 | use crate::data::qpackages; 4 | 5 | use super::DependencyRepository; 6 | 7 | 8 | 9 | pub struct QPMRepository {} 10 | 11 | impl QPMRepository { 12 | pub fn new() -> Self { 13 | QPMRepository { } 14 | } 15 | } 16 | 17 | impl DependencyRepository for QPMRepository { 18 | fn get_versions(&self, id: &str) -> Option> { 19 | qpackages::get_versions(id) 20 | } 21 | 22 | fn get_shared_package(&self, id: &str, version: &Version) -> Option { 23 | qpackages::get_shared_package(id, version) 24 | } 25 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![feature(once_cell)] 2 | #![feature(map_try_insert)] 3 | 4 | use clap::{Parser, Subcommand}; 5 | use serde::{Deserialize, Serialize}; 6 | 7 | mod commands; 8 | mod data; 9 | mod resolver; 10 | mod utils; 11 | 12 | /// QPM is a command line tool that allows modmakers to 13 | /// easily download dependencies for interacting with a game or other mods 14 | #[derive(Parser, Debug)] 15 | #[clap(version = "0.1.0", author = "RedBrumbler & Sc2ad")] 16 | 17 | struct Opts { 18 | #[clap(subcommand)] 19 | subcmd: MainCommand, 20 | } 21 | 22 | #[derive(Subcommand, Debug, Clone)] 23 | enum MainCommand { 24 | /// Cache control 25 | Cache(commands::cache::Cache), 26 | /// Clear all resolved dependencies by clearing the lock file 27 | Clear, 28 | /// Collect and collapse dependencies and print them to console 29 | Collapse, 30 | /// Config control 31 | Config(commands::config::Config), 32 | /// Dependency control 33 | Dependency(commands::dependency::Dependency), 34 | /// Package control 35 | Package(commands::package::Package), 36 | /// List all properties that are currently supported by QPM 37 | List(commands::list::ListOperation), 38 | /// Publish package 39 | Publish(commands::publish::Publish), 40 | /// Restore and resolve all dependencies from the package 41 | Restore, 42 | /// Qmod control 43 | Qmod(commands::qmod::Qmod), 44 | /// Install to local repository 45 | Install(commands::install::InstallOperation), 46 | } 47 | 48 | fn main() { 49 | // You can handle information about subcommands by requesting their matches by name 50 | // (as below), requesting just the name used, or both at the same time 51 | match (Opts::parse() as Opts).subcmd { 52 | MainCommand::Cache(c) => commands::cache::execute_cache_operation(c), 53 | MainCommand::Clear => commands::clear::execute_clear_operation(), 54 | MainCommand::Collapse => commands::collapse::execute_collapse_operation(), 55 | MainCommand::Config(c) => commands::config::execute_config_operation(c), 56 | MainCommand::Dependency(d) => commands::dependency::execute_dependency_operation(d), 57 | MainCommand::Package(p) => commands::package::execute_package_operation(p), 58 | MainCommand::List(l) => commands::list::execute_list_operation(l), 59 | MainCommand::Publish(a) => commands::publish::execute_publish_operation(&a), 60 | MainCommand::Restore => commands::restore::execute_restore_operation(), 61 | MainCommand::Qmod(q) => commands::qmod::execute_qmod_operation(q), 62 | MainCommand::Install(i) => commands::install::execute_install_operation(i), 63 | } 64 | } 65 | 66 | #[derive(Serialize, Deserialize, Clone, Debug)] 67 | pub struct Config { 68 | pub cache_path: String, 69 | pub timeout: u32, 70 | } 71 | -------------------------------------------------------------------------------- /src/resolver/mod.rs: -------------------------------------------------------------------------------- 1 | use std::process; 2 | 3 | use pubgrub::{ 4 | error::PubGrubError, 5 | report::{DefaultStringReporter, Reporter}, 6 | }; 7 | 8 | 9 | use crate::data::{ 10 | package::{PackageConfig, SharedPackageConfig}, repo::{DependencyRepository, multi_provider::MultiDependencyProvider}, 11 | }; 12 | 13 | use self::provider::HackDependencyProvider; 14 | 15 | mod provider; 16 | mod semver; 17 | 18 | 19 | 20 | pub fn resolve(root: &PackageConfig) -> impl Iterator + '_ { 21 | let provider = HackDependencyProvider::new(root, MultiDependencyProvider::useful_default_new()); 22 | match pubgrub::solver::resolve(&provider, root.info.id.clone(), root.info.version.clone()) { 23 | Ok(deps) => deps 24 | .into_iter() 25 | .filter_map(move |(id, version)| { 26 | if id == root.info.id && version == root.info.version { 27 | return None; 28 | } 29 | 30 | provider.get_shared_package(&id, &version.into()) 31 | }), 32 | 33 | Err(PubGrubError::NoSolution(tree)) => { 34 | let report = DefaultStringReporter::report(&tree); 35 | eprintln!("failed to resolve dependencies: \n{}", report); 36 | process::exit(1) 37 | } 38 | Err(err) => { 39 | eprintln!("{}", err); 40 | process::exit(1) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/resolver/provider.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Borrow; 2 | 3 | use pubgrub::{range::Range, solver::Dependencies}; 4 | 5 | use super::semver::{req_to_range, Version}; 6 | use crate::data::{ 7 | package::{PackageConfig, SharedPackageConfig}, 8 | qpackages::{self, PackageVersion}, 9 | repo::{multi_provider::MultiDependencyProvider, DependencyRepository}, 10 | }; 11 | 12 | pub struct HackDependencyProvider<'a> { 13 | root: &'a PackageConfig, 14 | repo: MultiDependencyProvider, 15 | } 16 | 17 | impl<'a> HackDependencyProvider<'a> { 18 | // Repositories sorted in order 19 | pub fn new(root: &'a PackageConfig, repo: MultiDependencyProvider) -> Self { 20 | Self { root, repo } 21 | } 22 | } 23 | 24 | /// 25 | /// Merge multiple repositories into one 26 | /// Allow fetching from multiple backends 27 | /// 28 | impl DependencyRepository for HackDependencyProvider<'_> { 29 | // get versions of all repositories 30 | fn get_versions(&self, id: &str) -> Option> { 31 | // we add ourselves to the gotten versions, so the local version always can be resolved as most ideal 32 | if *id == self.root.info.id { 33 | return Some(vec![qpackages::PackageVersion { 34 | id: self.root.info.id.clone(), 35 | version: self.root.info.version.clone(), 36 | }]); 37 | } 38 | 39 | let result = self.repo.get_versions(id); 40 | 41 | if result.is_none() || result.as_ref().unwrap().is_empty() { 42 | return None; 43 | } 44 | 45 | result 46 | } 47 | 48 | // get package from the first repository that has it 49 | fn get_shared_package( 50 | &self, 51 | id: &str, 52 | version: &semver::Version, 53 | ) -> Option { 54 | self.repo.get_shared_package(id, version) 55 | } 56 | } 57 | 58 | impl pubgrub::solver::DependencyProvider for HackDependencyProvider<'_> { 59 | fn choose_package_version, U: Borrow>>( 60 | &self, 61 | potential_packages: impl Iterator, 62 | ) -> Result<(T, Option), Box> { 63 | Ok(pubgrub::solver::choose_package_with_fewest_versions( 64 | |id| { 65 | self.get_versions(id) 66 | // TODO: Anyhow 67 | .unwrap_or_else(|| panic!("Unable to find versions for package {id}")) 68 | .into_iter() 69 | .map(|pv: qpackages::PackageVersion| pv.version.into()) 70 | }, 71 | potential_packages, 72 | )) 73 | } 74 | 75 | fn get_dependencies( 76 | &self, 77 | id: &String, 78 | version: &Version, 79 | ) -> Result, Box> { 80 | if id == &self.root.info.id && version == &self.root.info.version { 81 | let deps = self 82 | .root 83 | .dependencies 84 | .iter() 85 | .map(|dep| { 86 | let id = &dep.id; 87 | let version = req_to_range(dep.version_range.clone()); 88 | (id.clone(), version) 89 | }) 90 | .collect(); 91 | Ok(Dependencies::Known(deps)) 92 | } else { 93 | let mut package = self 94 | .get_shared_package(id, &version.clone().into()) 95 | .unwrap_or_else(|| panic!("Could not find package {id} with version {version}")); 96 | // remove any private dependencies 97 | package 98 | .config 99 | .dependencies 100 | .retain(|dep| !dep.additional_data.is_private.unwrap_or(false)); 101 | 102 | let deps = package 103 | .config 104 | .dependencies 105 | .into_iter() 106 | .map(|dep| { 107 | let id = dep.id; 108 | let version = req_to_range(dep.version_range); 109 | (id, version) 110 | }) 111 | .collect(); 112 | Ok(Dependencies::Known(deps)) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/resolver/semver.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use pubgrub::range::Range; 4 | use semver::{Comparator, Op, Prerelease, VersionReq}; 5 | 6 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] 7 | pub(super) struct Version(semver::Version); 8 | 9 | pub(super) fn req_to_range(req: VersionReq) -> Range { 10 | let mut range = Range::any(); 11 | for comparator in req.comparators { 12 | let next = match comparator { 13 | Comparator { 14 | op: Op::Exact, 15 | major, 16 | minor: Some(minor), 17 | patch: Some(patch), 18 | pre, 19 | } => exact_xyz(major, minor, patch, pre), 20 | Comparator { 21 | op: Op::Exact, 22 | major, 23 | minor: Some(minor), 24 | .. 25 | } => exact_xy(major, minor), 26 | Comparator { 27 | op: Op::Exact, 28 | major, 29 | .. 30 | } => exact_x(major), 31 | 32 | Comparator { 33 | op: Op::Greater, 34 | major, 35 | minor: Some(minor), 36 | patch: Some(patch), 37 | pre, 38 | } => greater_xyz(major, minor, patch, pre), 39 | Comparator { 40 | op: Op::Greater, 41 | major, 42 | minor: Some(minor), 43 | .. 44 | } => greater_xy(major, minor), 45 | Comparator { 46 | op: Op::Greater, 47 | major, 48 | .. 49 | } => greater_x(major), 50 | 51 | Comparator { 52 | op: Op::GreaterEq, 53 | major, 54 | minor: Some(minor), 55 | patch: Some(patch), 56 | pre, 57 | } => greater_eq_xyz(major, minor, patch, pre), 58 | Comparator { 59 | op: Op::GreaterEq, 60 | major, 61 | minor: Some(minor), 62 | .. 63 | } => greater_eq_xy(major, minor), 64 | Comparator { 65 | op: Op::GreaterEq, 66 | major, 67 | .. 68 | } => greater_eq_x(major), 69 | 70 | Comparator { 71 | op: Op::Less, 72 | major, 73 | minor: Some(minor), 74 | patch: Some(patch), 75 | pre, 76 | } => less_xyz(major, minor, patch, pre), 77 | Comparator { 78 | op: Op::Less, 79 | major, 80 | minor: Some(minor), 81 | .. 82 | } => less_xy(major, minor), 83 | Comparator { 84 | op: Op::Less, 85 | major, 86 | .. 87 | } => less_x(major), 88 | 89 | Comparator { 90 | op: Op::LessEq, 91 | major, 92 | minor: Some(minor), 93 | patch: Some(patch), 94 | pre, 95 | } => less_eq_xyz(major, minor, patch, pre), 96 | Comparator { 97 | op: Op::LessEq, 98 | major, 99 | minor: Some(minor), 100 | .. 101 | } => less_eq_xy(major, minor), 102 | Comparator { 103 | op: Op::LessEq, 104 | major, 105 | .. 106 | } => less_eq_x(major), 107 | 108 | Comparator { 109 | op: Op::Tilde, 110 | major, 111 | minor: Some(minor), 112 | patch: Some(patch), 113 | pre, 114 | } => tilde_xyz(major, minor, patch, pre), 115 | Comparator { 116 | op: Op::Tilde, 117 | major, 118 | minor: Some(minor), 119 | .. 120 | } => tilde_xy(major, minor), 121 | Comparator { 122 | op: Op::Tilde, 123 | major, 124 | .. 125 | } => tilde_x(major), 126 | 127 | Comparator { 128 | op: Op::Caret, 129 | major: 0, 130 | minor: Some(0), 131 | patch: Some(patch), 132 | pre, 133 | } => caret_00z(patch, pre), 134 | Comparator { 135 | op: Op::Caret, 136 | major: 0, 137 | minor: Some(minor), 138 | patch: Some(patch), 139 | pre, 140 | } => caret_0yz(minor, patch, pre), 141 | Comparator { 142 | op: Op::Caret, 143 | major, 144 | minor: Some(minor), 145 | patch: Some(patch), 146 | pre, 147 | } => caret_xyz(major, minor, patch, pre), 148 | Comparator { 149 | op: Op::Caret, 150 | major: 0, 151 | minor: Some(0), 152 | .. 153 | } => caret_00(), 154 | Comparator { 155 | op: Op::Caret, 156 | major, 157 | minor: Some(minor), 158 | .. 159 | } => caret_xy(major, minor), 160 | Comparator { 161 | op: Op::Caret, 162 | major, 163 | .. 164 | } => caret_x(major), 165 | 166 | Comparator { 167 | op: Op::Wildcard, 168 | major, 169 | minor: Some(minor), 170 | .. 171 | } => wildcard_xy(major, minor), 172 | Comparator { 173 | op: Op::Wildcard, 174 | major, 175 | .. 176 | } => wildcard_x(major), 177 | 178 | _ => unimplemented!(), 179 | }; 180 | range = range.intersection(&next); 181 | } 182 | range 183 | } 184 | 185 | fn exact_xyz(major: u64, minor: u64, patch: u64, pre: Prerelease) -> Range { 186 | Range::exact(semver::Version { 187 | major, 188 | minor, 189 | patch, 190 | pre, 191 | build: Default::default(), 192 | }) 193 | } 194 | fn exact_xy(major: u64, minor: u64) -> Range { 195 | greater_eq_xyz(major, minor, 0, Prerelease::EMPTY).intersection(&less_xyz( 196 | major, 197 | minor + 1, 198 | 0, 199 | Prerelease::EMPTY, 200 | )) 201 | } 202 | fn exact_x(major: u64) -> Range { 203 | greater_eq_xyz(major, 0, 0, Prerelease::EMPTY).intersection(&less_xyz( 204 | major + 1, 205 | 0, 206 | 0, 207 | Prerelease::EMPTY, 208 | )) 209 | } 210 | 211 | fn greater_xyz(major: u64, minor: u64, patch: u64, pre: Prerelease) -> Range { 212 | greater_eq_xyz(major, minor, patch, pre.clone()) 213 | .intersection(&exact_xyz(major, minor, patch, pre).negate()) 214 | } 215 | fn greater_xy(major: u64, minor: u64) -> Range { 216 | greater_eq_xyz(major, minor + 1, 0, Prerelease::EMPTY) 217 | } 218 | fn greater_x(major: u64) -> Range { 219 | greater_eq_xyz(major + 1, 0, 0, Prerelease::EMPTY) 220 | } 221 | 222 | fn greater_eq_xyz(major: u64, minor: u64, patch: u64, pre: Prerelease) -> Range { 223 | Range::higher_than(semver::Version { 224 | major, 225 | minor, 226 | patch, 227 | pre, 228 | build: Default::default(), 229 | }) 230 | } 231 | fn greater_eq_xy(major: u64, minor: u64) -> Range { 232 | greater_eq_xyz(major, minor, 0, Prerelease::EMPTY) 233 | } 234 | fn greater_eq_x(major: u64) -> Range { 235 | greater_eq_xyz(major, 0, 0, Prerelease::EMPTY) 236 | } 237 | 238 | fn less_xyz(major: u64, minor: u64, patch: u64, pre: Prerelease) -> Range { 239 | Range::strictly_lower_than(semver::Version { 240 | major, 241 | minor, 242 | patch, 243 | pre, 244 | build: Default::default(), 245 | }) 246 | } 247 | fn less_xy(major: u64, minor: u64) -> Range { 248 | less_xyz(major, minor, 0, Prerelease::EMPTY) 249 | } 250 | fn less_x(major: u64) -> Range { 251 | less_xyz(major, 0, 0, Prerelease::EMPTY) 252 | } 253 | 254 | fn less_eq_xyz(major: u64, minor: u64, patch: u64, pre: Prerelease) -> Range { 255 | less_xyz(major, minor, patch, pre.clone()).union(&exact_xyz(major, minor, patch, pre)) 256 | } 257 | fn less_eq_xy(major: u64, minor: u64) -> Range { 258 | less_xyz(major, minor + 1, 0, Prerelease::EMPTY) 259 | } 260 | fn less_eq_x(major: u64) -> Range { 261 | less_xyz(major + 1, 0, 0, Prerelease::EMPTY) 262 | } 263 | 264 | fn tilde_xyz(major: u64, minor: u64, patch: u64, pre: Prerelease) -> Range { 265 | greater_eq_xyz(major, minor, patch, pre).intersection(&less_xyz( 266 | major, 267 | minor + 1, 268 | 0, 269 | Prerelease::EMPTY, 270 | )) 271 | } 272 | fn tilde_xy(major: u64, minor: u64) -> Range { 273 | exact_xy(major, minor) 274 | } 275 | fn tilde_x(major: u64) -> Range { 276 | exact_x(major) 277 | } 278 | 279 | fn caret_00z(patch: u64, pre: Prerelease) -> Range { 280 | exact_xyz(0, 0, patch, pre) 281 | } 282 | fn caret_0yz(minor: u64, patch: u64, pre: Prerelease) -> Range { 283 | greater_eq_xyz(0, minor, patch, pre).intersection(&less_xyz(0, minor + 1, 0, Prerelease::EMPTY)) 284 | } 285 | fn caret_xyz(major: u64, minor: u64, patch: u64, pre: Prerelease) -> Range { 286 | greater_eq_xyz(major, minor, patch, pre).intersection(&less_xyz( 287 | major + 1, 288 | 0, 289 | 0, 290 | Prerelease::EMPTY, 291 | )) 292 | } 293 | fn caret_00() -> Range { 294 | exact_xy(0, 0) 295 | } 296 | fn caret_xy(major: u64, minor: u64) -> Range { 297 | caret_xyz(major, minor, 0, Prerelease::EMPTY) 298 | } 299 | fn caret_x(major: u64) -> Range { 300 | exact_x(major) 301 | } 302 | 303 | fn wildcard_xy(major: u64, minor: u64) -> Range { 304 | exact_xy(major, minor) 305 | } 306 | fn wildcard_x(major: u64) -> Range { 307 | exact_x(major) 308 | } 309 | 310 | impl pubgrub::version::Version for Version { 311 | fn lowest() -> Self { 312 | Self(semver::Version::new(0, 0, 0)) 313 | } 314 | 315 | fn bump(&self) -> Self { 316 | let mut v = self.0.clone(); 317 | v.patch += 1; 318 | Self(v) 319 | } 320 | } 321 | 322 | macro_rules! impl_traits { 323 | ($($t:ty => $tt:ty),*) => { 324 | $( 325 | impl fmt::Debug for $t { 326 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 327 | fmt::Debug::fmt(&self.0, f) 328 | } 329 | } 330 | impl fmt::Display for $t { 331 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 332 | fmt::Display::fmt(&self.0, f) 333 | } 334 | } 335 | impl From<$t> for $tt { 336 | fn from(v: $t) -> Self { 337 | v.0 338 | } 339 | } 340 | impl From<$tt> for $t { 341 | fn from(v: $tt) -> Self { 342 | Self(v) 343 | } 344 | } 345 | impl PartialEq<$tt> for $t { 346 | fn eq(&self, other: &$tt) -> bool { 347 | self.0.eq(other) 348 | } 349 | } 350 | )* 351 | }; 352 | } 353 | impl_traits!(Version => semver::Version); 354 | -------------------------------------------------------------------------------- /src/utils/git.rs: -------------------------------------------------------------------------------- 1 | use owo_colors::OwoColorize; 2 | //use duct::cmd; 3 | use serde::{Deserialize, Serialize}; 4 | 5 | use crate::data::config::get_keyring; 6 | 7 | use super::network::get_agent; 8 | 9 | pub fn check_git() { 10 | let mut git = std::process::Command::new("git"); 11 | git.arg("--version"); 12 | 13 | match git.output() { 14 | Ok(_) => { 15 | #[cfg(debug_assertions)] 16 | println!("git detected on command line!"); 17 | } 18 | Err(_e) => { 19 | #[cfg(windows)] 20 | panic!( 21 | "Please make sure git ({}) is installed an on path, then try again!", 22 | "https://git-scm.com/download/windows".bright_yellow() 23 | ); 24 | #[cfg(target_os = "linux")] 25 | panic!( 26 | "Please make sure git ({}) is installed an on path, then try again!", 27 | "https://git-scm.com/download/linux".bright_yellow() 28 | ); 29 | #[cfg(target_os = "macos")] 30 | panic!( 31 | "Please make sure git ({}) is installed an on path, then try again!", 32 | "https://git-scm.com/download/mac".bright_yellow() 33 | ); 34 | } 35 | } 36 | } 37 | 38 | pub fn get_release(url: &str, out: &std::path::Path) -> bool { 39 | check_git(); 40 | if let Ok(token_unwrapped) = get_keyring().get_password() { 41 | get_release_with_token(url, out, &token_unwrapped) 42 | } else { 43 | get_release_without_token(url, out) 44 | } 45 | } 46 | 47 | pub fn get_release_without_token(url: &str, out: &std::path::Path) -> bool { 48 | let mut file = std::fs::File::create(out).expect("create so file failed"); 49 | get_agent().get(url) 50 | .send() 51 | .unwrap() 52 | .copy_to(&mut file) 53 | .expect("Failed to write to file"); 54 | 55 | out.exists() 56 | } 57 | 58 | pub fn get_release_with_token(url: &str, out: &std::path::Path, token: &str) -> bool { 59 | // had token, use it! 60 | // download url for a private thing: still need to get asset id! 61 | // from this: "https://github.com/$USER/$REPO/releases/download/$TAG/$FILENAME" 62 | // to this: "https://$TOKEN@api.github.com/repos/$USER/$REPO/releases/assets/$ASSET_ID" 63 | let split: Vec = url.split('/').map(|el| el.to_string()).collect(); 64 | 65 | // Obviously this is a bad way of parsing the GH url but like I see no better way, people better not use direct lib uploads lol 66 | // (I know mentioning it here will make people do that, so fuck y'all actually thinking of doing that) 67 | // HACK: Not ideal way of getting these values 68 | let user = split.get(3).unwrap(); 69 | let repo = split.get(4).unwrap(); 70 | let tag = split.get(7).unwrap(); 71 | let filename = split.get(8).unwrap(); 72 | 73 | let asset_data_link = format!( 74 | "https://{}@api.github.com/repos/{}/{}/releases/tags/{}", 75 | &token, &user, &repo, &tag 76 | ); 77 | 78 | let data = match get_agent().get(&asset_data_link).send() { 79 | Ok(o) => o.json::().unwrap(), 80 | Err(e) => { 81 | let error_string = e.to_string().replace(&token, "***"); 82 | panic!("{}", error_string); 83 | } 84 | }; 85 | 86 | for asset in data.assets.iter() { 87 | if asset.name.eq(filename) { 88 | // this is the correct asset! 89 | let download = asset 90 | .url 91 | .replace("api.github.com", &format!("{}@api.github.com", token)); 92 | 93 | let mut file = std::fs::File::create(out).expect("create so file failed"); 94 | 95 | get_agent().get(&download) 96 | .send() 97 | .unwrap() 98 | .copy_to(&mut file) 99 | .expect("Failed to write out downloaded bytes"); 100 | break; 101 | } 102 | } 103 | 104 | out.exists() 105 | } 106 | 107 | pub fn clone(mut url: String, branch: Option<&String>, out: &std::path::Path) -> bool { 108 | check_git(); 109 | if let Ok(token_unwrapped) = get_keyring().get_password() { 110 | if let Some(gitidx) = url.find("github.com") { 111 | url.insert_str(gitidx, &format!("{}@", token_unwrapped)); 112 | } 113 | } 114 | 115 | if url.ends_with('/') { 116 | url = url[..url.len() - 1].to_string(); 117 | } 118 | 119 | let mut git = std::process::Command::new("git"); 120 | git.arg("clone") 121 | .arg(format!("{}.git", url)) 122 | .arg(&out) 123 | .arg("--depth") 124 | .arg("1") 125 | .arg("--recurse-submodules") 126 | .arg("--shallow-submodules") 127 | .arg("--quiet") 128 | .arg("--single-branch"); 129 | 130 | if let Some(branch_unwrapped) = branch { 131 | git.arg("-b").arg(branch_unwrapped); 132 | } else { 133 | println!("No branch name found, cloning default branch"); 134 | } 135 | 136 | match git.output() { 137 | Ok(_o) => { 138 | if _o.status.code().unwrap_or(-1) != 0 { 139 | let mut error_string = std::str::from_utf8(_o.stderr.as_slice()) 140 | .unwrap() 141 | .to_string(); 142 | 143 | if let Ok(token_unwrapped) = get_keyring().get_password() { 144 | error_string = error_string.replace(&token_unwrapped, "***"); 145 | } 146 | 147 | panic!("Exit code {}: {}", _o.status, error_string); 148 | } 149 | } 150 | Err(e) => { 151 | let mut error_string = e.to_string(); 152 | 153 | if let Ok(token_unwrapped) = get_keyring().get_password() { 154 | error_string = error_string.replace(&token_unwrapped, "***"); 155 | } 156 | 157 | panic!("{}", error_string); 158 | } 159 | } 160 | 161 | out.exists() 162 | } 163 | 164 | #[derive(Serialize, Deserialize, Debug)] 165 | pub struct GithubReleaseAsset { 166 | pub url: String, 167 | pub name: String, 168 | } 169 | 170 | #[derive(Serialize, Deserialize, Debug)] 171 | pub struct GithubReleaseData { 172 | pub assets: Vec, 173 | } 174 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod git; 2 | pub mod toggle; 3 | pub mod network; -------------------------------------------------------------------------------- /src/utils/network.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | sync, 3 | time::Duration, 4 | }; 5 | 6 | 7 | use crate::data::config::Config; 8 | 9 | static AGENT: sync::OnceLock = sync::OnceLock::new(); 10 | 11 | pub fn get_agent() -> &'static reqwest::blocking::Client { 12 | AGENT.get_or_init(|| { 13 | reqwest::blocking::ClientBuilder::new() 14 | .connect_timeout(Duration::from_millis( 15 | Config::read_combine().timeout.unwrap(), 16 | )) 17 | .user_agent(format!("questpackagemanager-rust/{}", env!("CARGO_PKG_VERSION")).as_str()) 18 | .build() 19 | .expect("Client agent was not buildable") 20 | }) 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/toggle.rs: -------------------------------------------------------------------------------- 1 | use clap::{Subcommand}; 2 | 3 | #[derive(Subcommand, Debug, Clone)] 4 | 5 | pub enum Toggle { 6 | // Enable this thing 7 | Enable, 8 | // Disable this thing 9 | Disable, 10 | // Anything else 11 | Invalid, 12 | } 13 | 14 | impl std::str::FromStr for Toggle { 15 | type Err = std::string::ParseError; 16 | fn from_str(s: &str) -> Result { 17 | Ok(match s.to_lowercase().as_str() { 18 | // values that should return "true" more or less 19 | "enable" => Toggle::Enable, 20 | "e" => Toggle::Enable, 21 | "true" => Toggle::Enable, 22 | "t" => Toggle::Enable, 23 | "1" => Toggle::Enable, 24 | // values that should return "false" more or less 25 | "disable" => Toggle::Disable, 26 | "d" => Toggle::Disable, 27 | "false" => Toggle::Disable, 28 | "f" => Toggle::Disable, 29 | "0" => Toggle::Disable, 30 | // anything else 31 | _ => Toggle::Invalid, 32 | }) 33 | } 34 | } 35 | 36 | impl From for Toggle { 37 | fn from(b: bool) -> Self { 38 | match b { 39 | true => Toggle::Enable, 40 | false => Toggle::Disable, 41 | } 42 | } 43 | } 44 | 45 | impl From for bool { 46 | fn from(t: Toggle) -> Self { 47 | match t { 48 | Toggle::Enable => true, 49 | Toggle::Disable => false, 50 | Toggle::Invalid => false, 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | { 7 | "path": "..\\qpm-rust-test" 8 | } 9 | ], 10 | "settings": { 11 | "editor.formatOnPaste": true 12 | } 13 | } --------------------------------------------------------------------------------