├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md └── src ├── auto_req ├── builtin.rs ├── mod.rs └── script.rs ├── build_target.rs ├── cli.rs ├── config ├── file_info.rs ├── metadata.rs └── mod.rs ├── error.rs └── main.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | CARGO_TERM_COLOR: always 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/cache@v3 16 | with: 17 | path: | 18 | ~/.cargo/registry 19 | ~/.cargo/git 20 | target 21 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 22 | - name: Build 23 | run: cargo build --verbose 24 | 25 | - name: Run tests 26 | run: cargo test --verbose 27 | - name: Run binary for test purpose 28 | shell: bash -xe {0} 29 | run: | 30 | cargo run --profile dev -- --profile dev 31 | test -f target/generate-rpm/cargo-generate-rpm-*.rpm 32 | rm -f target/generate-rpm/cargo-generate-rpm-*.rpm 33 | cargo run --release -- generate-rpm 34 | test -f target/generate-rpm/cargo-generate-rpm-*.rpm 35 | rm -f target/generate-rpm/cargo-generate-rpm-*.rpm 36 | 37 | - name: Package 38 | run: cargo package 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .gradle 4 | out/ 5 | build/ 6 | gen/ 7 | deps/ 8 | exampleProject 9 | testData 10 | .DS_Store 11 | 12 | # Created by https://www.gitignore.io/api/vim,emacs,intellij,visualstudiocode,rust 13 | # Edit at https://www.gitignore.io/?templates=vim,emacs,intellij,visualstudiocode,rust 14 | 15 | ### Emacs ### 16 | # -*- mode: gitignore; -*- 17 | *~ 18 | \#*\# 19 | /.emacs.desktop 20 | /.emacs.desktop.lock 21 | *.elc 22 | auto-save-list 23 | tramp 24 | .\#* 25 | 26 | # Org-mode 27 | .org-id-locations 28 | *_archive 29 | 30 | # flymake-mode 31 | *_flymake.* 32 | 33 | # eshell files 34 | /eshell/history 35 | /eshell/lastdir 36 | 37 | # elpa packages 38 | /elpa/ 39 | 40 | # reftex files 41 | *.rel 42 | 43 | # AUCTeX auto folder 44 | /auto/ 45 | 46 | # cask packages 47 | .cask/ 48 | dist/ 49 | 50 | # Flycheck 51 | flycheck_*.el 52 | 53 | # server auth directory 54 | /server/ 55 | 56 | # projectiles files 57 | .projectile 58 | 59 | # directory configuration 60 | .dir-locals.el 61 | 62 | # network security 63 | /network-security.data 64 | 65 | 66 | ### Intellij ### 67 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 68 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 69 | 70 | # User-specific stuff 71 | .idea/**/workspace.xml 72 | .idea/**/tasks.xml 73 | .idea/**/usage.statistics.xml 74 | .idea/**/dictionaries 75 | .idea/**/shelf 76 | 77 | # Generated files 78 | .idea/**/contentModel.xml 79 | 80 | # Sensitive or high-churn files 81 | .idea/**/dataSources/ 82 | .idea/**/dataSources.ids 83 | .idea/**/dataSources.local.xml 84 | .idea/**/sqlDataSources.xml 85 | .idea/**/dynamic.xml 86 | .idea/**/uiDesigner.xml 87 | .idea/**/dbnavigator.xml 88 | 89 | # Gradle 90 | .idea/**/gradle.xml 91 | .idea/**/libraries 92 | 93 | # Gradle and Maven with auto-import 94 | # When using Gradle or Maven with auto-import, you should exclude module files, 95 | # since they will be recreated, and may cause churn. Uncomment if using 96 | # auto-import. 97 | # .idea/modules.xml 98 | # .idea/*.iml 99 | # .idea/modules 100 | # *.iml 101 | # *.ipr 102 | 103 | # CMake 104 | cmake-build-*/ 105 | 106 | # Mongo Explorer plugin 107 | .idea/**/mongoSettings.xml 108 | 109 | # File-based project format 110 | *.iws 111 | 112 | # IntelliJ 113 | out/ 114 | 115 | # mpeltonen/sbt-idea plugin 116 | .idea_modules/ 117 | 118 | # JIRA plugin 119 | atlassian-ide-plugin.xml 120 | 121 | # Cursive Clojure plugin 122 | .idea/replstate.xml 123 | 124 | # Crashlytics plugin (for Android Studio and IntelliJ) 125 | com_crashlytics_export_strings.xml 126 | crashlytics.properties 127 | crashlytics-build.properties 128 | fabric.properties 129 | 130 | # Editor-based Rest Client 131 | .idea/httpRequests 132 | 133 | # Android studio 3.1+ serialized cache file 134 | .idea/caches/build_file_checksums.ser 135 | 136 | ### Intellij Patch ### 137 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 138 | 139 | # *.iml 140 | # modules.xml 141 | # .idea/misc.xml 142 | # *.ipr 143 | 144 | # Sonarlint plugin 145 | .idea/sonarlint 146 | 147 | ### Rust ### 148 | # Generated by Cargo 149 | # will have compiled files and executables 150 | /target 151 | 152 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 153 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 154 | Cargo.lock 155 | 156 | # These are backup files generated by rustfmt 157 | **/*.rs.bk 158 | 159 | ### Vim ### 160 | # Swap 161 | [._]*.s[a-v][a-z] 162 | [._]*.sw[a-p] 163 | [._]s[a-rt-v][a-z] 164 | [._]ss[a-gi-z] 165 | [._]sw[a-p] 166 | 167 | # Session 168 | Session.vim 169 | Sessionx.vim 170 | 171 | # Temporary 172 | .netrwhist 173 | # Auto-generated tag files 174 | tags 175 | # Persistent undo 176 | [._]*.un~ 177 | 178 | ### VisualStudioCode ### 179 | .vscode/* 180 | !.vscode/settings.json 181 | !.vscode/tasks.json 182 | !.vscode/launch.json 183 | !.vscode/extensions.json 184 | 185 | ### VisualStudioCode Patch ### 186 | # Ignore all local history of files 187 | .history 188 | 189 | # End of https://www.gitignore.io/api/vim,emacs,intellij,visualstudiocode,rust 190 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-generate-rpm" 3 | license = "MIT" 4 | authors = ["@cat_in_136"] 5 | categories = [ 6 | "command-line-utilities", 7 | "development-tools::cargo-plugins", 8 | "development-tools::build-utils", 9 | ] 10 | description = "Generate a binary RPM package (.rpm) from Cargo projects" 11 | homepage = "https://github.com/cat-in-136/cargo-generate-rpm" 12 | readme = "README.md" 13 | keywords = ["rpm", "package", "cargo", "subcommand"] 14 | repository = "https://github.com/cat-in-136/cargo-generate-rpm" 15 | version = "0.16.1" 16 | edition = "2024" 17 | 18 | [dependencies] 19 | glob = "0.3" 20 | rpm = { version = "0.17", default-features = false, features = [ 21 | "zstd-compression", 22 | "gzip-compression", 23 | "xz-compression", 24 | "bzip2-compression", 25 | ] } 26 | toml = "0.8" 27 | cargo_toml = "0.22" 28 | clap = { version = "~4.5", features = ["derive"] } 29 | color-print = "0.3" 30 | thiserror = "2" 31 | elf = "0.7" 32 | 33 | [dev-dependencies] 34 | tempfile = "3" 35 | 36 | [package.metadata.generate-rpm] 37 | assets = [ 38 | { source = "target/release/cargo-generate-rpm", dest = "/usr/bin/cargo-generate-rpm", mode = "0755" }, 39 | { source = "LICENSE", dest = "/usr/share/doc/cargo-generate-rpm/LICENSE", doc = true, mode = "0644" }, 40 | { source = "README.md", dest = "/usr/share/doc/cargo-generate-rpm/README.md", doc = true, mode = "0644" }, 41 | ] 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 @cat_in_136 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo-generate-rpm 2 | 3 | [Cargo](https://doc.rust-lang.org/cargo/) helper command to generate a binary [RPM package](https://rpm.org/) (.rpm) 4 | from Cargo project. 5 | 6 | This command does not depend on `rpmbuild` and generates an RPM package file without a spec file by 7 | using the [`rpm`](https://crates.io/crates/rpm) crate. 8 | 9 | ![Rust](https://github.com/cat-in-136/cargo-generate-rpm/workflows/Rust/badge.svg) 10 | [![cargo-generate-rpm at crates.io](https://img.shields.io/crates/v/cargo-generate-rpm.svg)](https://crates.io/crates/cargo-generate-rpm) 11 | 12 | Legacy systems requiring RPMv3 (e.g. CentOS 7) are no longer supported due to rpm-rs compatibility. 13 | Use versions prior to 0.15 for such a system. 14 | 15 | ## Install 16 | 17 | ```sh 18 | cargo install cargo-generate-rpm 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```sh 24 | cargo build --release 25 | strip -s target/release/XXX 26 | cargo generate-rpm 27 | ``` 28 | 29 | Upon run `cargo generate-rpm` on your cargo project, a binary RPM package file will be created 30 | in `target/generate-rpm/XXX.rpm`. 31 | You can change the RPM package file location using `-o` option. 32 | 33 | In advance, run `cargo build --release` and strip the debug symbols (`strip -s target/release/XXX`), because these are not 34 | run upon `cargo generate-rpm` as of now. 35 | 36 | ## Configuration 37 | 38 | This command generates RPM metadata 39 | from [the `Cargo.toml` file](https://doc.rust-lang.org/cargo/reference/manifest.html): 40 | 41 | ### `[package.metadata.generate-rpm]` options 42 | 43 | * name: the package name. If not present, `package.name` is used. 44 | * version: the package version. If not present, `package.version` is used. 45 | * license: the package license. If not present, `package.license` is used. 46 | * summary: the package summary/description. If not present, `package.description` is used. 47 | * url: the package homepage url. If not present, `package.homepage` is used. If neither present, `package.repository` is 48 | used. 49 | * assets: (**mandatory**) the array of the files to be included in the package 50 | * source: the location of that asset in the Rust project. (e.g. `target/release/XXX`) 51 | Wildcard character `*` is allowed. 52 | * dest: the install-destination. (e.g. `/usr/bin/XXX`) It shall be a file path or a directory path ending `/`. 53 | If source contains wildcard character `*`, it must be a directory, not a file path. 54 | * mode: the permissions as octal string. (e.g. `755` to indicate `-rwxr-xr-x`) 55 | * config: set true if it is a configuration file. 56 | Set the string `"noreplace"` instead to avoid overwriting an existing file that have been modified. 57 | (Not supported for `"missingok"` as of now) 58 | * doc: set true if it is a document file. 59 | * user: the owner of the file. 60 | * group: the group owner of the file. 61 | * caps: optional string of capabilities. (e.g. `cap_sys_admin=pe`) 62 | * release: optional string of release. 63 | * epoch: optional number of epoch. 64 | * pre_install_script: optional string or file path of pre_install_script. 65 | * pre_install_script_flags: optional integer value to set scriptlet flags. 66 | * pre_install_script_prog: optional string array to set scriptlet interpreter/arguments. 67 | * pre_uninstall_script: optional string or file path of pre_uninstall_script. 68 | * pre_uninstall_script_flags: optional integer value to set scriptlet flags. 69 | * pre_uninstall_script_prog: optional string array to set scriptlet interpreter/arguments. 70 | * pre_trans_script: optional string or file path of pre_trans_script. 71 | * pre_trans_script_flags: optional integer value to set scriptlet flags. 72 | * pre_trans_script_prog: optional string array to set scriptlet interpreter/arguments. 73 | * pre_untrans_script: optional string or file path of pre_untrans_script. 74 | * pre_untrans_script_flags: optional integer value to set scriptlet flags. 75 | * pre_untrans_script_prog: optional string array to set scriptlet interpreter/arguments. 76 | * post_install_script: optional string or file path of post_install_script. 77 | * post_install_script_flags: optional integer value to set scriptlet flags. 78 | * post_install_script_prog: optional string array to set scriptlet interpreter/arguments. 79 | * post_uninstall_script: optional string or file path of post_uninstall_script. 80 | * post_uninstall_script_flags: optional integer value to set scriptlet flags. 81 | * post_uninstall_script_prog: optional string array to set scriptlet interpreter/arguments. 82 | * post_trans_script: optional string or file path of post_trans_script. 83 | * post_trans_script_flags: optional integer value to set scriptlet flags. 84 | * post_trans_script_prog: optional string array to set scriptlet interpreter/arguments. 85 | * post_untrans_script: optional string or file path of post_untrans_script. 86 | * post_untrans_script_flags: optional integer value to set scriptlet flags. 87 | * post_untrans_script_prog: optional string array to set scriptlet interpreter/arguments. 88 | * requires: optional list of Requires 89 | * auto-req: optional string `"no"` to disable the automatic dependency process 90 | * require-sh: optional boolean `false` to omit `/bin/sh` from Requirements 91 | * obsoletes: optional list of Obsoletes 92 | * conflicts: optional list of Conflicts 93 | * provides: optional list of Provides 94 | * recommends: optional list of Recommends 95 | * supplements: optional list of Supplements 96 | * suggests: optional list of Suggests 97 | * enhances: optional list of Enhances 98 | * vendor: optional string of Vendor 99 | 100 | Adding assets such as the binary file, ``.desktop`` file, or icons, shall be written in the following way. 101 | 102 | ```toml 103 | [package.metadata.generate-rpm] 104 | assets = [ 105 | { source = "target/release/XXX", dest = "/usr/bin/XXX", mode = "755" }, 106 | { source = "/XXX.desktop", dest = "/usr/share/applications/XXX.desktop", mode = "644" }, 107 | { source = "/*/apps/XXX.png", dest = "/usr/share/icons/hicolor/", mode = "644" }, 108 | ] 109 | ``` 110 | 111 | ### `[package.metadata.generate-rpm.{requires,obsoletes,conflicts,provides,recommends,supplements,suggests,enhances}]` options 112 | 113 | Dependencies such as "requires", "obsoletes", "conflicts" and "provides" and 114 | weak dependencies such as "recommends", "supplements", "suggests", and "enhances" 115 | shall be written in similar way as dependencies in Cargo.toml. 116 | 117 | ```toml 118 | [package.metadata.generate-rpm.requires] 119 | alternative = "*" 120 | filesystem = ">= 3" 121 | ``` 122 | 123 | This example states that the package requires with any versions of `alternative` and all versions of `filesystem` 3.0 or 124 | higher. 125 | 126 | Following table lists the version comparisons: 127 | 128 | | Comparison | Meaning | 129 | |--------------------------|------------------------------------------------------------------| 130 | | `package = "*"` | A package at any version number | 131 | | `package = "< version"` | A package with a version number less than version | 132 | | `package = "<= version"` | A package with a version number less than or equal to version | 133 | | `package = "= version"` | A package with a version number equal to version | 134 | | `package = "> version"` | A package with a version number greater than version | 135 | | `package = ">= version"` | A package with a version number greater than or equal to version | 136 | 137 | It is necessary to place a space between version and symbols such as `<`, `<=`, etc... 138 | `package = "version"` is not accepted, instead use `package = "= version"`. 139 | 140 | To specify multiple version requirements, the version comparisons shall be separated with a comma 141 | e.g., `package = ">= 1.2, < 3.4"`. 142 | 143 | This command automatically determines what shared libraries a package requires. 144 | There may be times when the automatic dependency processing is not desired. 145 | The packege author and users can configure the processing. 146 | 147 | * `--auto-req auto` or `--auto-req` not specified: Use the preferred automatic dependency process. 148 | The following rules are used: 149 | * If `package.metadata.generate-rpm.auto-req` set to `"no"` or `"disabled"`, the process is disabled. 150 | * If `/usr/lib/rpm/find-requires` exists, it is used (same behaviour as `--auto-req find-requires`). 151 | * Otherwise, builtin procedure is used (same behaviour as `--auto-req builtin`). 152 | * `--auto-req disabled`, `--auto-req no`: Disable the discovery of dependencies. 153 | * `--auto-req builtin`: Use the builtin procedure based on `ldd`. 154 | * `--auto-req find-requires`: Use `/usr/lib/rpm/find-requires`. This behavior is the same as the original `rpmbuild`. 155 | * `--auto-req /path/to/find-requires`: Use the specified external program is used. 156 | 157 | `/bin/sh` is always added to the package requirements. To disable it, set `package.metadata.generate-rpm.require-sh` 158 | to `false`. You should not do this if you use scripts such as `pre_install_script` or if your assets contain shell 159 | scripts. 160 | 161 | ### Overwrite configuration 162 | 163 | `[package.metadata.generate-rpm]` can be overwritten. The following command line options are used: 164 | 165 | * `--metadata-overwrite=TOML_FILE.toml` : Overwrite the `[package.metadata.generate-rpm]` options with the contents of 166 | the specified TOML file. Multiple files can be specified, separated by commas. 167 | * `--metadata-overwrite=TOML_FILE.toml#TOML.PATH` : Overwrites the `[package.metadata.generate-rpm]` options with the 168 | table specified in the TOML path of the TOML file. 169 | Only a sequence of bare keys connected by dots is acceptable for the TOML path. 170 | Path containing quoted keys (such as `metadata."παραλλαγή"`) cannot be acceptable. 171 | Multiple files with TOML pathes can be specified, separated by commas. 172 | * `-s 'toml "text"'` or `--set-metadata='toml "text"'` : Overwrite the `[package.metadata.generate-rpm]` options with 173 | inline TOML text. 174 | The argument text --- inline TOML text must be enclosed in quotation marks since it contains spaces. 175 | * `--variant=VARIANT` : Overwrites the `[package.metadata.generate-rpm]` options with the table specified 176 | in `[package.metadata.generate-rpm.variants.VARIANT]` of the TOML file. 177 | It is a shortcut to `--metadata-overwrite=path/to/Cargo.toml#package.metadata.generate-rpm.variants.VARIANT`. 178 | It is intended for providing multiple variants of the metadata in a Cargo.toml and ability for the users to select the 179 | variant using --variant=name option. 180 | Multiple variant names can be specified, separated by commas. 181 | 182 | These options may be specified multiple times, with the last one written being applied regardless of the kind of option. 183 | For example, the arguments `-s 'release = "alpha"' --metadata-overwrite=beta.toml` where beta.toml 184 | contains `release = "beta"`, then gives `release = "beta"`. 185 | 186 | ## Advanced Usage 187 | 188 | ### Workspace 189 | 190 | To generate an RPM package from a member of a workspace, execute `cargo generate-rpm` in the workspace directory 191 | with specifying the package (directory path) with option `-p`: 192 | 193 | ```sh 194 | cargo build --release 195 | strip -s target/release/XXX 196 | cargo generate-rpm -p XXX 197 | ``` 198 | 199 | `[package.metadata.generate-rpm]` options should be written in `XXX/Cargo.toml`. 200 | 201 | When the option `-p` specified, first, the asset file `source` shall be treated as a relative path from the current 202 | directory. 203 | If not found, it shall be treated as a relative path from the directory of the package. 204 | If both not found, `cargo generate-rpm` shall fail with an error. 205 | 206 | For example, `source = target/bin/XXX` would usually be treated as a relative path from the current directory. 207 | Because all packages in the workspace share a common output directory that is located `target` in workspace directory. 208 | 209 | ### Cross compilation 210 | 211 | This command supports `--target-dir`, `--target`, and `--profile` options like `cargo build`. 212 | Depending on these options, this command changes the RPM package file location and replaces `target/release/` of 213 | the source locations of the assets. 214 | 215 | ```sh 216 | cargo build --release --target x86_64-unknown-linux-gnu 217 | cargo generate-rpm --target x86_64-unknown-linux-gnu 218 | ``` 219 | 220 | When `--target-dir TARGET-DIR` and `--target x86_64-unknown-linux-gnu` are specified, a binary RPM file will be created 221 | at `TARGET-DIR/x86_64-unknown-linux-gnu/generate-rpm/XXX.rpm` instead of `target/generate-rpm/XXX.rpm`. 222 | In this case, the source of the asset `{ source = "target/release/XXX", dest = "/usr/bin/XXX" }` will be treated as 223 | `TARGET-DIR/x86_64-unknown-linux-gnu/release/XXX` instead of `target/release/XXX`. 224 | 225 | You can use `CARGO_BUILD_TARGET` environment variable instead of `--target` option and `CARGO_BUILD_TARGET_DIR` or 226 | `CARGO_TARGET_DIR` instead of `--target-dir`. 227 | 228 | Similarly, if using a custom build profile with, for example, `--profile custom` the source of the asset 229 | `{ source = "target/release/XXX" }` will be treated as `target/custom/XXX`. 230 | 231 | ### Payload compress type 232 | 233 | The default payload compress type of the generated RPM file is zstd. 234 | You can specify the payload compress type with `--payload-compress TYPE`: none, gzip, or zstd. 235 | 236 | ### Scriptlet Flags and Prog Settings 237 | 238 | Scriptlet settings can be configured via `*_script_flags` and `*_script_prog` settings. 239 | 240 | **Scriptlet Flags** 241 | 242 | | Flag | Setting Value | Description | Example Usage | 243 | | ---- | ------------- | ----------- | ------- | 244 | | `RPMSCRIPT_FLAG_EXPAND` | 1 | Enables macro expansion | `pre_install_script_flags = 0b001` | 245 | | `RPMSCRIPT_FLAG_QFORMAT` | 2 | Enables header query format expansion | `pre_install_script_flags = 0b010` | 246 | | `RPMSCRIPT_FLAG_CRITICAL` | 4 | Enables critical severity for scriplet success or failure | `pre_install_script_flags = 0b100` | 247 | 248 | **Example** 249 | 250 | ```toml 251 | pre_install_script = """ 252 | echo preinstall 253 | """ 254 | pre_install_script_flags = 0b011 # Enables EXPAND and QFORMAT flags 255 | pre_install_script_prog = ["/bin/blah/bash", "-c"] # Sets the interpreter/argument settings for the scriptlet 256 | ``` 257 | -------------------------------------------------------------------------------- /src/auto_req/builtin.rs: -------------------------------------------------------------------------------- 1 | use crate::error::AutoReqError; 2 | use elf::abi::{EM_ALPHA, SHT_GNU_HASH, SHT_HASH}; 3 | use elf::endian::AnyEndian; 4 | use elf::file::{Class, FileHeader}; 5 | use elf::{ElfStream, ParseError}; 6 | use std::collections::BTreeSet; 7 | use std::ffi::OsString; 8 | use std::fs::File; 9 | use std::io::{BufRead, BufReader, Read}; 10 | use std::path::Path; 11 | use std::process::{Command, Stdio}; 12 | 13 | #[derive(Debug)] 14 | struct ElfInfo { 15 | machine: (Class, u16), 16 | got_hash: bool, 17 | got_gnu_hash: bool, 18 | } 19 | 20 | impl ElfInfo { 21 | fn new>(path: P) -> Result { 22 | let file = File::open(path)?; 23 | let elf_stream = ElfStream::open_stream(file)?; 24 | let ehdr: FileHeader = elf_stream.ehdr; 25 | let shdrs = elf_stream.section_headers(); 26 | 27 | let machine = (ehdr.class, ehdr.e_machine); 28 | let got_hash = shdrs.iter().any(|s| s.sh_type == SHT_HASH); 29 | let got_gnu_hash = shdrs.iter().any(|s| s.sh_type == SHT_GNU_HASH); 30 | 31 | Ok(Self { 32 | machine, 33 | got_hash, 34 | got_gnu_hash, 35 | }) 36 | } 37 | 38 | pub fn marker(&self) -> Option<&'static str> { 39 | match self.machine { 40 | (Class::ELF64, EM_ALPHA) | (Class::ELF64, 0x9026) => None, // alpha doesn't traditionally have 64bit markers 41 | (Class::ELF64, _) => Some("(64bit)"), 42 | (Class::ELF32, _) => None, 43 | } 44 | } 45 | } 46 | 47 | #[test] 48 | fn test_elf_info_new() { 49 | ElfInfo::new("/bin/sh").unwrap(); 50 | } 51 | 52 | fn find_requires_by_ldd( 53 | path: &Path, 54 | marker: Option<&str>, 55 | ) -> Result, AutoReqError> { 56 | fn skip_so_name(so_name: &str) -> bool { 57 | so_name.contains(".so") 58 | && (so_name.starts_with("ld.") 59 | || so_name.starts_with("ld-") 60 | || so_name.starts_with("ld64.") 61 | || so_name.starts_with("ld64-") 62 | || so_name.starts_with("lib")) 63 | } 64 | 65 | let process = Command::new("ldd") 66 | .arg("-v") 67 | .arg(path.as_os_str()) 68 | .stdout(Stdio::piped()) 69 | .spawn() 70 | .map_err(|e| AutoReqError::ProcessError(OsString::from("ldd"), e))?; 71 | 72 | let mut s = String::new(); 73 | process 74 | .stdout 75 | .unwrap() 76 | .read_to_string(&mut s) 77 | .map_err(|e| AutoReqError::ProcessError(OsString::from("ldd"), e))?; 78 | 79 | let unversioned_libraries = s 80 | .split('\n') 81 | .take_while(|&line| !line.trim().is_empty()) 82 | .filter_map(|line| line.trim_start().split(' ').next()); 83 | let versioned_libraries = s 84 | .split('\n') 85 | .skip_while(|&line| !line.contains("Version information:")) 86 | .skip(1) 87 | .skip_while(|&line| !line.contains(path.to_str().unwrap())) 88 | .skip(1) 89 | .take_while(|&line| line.contains(" => ")) 90 | .filter_map(|line| line.trim_start().split(" => ").next()); 91 | 92 | let marker = marker.unwrap_or_default(); 93 | let mut requires = BTreeSet::new(); 94 | for name in unversioned_libraries 95 | .into_iter() 96 | .chain(versioned_libraries.into_iter()) 97 | .filter(|&name| skip_so_name(name)) 98 | { 99 | if name.contains(" (") { 100 | // Insert "unversioned" library name 101 | requires.insert(format!("{}(){}", name.split(' ').next().unwrap(), marker)); 102 | requires.insert(format!("{}{}", name.replace(' ', ""), marker)); 103 | } else { 104 | requires.insert(format!("{}(){}", name.replace(' ', ""), marker)); 105 | } 106 | } 107 | Ok(requires) 108 | } 109 | 110 | fn find_requires_of_elf(path: &Path) -> Result>, AutoReqError> { 111 | if let Ok(info) = ElfInfo::new(path) { 112 | let mut requires = find_requires_by_ldd(path, info.marker())?; 113 | if info.got_gnu_hash && !info.got_hash { 114 | requires.insert("rtld(GNU_HASH)".to_string()); 115 | } 116 | Ok(Some(requires)) 117 | } else { 118 | Ok(None) 119 | } 120 | } 121 | 122 | #[test] 123 | fn test_find_requires_of_elf() { 124 | let requires = find_requires_of_elf(Path::new("/bin/sh")).unwrap().unwrap(); 125 | assert!(requires 126 | .iter() 127 | .all(|v| v.contains(".so") || v == "rtld(GNU_HASH)")); 128 | assert!(matches!(find_requires_of_elf(Path::new(file!())), Ok(None))); 129 | } 130 | 131 | fn find_require_of_shebang(path: &Path) -> Result, AutoReqError> { 132 | let interpreter = { 133 | let file = File::open(path)?; 134 | let mut read = BufReader::new(file); 135 | let mut shebang = [0u8; 2]; 136 | let shebang_size = read.read(&mut shebang)?; 137 | if shebang_size == 2 || shebang == [b'#', b'!'] { 138 | let mut line = String::new(); 139 | read.read_line(&mut line)?; 140 | line.trim() 141 | .split(|c: char| !c.is_ascii() || c.is_whitespace()) 142 | .next() 143 | .map(String::from) 144 | } else { 145 | None 146 | } 147 | }; 148 | 149 | Ok(match interpreter { 150 | Some(i) if Path::new(&i).exists() => Some(i), 151 | _ => None, 152 | }) 153 | } 154 | 155 | #[test] 156 | fn test_find_require_of_shebang() { 157 | assert!(matches!( 158 | find_require_of_shebang(Path::new("/usr/bin/ldd")), 159 | Ok(Some(_)) 160 | )); 161 | assert!(matches!( 162 | find_require_of_shebang(Path::new(file!())), 163 | Ok(None) 164 | )); 165 | } 166 | 167 | #[cfg(unix)] 168 | fn is_executable(path: &Path) -> bool { 169 | use std::os::unix::fs::MetadataExt; 170 | std::fs::metadata(path) 171 | .map(|metadata| metadata.mode()) 172 | .map(|mode| mode & 0o111 != 0) 173 | .unwrap_or_default() 174 | } 175 | 176 | #[cfg(unix)] 177 | #[test] 178 | fn test_is_executable() { 179 | assert!(is_executable(Path::new("/bin/sh"))); 180 | assert!(!is_executable(Path::new(file!()))); 181 | } 182 | 183 | #[cfg(not(unix))] 184 | fn is_executable(path: &Path) -> bool { 185 | true 186 | } 187 | 188 | /// find requires. 189 | pub(super) fn find_requires>(path: &[P]) -> Result, AutoReqError> { 190 | let mut requires = Vec::new(); 191 | for p in path.iter().map(|v| v.as_ref()) { 192 | if is_executable(p) { 193 | if let Some(elf_requires) = find_requires_of_elf(p)? { 194 | requires.extend(elf_requires); 195 | } else if let Some(shebang_require) = find_require_of_shebang(p)? { 196 | requires.push(shebang_require); 197 | } 198 | } 199 | } 200 | Ok(requires) 201 | } 202 | -------------------------------------------------------------------------------- /src/auto_req/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{cli, error::AutoReqError}; 2 | use std::path::{Path, PathBuf}; 3 | 4 | mod builtin; 5 | mod script; 6 | 7 | /// The path to the system default find-requires program 8 | const RPM_FIND_REQUIRES: &str = "/usr/lib/rpm/find-requires"; 9 | 10 | /// The method to auto-req 11 | #[derive(Debug, PartialEq, Eq)] 12 | pub enum AutoReqMode { 13 | /// Automatically selected 14 | Auto, 15 | /// Disable 16 | Disabled, 17 | /// `find-requires` script 18 | Script(PathBuf), 19 | /// Builtin 20 | BuiltIn, 21 | } 22 | 23 | impl From for AutoReqMode { 24 | fn from(value: cli::AutoReqMode) -> Self { 25 | match value { 26 | cli::AutoReqMode::Auto => AutoReqMode::Auto, 27 | cli::AutoReqMode::Disabled => AutoReqMode::Disabled, 28 | cli::AutoReqMode::Builtin => AutoReqMode::BuiltIn, 29 | cli::AutoReqMode::FindRequires => AutoReqMode::Script(PathBuf::from(RPM_FIND_REQUIRES)), 30 | cli::AutoReqMode::Script(path) => AutoReqMode::Script(path), 31 | } 32 | } 33 | } 34 | 35 | /// Find requires 36 | pub fn find_requires, P: AsRef>( 37 | files: T, 38 | mode: AutoReqMode, 39 | ) -> Result, AutoReqError> { 40 | match mode { 41 | AutoReqMode::Auto => { 42 | if Path::new(RPM_FIND_REQUIRES).exists() { 43 | find_requires(files, AutoReqMode::Script(PathBuf::from(RPM_FIND_REQUIRES))) 44 | } else { 45 | find_requires(files, AutoReqMode::BuiltIn) 46 | } 47 | } 48 | AutoReqMode::Disabled => Ok(Vec::new()), 49 | AutoReqMode::Script(script) => Ok(script::find_requires( 50 | files.into_iter().collect::>().as_slice(), 51 | script.as_path(), 52 | )?), 53 | AutoReqMode::BuiltIn => Ok(builtin::find_requires( 54 | files.into_iter().collect::>().as_slice(), 55 | )?), 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/auto_req/script.rs: -------------------------------------------------------------------------------- 1 | use crate::error::AutoReqError; 2 | use std::ffi::OsStr; 3 | use std::io::{BufRead, BufReader, Write}; 4 | use std::path::Path; 5 | use std::process::{Command, Stdio}; 6 | 7 | /// find requires using `find-requires` program located at `script_path`. 8 | pub(super) fn find_requires, S: AsRef>( 9 | path: &[P], 10 | script_path: S, 11 | ) -> Result, AutoReqError> { 12 | let process = Command::new(&script_path) 13 | .stdin(Stdio::piped()) 14 | .stdout(Stdio::piped()) 15 | .spawn() 16 | .map_err(|e| AutoReqError::ProcessError(script_path.as_ref().to_os_string(), e))?; 17 | 18 | let filenames = path 19 | .iter() 20 | .filter_map(|v| v.as_ref().to_str()) 21 | .collect::>() 22 | .join("\n"); 23 | process 24 | .stdin 25 | .unwrap() 26 | .write_all(filenames.as_bytes()) 27 | .map_err(|e| AutoReqError::ProcessError(script_path.as_ref().to_os_string(), e))?; 28 | 29 | let mut requires = Vec::new(); 30 | let reader = BufReader::new(process.stdout.unwrap()); 31 | 32 | for line in reader.lines() { 33 | match line { 34 | Ok(content) if content == "" => (), // ignore empty line 35 | Ok(content) => requires.push(content), 36 | Err(e) => { 37 | return Err(AutoReqError::ProcessError( 38 | script_path.as_ref().to_os_string(), 39 | e, 40 | )) 41 | } 42 | } 43 | } 44 | 45 | Ok(requires) 46 | } 47 | 48 | #[test] 49 | fn test_find_requires() { 50 | assert_eq!( 51 | find_requires(&[file!()], "/bin/cat").unwrap(), 52 | vec![file!().to_string()] 53 | ); 54 | assert!(matches!( 55 | find_requires(&[file!()], "not-exist"), 56 | Err(AutoReqError::ProcessError(_, _)) 57 | )); 58 | if Path::new(super::RPM_FIND_REQUIRES).is_file() { 59 | assert!(!find_requires(&["/bin/cat"], super::RPM_FIND_REQUIRES) 60 | .unwrap() 61 | .is_empty()); 62 | } 63 | 64 | // empty dependencies shall return empty vector 65 | assert!(find_requires(&[file!()], "/bin/false").unwrap().is_empty()); 66 | if Path::new(super::RPM_FIND_REQUIRES).is_file() { 67 | assert!(find_requires(&["/dev/null"], super::RPM_FIND_REQUIRES) 68 | .unwrap() 69 | .is_empty()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/build_target.rs: -------------------------------------------------------------------------------- 1 | use std::env::consts::ARCH; 2 | use std::path::{Path, PathBuf}; 3 | 4 | use crate::cli::Cli; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct BuildTarget { 8 | target_dir: Option, 9 | target: Option, 10 | profile: String, 11 | arch: Option, 12 | } 13 | 14 | impl BuildTarget { 15 | pub fn new(args: &Cli) -> Self { 16 | Self { 17 | target_dir: args.target_dir.clone(), 18 | target: args.target.clone(), 19 | profile: args.profile.clone(), 20 | arch: args.arch.clone(), 21 | } 22 | } 23 | 24 | pub fn profile(&self) -> &str { 25 | self.profile.as_str() 26 | } 27 | 28 | pub fn build_target_path(&self) -> PathBuf { 29 | if let Some(target_dir) = &self.target_dir { 30 | PathBuf::from(&target_dir) 31 | } else { 32 | let target_build_dir = std::env::var("CARGO_BUILD_TARGET_DIR") 33 | .or_else(|_| std::env::var("CARGO_TARGET_DIR")) 34 | .unwrap_or("target".to_string()); 35 | PathBuf::from(&target_build_dir) 36 | } 37 | } 38 | 39 | pub fn target_path>(&self, dir_name: P) -> PathBuf { 40 | let mut path = self.build_target_path(); 41 | if let Some(target) = &self.target { 42 | path = path.join(target) 43 | } 44 | path.join(dir_name) 45 | } 46 | 47 | pub fn binary_arch(&self) -> String { 48 | if let Some(arch) = &self.arch { 49 | arch.clone() 50 | } else { 51 | let arch = self 52 | .target 53 | .as_ref() 54 | .and_then(|v| v.split('-').next()) 55 | .unwrap_or(ARCH); 56 | 57 | match arch { 58 | "x86" => "i586", 59 | "arm" => "armhfp", 60 | "powerpc" => "ppc", 61 | "powerpc64" => "ppc64", 62 | "powerpc64le" => "ppc64le", 63 | _ => arch, 64 | } 65 | .to_string() 66 | } 67 | } 68 | } 69 | 70 | #[cfg(test)] 71 | mod test { 72 | use super::*; 73 | 74 | #[test] 75 | fn test_build_target_path() { 76 | let args = crate::cli::Cli::default(); 77 | let target = BuildTarget::new(&args); 78 | assert_eq!(target.build_target_path(), PathBuf::from("target")); 79 | 80 | let target = BuildTarget { 81 | target_dir: Some("/tmp/foobar/target".to_string()), 82 | ..target 83 | }; 84 | assert_eq!( 85 | target.build_target_path(), 86 | PathBuf::from("/tmp/foobar/target") 87 | ); 88 | } 89 | 90 | #[test] 91 | fn test_target_path() { 92 | let args = crate::cli::Cli::default(); 93 | let default_target = BuildTarget::new(&args); 94 | assert_eq!( 95 | default_target.target_path("release"), 96 | PathBuf::from("target/release") 97 | ); 98 | 99 | let target = BuildTarget { 100 | target: Some("x86_64-unknown-linux-gnu".to_string()), 101 | ..default_target.clone() 102 | }; 103 | assert_eq!( 104 | target.target_path("release"), 105 | PathBuf::from("target/x86_64-unknown-linux-gnu/release") 106 | ); 107 | 108 | let target = BuildTarget { 109 | target_dir: Some("/tmp/foobar/target".to_string()), 110 | ..default_target.clone() 111 | }; 112 | assert_eq!( 113 | target.target_path("debug"), 114 | PathBuf::from("/tmp/foobar/target/debug") 115 | ); 116 | 117 | let target = BuildTarget { 118 | target_dir: Some("/tmp/foobar/target".to_string()), 119 | target: Some("x86_64-unknown-linux-gnu".to_string()), 120 | ..default_target 121 | }; 122 | assert_eq!( 123 | target.target_path("debug"), 124 | PathBuf::from("/tmp/foobar/target/x86_64-unknown-linux-gnu/debug") 125 | ); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::{ 2 | builder::{PathBufValueParser, PossibleValuesParser, TypedValueParser, ValueParserFactory}, 3 | Arg, ArgMatches, Command, CommandFactory, FromArgMatches, Parser, ValueEnum, 4 | }; 5 | use std::ffi::{OsStr, OsString}; 6 | use std::path::PathBuf; 7 | 8 | /// Wrapper used when the application is executed as Cargo plugin 9 | #[derive(Debug, Parser)] 10 | #[command(name = "cargo")] 11 | #[command(bin_name = "cargo")] 12 | enum CargoWrapper { 13 | GenerateRpm(Cli), 14 | } 15 | 16 | /// Arguments of the command line interface 17 | #[derive(Debug, Parser)] 18 | #[command(name = "cargo-generate-rpm")] 19 | #[command(bin_name = "cargo-generate-rpm")] 20 | #[command(author, version, about, long_about = None)] 21 | pub struct Cli { 22 | /// Target arch of generated package. 23 | #[arg(short, long)] 24 | pub arch: Option, 25 | 26 | /// Output file or directory. 27 | #[arg(short, long)] 28 | pub output: Option, 29 | 30 | /// Name of a crate in the workspace for which 31 | /// RPM package will be generated. 32 | #[arg(short, long)] 33 | pub package: Option, 34 | 35 | /// Automatic dependency processing mode. 36 | #[arg(long, default_value = "auto", 37 | help = "Automatic dependency processing mode. \ 38 | [possible values: auto, disabled, builtin, find-requires, /path/to/find-requires]", 39 | long_help = color_print::cstr!("Automatic dependency processing mode.\n\n\ 40 | Possible values:\n\ 41 | - auto: Use the preferred automatic dependency process.\n\ 42 | - disabled: Disable the discovery of dependencies. [alias: no]\n\ 43 | - builtin: Use the builtin procedure based on ldd.\n\ 44 | - find-requires: Use /usr/lib/rpm/find-requires.\n\ 45 | - /path/to/find-requires: Use the specified external program."))] 46 | pub auto_req: AutoReqMode, 47 | 48 | /// Sub-directory name for all generated artifacts. May be 49 | /// specified with CARGO_BUILD_TARGET environment 50 | /// variable. 51 | #[arg(long)] 52 | pub target: Option, 53 | 54 | /// Directory for all generated artifacts. May be 55 | /// specified with CARGO_BUILD_TARGET_DIR or 56 | /// CARGO_TARGET_DIR environment variables. 57 | #[arg(long)] 58 | pub target_dir: Option, 59 | 60 | /// Build profile for packaging. 61 | #[arg(long, default_value = "release")] 62 | pub profile: String, 63 | 64 | /// Compression type of package payload. 65 | #[arg(long, default_value = "zstd")] 66 | pub payload_compress: Compression, 67 | 68 | /// Timestamp in seconds since the UNIX Epoch for clamping 69 | /// modification time of included files and package build time. 70 | /// 71 | /// This value can also be provided using the SOURCE_DATE_EPOCH 72 | /// environment variable. 73 | #[arg(long)] 74 | pub source_date: Option, 75 | 76 | /// Overwrite metadata with TOML file. If "#dotted.key" 77 | /// suffixed, load "dotted.key" table instead of the root 78 | /// table. 79 | #[arg(long, value_delimiter = ',')] 80 | pub metadata_overwrite: Vec, 81 | 82 | /// Overwrite metadata with TOML text. 83 | #[arg(short, long)] 84 | pub set_metadata: Vec, 85 | 86 | /// Shortcut to --metadata-overwrite=path/to/Cargo.toml#package.metadata.generate-rpm.variants.VARIANT 87 | #[arg(long, value_delimiter = ',')] 88 | pub variant: Vec, 89 | } 90 | 91 | impl Cli { 92 | #[inline] 93 | fn get_matches_and_try_parse_from( 94 | args_fn: F, 95 | ) -> Result<(Self, ArgMatches), clap::Error> 96 | where 97 | F: Fn() -> I, 98 | I: IntoIterator + Iterator, 99 | T: Into + Clone, 100 | { 101 | let mut args = args_fn(); 102 | if args.nth(1) == Some(OsString::from("generate-rpm")) { 103 | let args = args_fn(); 104 | // This is the matches of the cargo command 105 | let matches = ::command().get_matches_from(args); 106 | let CargoWrapper::GenerateRpm(arg) = 107 | CargoWrapper::from_arg_matches_mut(&mut matches.clone())?; 108 | // matches are the args on the "cargo" call, generate-rpm is a subcommand 109 | // we need to get the subcommand arguments from matches and return those. 110 | // It's acceptable to unwrap here because we know that the subcommand is present based on the check above. 111 | Ok(( 112 | arg, 113 | matches 114 | .subcommand_matches("generate-rpm") 115 | .unwrap() 116 | .to_owned(), 117 | )) 118 | } else { 119 | let args = args_fn(); 120 | let matches = ::command().get_matches_from(args); 121 | let arg = Self::from_arg_matches_mut(&mut matches.clone())?; 122 | Ok((arg, matches)) 123 | } 124 | } 125 | 126 | pub fn get_matches_and_try_parse() -> Result<(Self, ArgMatches), clap::Error> { 127 | Self::get_matches_and_try_parse_from(std::env::args_os) 128 | } 129 | 130 | pub fn extra_metadata(&self, matches: &ArgMatches) -> Vec { 131 | let mut extra_metadata_args = Vec::new(); 132 | 133 | if let Some(indices) = matches.indices_of("metadata_overwrite") { 134 | for (v, i) in self.metadata_overwrite.iter().zip(indices) { 135 | let (file, branch) = match v.split_once('#') { 136 | None => (PathBuf::from(v), None), 137 | Some((file, branch)) => (PathBuf::from(file), Some(branch.to_string())), 138 | }; 139 | extra_metadata_args.push((i, ExtraMetadataSource::File(file, branch))); 140 | } 141 | } 142 | 143 | if let Some(indices) = matches.indices_of("set_metadata") { 144 | for (v, i) in self.set_metadata.iter().zip(indices) { 145 | extra_metadata_args.push((i, ExtraMetadataSource::Text(v.to_string()))); 146 | } 147 | } 148 | 149 | if let Some(indices) = matches.indices_of("variant") { 150 | for (v, i) in self.variant.iter().zip(indices) { 151 | extra_metadata_args.push((i, ExtraMetadataSource::Variant(v.to_string()))); 152 | } 153 | } 154 | 155 | extra_metadata_args.sort_by_key(|v| v.0); 156 | extra_metadata_args.drain(..).map(|v| v.1).collect() 157 | } 158 | } 159 | 160 | impl Default for Cli { 161 | fn default() -> Self { 162 | Cli::parse_from([""]) 163 | } 164 | } 165 | 166 | #[derive(ValueEnum, Clone, Copy, Debug, Default)] 167 | pub enum Compression { 168 | None, 169 | Gzip, 170 | #[default] 171 | Zstd, 172 | Xz, 173 | } 174 | 175 | impl From for rpm::CompressionWithLevel { 176 | fn from(val: Compression) -> Self { 177 | let ct = match val { 178 | Compression::None => rpm::CompressionType::None, 179 | Compression::Gzip => rpm::CompressionType::Gzip, 180 | Compression::Zstd => rpm::CompressionType::Zstd, 181 | Compression::Xz => rpm::CompressionType::Xz, 182 | }; 183 | ct.into() 184 | } 185 | } 186 | 187 | #[derive(Clone, Debug, PartialEq, Eq)] 188 | pub enum AutoReqMode { 189 | Auto, 190 | Disabled, 191 | Builtin, 192 | FindRequires, 193 | Script(PathBuf), 194 | } 195 | 196 | impl ValueParserFactory for AutoReqMode { 197 | type Parser = AutoReqModeParser; 198 | 199 | fn value_parser() -> Self::Parser { 200 | AutoReqModeParser 201 | } 202 | } 203 | 204 | #[derive(Clone, Debug)] 205 | pub struct AutoReqModeParser; 206 | 207 | impl TypedValueParser for AutoReqModeParser { 208 | type Value = AutoReqMode; 209 | fn parse_ref( 210 | &self, 211 | cmd: &Command, 212 | arg: Option<&Arg>, 213 | value: &OsStr, 214 | ) -> Result { 215 | const VALUES: [(&str, AutoReqMode); 5] = [ 216 | ("auto", AutoReqMode::Auto), 217 | ("disabled", AutoReqMode::Disabled), 218 | ("no", AutoReqMode::Disabled), 219 | ("builtin", AutoReqMode::Builtin), 220 | ("find-requires", AutoReqMode::FindRequires), 221 | ]; 222 | 223 | let inner = PossibleValuesParser::new(VALUES.iter().map(|(k, _v)| k)); 224 | match inner.parse_ref(cmd, arg, value) { 225 | Ok(name) => Ok(VALUES.iter().find(|(k, _v)| name.eq(k)).unwrap().1.clone()), 226 | Err(e) if e.kind() == clap::error::ErrorKind::InvalidValue => { 227 | let inner = PathBufValueParser::new(); 228 | match inner.parse_ref(cmd, arg, value) { 229 | Ok(v) => Ok(AutoReqMode::Script(v)), 230 | Err(e) => Err(e), 231 | } 232 | } 233 | Err(e) => Err(e), 234 | } 235 | } 236 | } 237 | 238 | #[derive(Clone, Debug, PartialEq, Eq)] 239 | pub enum ExtraMetadataSource { 240 | File(PathBuf, Option), 241 | Text(String), 242 | Variant(String), 243 | } 244 | 245 | #[cfg(test)] 246 | mod tests { 247 | use super::*; 248 | #[test] 249 | fn verify_cli() { 250 | ::command().debug_assert() 251 | } 252 | 253 | #[test] 254 | fn verify_cargo_wrapper() { 255 | ::command().debug_assert() 256 | } 257 | 258 | #[test] 259 | fn test_get_matches_and_try_parse_from() { 260 | let (args, matcher) = Cli::get_matches_and_try_parse_from(|| { 261 | ["", "-o", "/dev/null"].map(&OsString::from).into_iter() 262 | }) 263 | .unwrap(); 264 | assert_eq!(args.output, Some(PathBuf::from("/dev/null"))); 265 | assert_eq!( 266 | matcher.indices_of("output").unwrap().collect::>(), 267 | &[2] 268 | ); 269 | 270 | // Simulate being called from Cargo 271 | let (args, matcher) = Cli::get_matches_and_try_parse_from(|| { 272 | [ 273 | "cargo", 274 | "generate-rpm", 275 | "-o", 276 | "/dev/null", 277 | "-s", 278 | "release=1.foo", 279 | ] 280 | .map(&OsString::from) 281 | .into_iter() 282 | }) 283 | .unwrap(); 284 | assert_eq!(args.output, Some(PathBuf::from("/dev/null"))); 285 | assert_eq!( 286 | matcher.indices_of("output").unwrap().collect::>(), 287 | &[2] 288 | ); 289 | } 290 | 291 | #[test] 292 | fn test_metadata_overwrite() { 293 | let args = Cli::try_parse_from([ 294 | "", 295 | "--metadata-overwrite", 296 | "TOML_FILE.toml", 297 | "--metadata-overwrite", 298 | "TOML_FILE.toml#TOML.PATH", 299 | ]) 300 | .unwrap(); 301 | assert_eq!( 302 | args.metadata_overwrite, 303 | vec!["TOML_FILE.toml", "TOML_FILE.toml#TOML.PATH"] 304 | ); 305 | } 306 | 307 | #[test] 308 | fn test_set_metadata() { 309 | let args = Cli::try_parse_from([ 310 | "", 311 | "-s", 312 | "toml \"text1\"", 313 | "--set-metadata", 314 | "toml \"text2\"", 315 | ]) 316 | .unwrap(); 317 | assert_eq!(args.set_metadata, vec!["toml \"text1\"", "toml \"text2\""]); 318 | } 319 | 320 | #[test] 321 | fn test_extrametadata() { 322 | let (args, matches) = Cli::get_matches_and_try_parse_from(|| { 323 | [ 324 | "", 325 | "--metadata-overwrite", 326 | "TOML_FILE1.toml", 327 | "-s", 328 | "toml \"text1\"", 329 | "--metadata-overwrite", 330 | "TOML_FILE2.toml#TOML.PATH", 331 | "--variant", 332 | "VARIANT1,VARIANT2", 333 | "--set-metadata", 334 | "toml \"text2\"", 335 | "--metadata-overwrite", 336 | "TOML_FILE3.toml#TOML.PATH,TOML_FILE4.toml", 337 | ] 338 | .map(&OsString::from) 339 | .into_iter() 340 | }) 341 | .unwrap(); 342 | 343 | let metadata = args.extra_metadata(&matches); 344 | assert_eq!( 345 | metadata, 346 | vec![ 347 | ExtraMetadataSource::File(PathBuf::from("TOML_FILE1.toml"), None), 348 | ExtraMetadataSource::Text(String::from("toml \"text1\"")), 349 | ExtraMetadataSource::File( 350 | PathBuf::from("TOML_FILE2.toml"), 351 | Some(String::from("TOML.PATH")) 352 | ), 353 | ExtraMetadataSource::Variant(String::from("VARIANT1")), 354 | ExtraMetadataSource::Variant(String::from("VARIANT2")), 355 | ExtraMetadataSource::Text(String::from("toml \"text2\"")), 356 | ExtraMetadataSource::File( 357 | PathBuf::from("TOML_FILE3.toml"), 358 | Some(String::from("TOML.PATH")) 359 | ), 360 | ExtraMetadataSource::File(PathBuf::from("TOML_FILE4.toml"), None), 361 | ] 362 | ); 363 | } 364 | 365 | #[test] 366 | fn test_auto_req() { 367 | let args = Cli::try_parse_from([""]).unwrap(); 368 | assert_eq!(args.auto_req, AutoReqMode::Auto); 369 | let args = Cli::try_parse_from(["", "--auto-req", "auto"]).unwrap(); 370 | assert_eq!(args.auto_req, AutoReqMode::Auto); 371 | let args = Cli::try_parse_from(["", "--auto-req", "builtin"]).unwrap(); 372 | assert_eq!(args.auto_req, AutoReqMode::Builtin); 373 | let args = Cli::try_parse_from(["", "--auto-req", "find-requires"]).unwrap(); 374 | assert_eq!(args.auto_req, AutoReqMode::FindRequires); 375 | let args = Cli::try_parse_from(["", "--auto-req", "/usr/lib/rpm/find-requires"]).unwrap(); 376 | assert!( 377 | matches!(args.auto_req, AutoReqMode::Script(v) if v == PathBuf::from("/usr/lib/rpm/find-requires")) 378 | ); 379 | let args = Cli::try_parse_from(["", "--auto-req", "no"]).unwrap(); 380 | assert_eq!(args.auto_req, AutoReqMode::Disabled); 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /src/config/file_info.rs: -------------------------------------------------------------------------------- 1 | use glob::glob; 2 | use toml::value::Table; 3 | 4 | use crate::build_target::BuildTarget; 5 | use crate::error::ConfigError; 6 | use std::path::{Path, PathBuf}; 7 | use toml::Value; 8 | 9 | #[derive(Debug, Eq, PartialEq, Clone)] 10 | pub struct FileInfo<'a, 'b, 'c, 'd, 'e> { 11 | pub source: &'a str, 12 | pub dest: &'b str, 13 | pub user: Option<&'c str>, 14 | pub group: Option<&'d str>, 15 | pub mode: Option, 16 | pub config: bool, 17 | pub config_noreplace: bool, 18 | pub doc: bool, 19 | pub caps: Option<&'e str>, 20 | } 21 | 22 | impl FileInfo<'_, '_, '_, '_, '_> { 23 | pub fn new(assets: &[Value]) -> Result, ConfigError> { 24 | let mut files = Vec::with_capacity(assets.len()); 25 | for (idx, value) in assets.iter().enumerate() { 26 | let table = value 27 | .as_table() 28 | .ok_or(ConfigError::AssetFileUndefined(idx, "source"))?; 29 | let source = table 30 | .get("source") 31 | .ok_or(ConfigError::AssetFileUndefined(idx, "source"))? 32 | .as_str() 33 | .ok_or(ConfigError::AssetFileWrongType(idx, "source", "string"))?; 34 | let dest = table 35 | .get("dest") 36 | .ok_or(ConfigError::AssetFileUndefined(idx, "dest"))? 37 | .as_str() 38 | .ok_or(ConfigError::AssetFileWrongType(idx, "dest", "string"))?; 39 | 40 | let user = if let Some(user) = table.get("user") { 41 | Some( 42 | user.as_str() 43 | .ok_or(ConfigError::AssetFileWrongType(idx, "user", "string"))?, 44 | ) 45 | } else { 46 | None 47 | }; 48 | let group = if let Some(group) = table.get("group") { 49 | Some( 50 | group 51 | .as_str() 52 | .ok_or(ConfigError::AssetFileWrongType(idx, "group", "string"))?, 53 | ) 54 | } else { 55 | None 56 | }; 57 | let mode = Self::get_mode(table, source, idx)?; 58 | let caps = if let Some(caps) = table.get("caps") { 59 | Some( 60 | caps.as_str() 61 | .ok_or(ConfigError::AssetFileWrongType(idx, "caps", "string"))?, 62 | ) 63 | } else { 64 | None 65 | }; 66 | let (config, config_noreplace, _config_missingok) = match table.get("config") { 67 | Some(Value::Boolean(v)) => (*v, false, false), 68 | Some(Value::String(v)) if v.eq("noreplace") => (false, true, false), 69 | //Some(Value::String(v)) if v.eq("missingok") => (false, false, true), 70 | None => (false, false, false), 71 | _ => { 72 | return Err(ConfigError::AssetFileWrongType( 73 | idx, 74 | "config", 75 | "bool or \"noreplace\"", 76 | )) 77 | } //_ => return Err(ConfigError::AssetFileWrongType(idx, "config", "bool or \"noreplace\" or \"missingok\"")), 78 | }; 79 | let doc = if let Some(is_doc) = table.get("doc") { 80 | is_doc 81 | .as_bool() 82 | .ok_or(ConfigError::AssetFileWrongType(idx, "doc", "bool"))? 83 | } else { 84 | false 85 | }; 86 | 87 | files.push(FileInfo { 88 | source, 89 | dest, 90 | user, 91 | group, 92 | mode, 93 | config, 94 | config_noreplace, 95 | doc, 96 | caps, 97 | }); 98 | } 99 | Ok(files) 100 | } 101 | 102 | fn get_mode(table: &Table, source: &str, idx: usize) -> Result, ConfigError> { 103 | if let Some(mode) = table.get("mode") { 104 | let mode = mode 105 | .as_str() 106 | .ok_or(ConfigError::AssetFileWrongType(idx, "mode", "string"))?; 107 | let mode = usize::from_str_radix(mode, 8) 108 | .map_err(|_| ConfigError::AssetFileWrongType(idx, "mode", "oct-string"))?; 109 | let file_mode = if mode & 0o170000 != 0 { 110 | None 111 | } else if source.ends_with('/') { 112 | Some(0o040000) // S_IFDIR 113 | } else { 114 | Some(0o100000) // S_IFREG 115 | }; 116 | Ok(Some(file_mode.unwrap_or_default() | mode)) 117 | } else { 118 | Ok(None) 119 | } 120 | } 121 | 122 | fn generate_expanded_path>( 123 | &self, 124 | build_target: &BuildTarget, 125 | parent: P, 126 | idx: usize, 127 | ) -> Result, ConfigError> { 128 | let source = get_asset_rel_path(self.source, build_target); 129 | 130 | let expanded = expand_glob(source.as_str(), self.dest, idx)?; 131 | if !expanded.is_empty() { 132 | return Ok(expanded); 133 | } 134 | 135 | if let Some(src) = parent.as_ref().join(&source).to_str() { 136 | let expanded = expand_glob(src, self.dest, idx)?; 137 | if !expanded.is_empty() { 138 | return Ok(expanded); 139 | } 140 | } 141 | 142 | Err(ConfigError::AssetFileNotFound(PathBuf::from(source))) 143 | } 144 | 145 | fn generate_rpm_file_options( 146 | &self, 147 | dest: T, 148 | idx: usize, 149 | ) -> Result { 150 | let mut rpm_file_option = rpm::FileOptions::new(dest.to_string()); 151 | if let Some(user) = self.user { 152 | rpm_file_option = rpm_file_option.user(user); 153 | } 154 | if let Some(group) = self.group { 155 | rpm_file_option = rpm_file_option.group(group); 156 | } 157 | if let Some(mode) = self.mode { 158 | rpm_file_option = rpm_file_option.mode(mode as i32); 159 | } 160 | if self.config { 161 | rpm_file_option = rpm_file_option.is_config(); 162 | } 163 | if self.config_noreplace { 164 | rpm_file_option = rpm_file_option.is_config_noreplace(); 165 | } 166 | if self.doc { 167 | rpm_file_option = rpm_file_option.is_doc(); 168 | } 169 | if let Some(caps) = self.caps { 170 | rpm_file_option = rpm_file_option 171 | .caps(caps) 172 | .map_err(|err| ConfigError::AssetFileRpm(idx, "caps", err.into()))?; 173 | } 174 | Ok(rpm_file_option.into()) 175 | } 176 | 177 | pub(crate) fn generate_rpm_file_entry>( 178 | &self, 179 | build_target: &BuildTarget, 180 | parent: P, 181 | idx: usize, 182 | ) -> Result, ConfigError> { 183 | self.generate_expanded_path(build_target, parent, idx)? 184 | .iter() 185 | .map(|(src, dst)| { 186 | self.generate_rpm_file_options(dst, idx) 187 | .map(|v| (src.clone(), v)) 188 | }) 189 | .collect::, _>>() 190 | } 191 | } 192 | 193 | fn get_base_from_glob(glob: &'_ str) -> PathBuf { 194 | let base = match glob.split_once('*') { 195 | Some((before, _)) => before, 196 | None => glob, 197 | }; 198 | 199 | let base_path = Path::new(base); 200 | let out_path = if base_path.is_dir() { 201 | base_path 202 | } else if let Some(parent) = base_path.parent() { 203 | parent 204 | } else { 205 | base_path 206 | }; 207 | 208 | out_path.into() 209 | } 210 | 211 | pub(crate) fn get_asset_rel_path(asset: &str, build_target: &BuildTarget) -> String { 212 | let dir_name = match build_target.profile() { 213 | "dev" => "debug", 214 | p => p, 215 | }; 216 | asset 217 | .strip_prefix("target/release/") 218 | .or_else(|| asset.strip_prefix(&format!("target/{dir_name}/"))) 219 | .and_then(|rel_path| { 220 | build_target 221 | .target_path(dir_name) 222 | .join(rel_path) 223 | .to_str() 224 | .map(|v| v.to_string()) 225 | }) 226 | .unwrap_or(asset.to_string()) 227 | } 228 | 229 | fn expand_glob( 230 | source: &str, 231 | dest: &str, 232 | idx: usize, 233 | ) -> Result, ConfigError> { 234 | let mut vec = Vec::new(); 235 | if source.contains('*') { 236 | let base = get_base_from_glob(source); 237 | for path in glob(source).map_err(|e| ConfigError::AssetGlobInvalid(idx, e.msg))? { 238 | let file = path.map_err(|_| ConfigError::AssetReadFailed(idx))?; 239 | if file.is_dir() { 240 | continue; 241 | } 242 | let rel_path = file.strip_prefix(&base).map_err(|_| { 243 | ConfigError::AssetGlobPathInvalid( 244 | idx, 245 | file.to_str().unwrap().to_owned(), 246 | base.to_str().unwrap().to_owned(), 247 | ) 248 | })?; 249 | let dest_path = Path::new(&dest).join(rel_path); 250 | let dst = dest_path.to_str().unwrap().to_owned(); 251 | 252 | vec.push((file, dst)); 253 | } 254 | } else if Path::new(source).exists() { 255 | let file = PathBuf::from(source); 256 | let dst = match file.file_name().map(|v| v.to_str()) { 257 | Some(Some(filename)) if dest.ends_with('/') => dest.to_string() + filename, 258 | _ => dest.to_string(), 259 | }; 260 | 261 | vec.push((file, dst)); 262 | } 263 | 264 | Ok(vec) 265 | } 266 | 267 | #[cfg(test)] 268 | mod test { 269 | use super::*; 270 | use cargo_toml::Manifest; 271 | use std::fs::File; 272 | 273 | #[test] 274 | fn test_get_base_from_glob() { 275 | let toml_dir = "../".to_string() 276 | + std::env::current_dir() 277 | .unwrap() 278 | .file_name() 279 | .unwrap() 280 | .to_str() 281 | .unwrap(); 282 | let toml_ptn = toml_dir.to_string() + "/*.toml"; 283 | 284 | let tests = &[ 285 | ("*", PathBuf::from("")), 286 | ("src/auto_req/*.rs", PathBuf::from("src/auto_req")), 287 | ("src/not_a_directory/*.rs", PathBuf::from("src")), 288 | ("*.things", PathBuf::from("")), 289 | (toml_ptn.as_str(), PathBuf::from(toml_dir)), 290 | ("src/auto_req", PathBuf::from("src/auto_req")), // shouldn't currently happen as we detect '*' in the string, but test the code path anyway 291 | ]; 292 | 293 | for test in tests { 294 | let out = get_base_from_glob(test.0); 295 | assert_eq!( 296 | out, test.1, 297 | "get_base_from_glob({0:?}) shall equal to {1:?}", 298 | test.0, test.1 299 | ); 300 | } 301 | } 302 | 303 | #[test] 304 | fn test_new() { 305 | let manifest = Manifest::from_path("./Cargo.toml").unwrap(); 306 | let metadata = manifest.package.unwrap().metadata.unwrap(); 307 | let metadata = metadata 308 | .as_table() 309 | .unwrap() 310 | .get("generate-rpm") 311 | .unwrap() 312 | .as_table() 313 | .unwrap(); 314 | let assets = metadata.get("assets").and_then(|v| v.as_array()).unwrap(); 315 | let files = FileInfo::new(assets.as_slice()).unwrap(); 316 | assert_eq!( 317 | files, 318 | vec![ 319 | FileInfo { 320 | source: "target/release/cargo-generate-rpm", 321 | dest: "/usr/bin/cargo-generate-rpm", 322 | user: None, 323 | group: None, 324 | mode: Some(0o0100755), 325 | config: false, 326 | config_noreplace: false, 327 | doc: false, 328 | caps: None, 329 | }, 330 | FileInfo { 331 | source: "LICENSE", 332 | dest: "/usr/share/doc/cargo-generate-rpm/LICENSE", 333 | user: None, 334 | group: None, 335 | mode: Some(0o0100644), 336 | config: false, 337 | config_noreplace: false, 338 | doc: true, 339 | caps: None, 340 | }, 341 | FileInfo { 342 | source: "README.md", 343 | dest: "/usr/share/doc/cargo-generate-rpm/README.md", 344 | user: None, 345 | group: None, 346 | mode: Some(0o0100644), 347 | config: false, 348 | config_noreplace: false, 349 | doc: true, 350 | caps: None, 351 | }, 352 | ] 353 | ); 354 | } 355 | 356 | #[test] 357 | fn test_generate_rpm_file_path() { 358 | let tempdir = tempfile::tempdir().unwrap(); 359 | let args = crate::cli::Cli::default(); 360 | let target = BuildTarget::new(&args); 361 | let file_info = FileInfo { 362 | source: "README.md", 363 | dest: "/usr/share/doc/cargo-generate-rpm/README.md", 364 | user: None, 365 | group: None, 366 | mode: None, 367 | config: false, 368 | config_noreplace: false, 369 | doc: true, 370 | caps: Some("cap_sys_admin=pe"), 371 | }; 372 | let expanded = file_info 373 | .generate_expanded_path(&target, &tempdir, 0) 374 | .unwrap(); 375 | assert_eq!( 376 | expanded 377 | .iter() 378 | .map(|(src, dst)| { (src.as_path().to_str(), dst) }) 379 | .collect::>(), 380 | vec![(Some(file_info.source), &file_info.dest.to_string())] 381 | ); 382 | 383 | let file_info = FileInfo { 384 | source: "not-exist-file", 385 | dest: "/usr/share/doc/cargo-generate-rpm/not-exist-file", 386 | user: None, 387 | group: None, 388 | mode: None, 389 | config: false, 390 | config_noreplace: false, 391 | doc: true, 392 | caps: None, 393 | }; 394 | assert!( 395 | matches!(file_info.generate_expanded_path(&target, &tempdir, 0), 396 | Err(ConfigError::AssetFileNotFound(v)) if v == PathBuf::from( "not-exist-file")) 397 | ); 398 | 399 | std::fs::create_dir_all(tempdir.path().join("target/release")).unwrap(); 400 | File::create(tempdir.path().join("target/release/foobar")).unwrap(); 401 | let file_info = FileInfo { 402 | source: "target/release/foobar", 403 | dest: "/usr/bin/foobar", 404 | user: None, 405 | group: None, 406 | mode: None, 407 | config: false, 408 | config_noreplace: false, 409 | doc: false, 410 | caps: None, 411 | }; 412 | let expanded = file_info 413 | .generate_expanded_path(&target, &tempdir, 0) 414 | .unwrap(); 415 | assert_eq!( 416 | expanded 417 | .iter() 418 | .map(|(src, dst)| { (src.as_path().to_str(), dst) }) 419 | .collect::>(), 420 | vec![( 421 | Some( 422 | tempdir 423 | .path() 424 | .join("target/release/foobar") 425 | .to_str() 426 | .unwrap() 427 | ), 428 | &file_info.dest.to_string() 429 | )] 430 | ); 431 | 432 | let args = crate::cli::Cli { 433 | target_dir: Some( 434 | tempdir 435 | .path() 436 | .join("target") 437 | .as_os_str() 438 | .to_str() 439 | .unwrap() 440 | .to_string(), 441 | ), 442 | ..Default::default() 443 | }; 444 | let target = BuildTarget::new(&args); 445 | let expanded = file_info 446 | .generate_expanded_path(&target, &tempdir, 0) 447 | .unwrap(); 448 | assert_eq!( 449 | expanded 450 | .iter() 451 | .map(|(src, dst)| { (src.as_path().to_str(), dst) }) 452 | .collect::>(), 453 | vec![( 454 | Some( 455 | tempdir 456 | .path() 457 | .join("target/release/foobar") 458 | .to_str() 459 | .unwrap() 460 | ), 461 | &file_info.dest.to_string() 462 | )] 463 | ); 464 | 465 | std::fs::create_dir_all(tempdir.path().join("target/target-triple/my-profile")).unwrap(); 466 | File::create( 467 | tempdir 468 | .path() 469 | .join("target/target-triple/my-profile/my-bin"), 470 | ) 471 | .unwrap(); 472 | let file_info = FileInfo { 473 | source: "target/release/my-bin", 474 | dest: "/usr/bin/my-bin", 475 | user: None, 476 | group: None, 477 | mode: None, 478 | config: false, 479 | config_noreplace: false, 480 | doc: false, 481 | caps: None, 482 | }; 483 | let args = crate::cli::Cli { 484 | target_dir: Some( 485 | tempdir 486 | .path() 487 | .join("target") 488 | .as_os_str() 489 | .to_str() 490 | .unwrap() 491 | .to_string(), 492 | ), 493 | target: Some("target-triple".to_string()), 494 | profile: "my-profile".to_string(), 495 | ..Default::default() 496 | }; 497 | let target = BuildTarget::new(&args); 498 | let expanded = file_info 499 | .generate_expanded_path(&target, &tempdir, 0) 500 | .unwrap(); 501 | assert_eq!( 502 | expanded 503 | .iter() 504 | .map(|(src, dst)| { (src.as_path().to_str(), dst) }) 505 | .collect::>(), 506 | vec![( 507 | Some( 508 | tempdir 509 | .path() 510 | .join("target/target-triple/my-profile/my-bin") 511 | .to_str() 512 | .unwrap() 513 | ), 514 | &file_info.dest.to_string() 515 | )] 516 | ); 517 | } 518 | 519 | #[test] 520 | fn test_expand_glob() { 521 | assert_eq!( 522 | expand_glob("*.md", "/usr/share/doc/cargo-generate-rpm/", 0).unwrap(), 523 | vec![( 524 | PathBuf::from("README.md"), 525 | "/usr/share/doc/cargo-generate-rpm/README.md".into() 526 | )] 527 | ); 528 | 529 | assert_eq!( 530 | expand_glob("*-not-exist-glob", "/usr/share/doc/cargo-generate-rpm/", 0).unwrap(), 531 | vec![] 532 | ); 533 | 534 | assert_eq!( 535 | expand_glob( 536 | "README.md", 537 | "/usr/share/doc/cargo-generate-rpm/README.md", 538 | 2 539 | ) 540 | .unwrap(), 541 | vec![( 542 | PathBuf::from("README.md"), 543 | "/usr/share/doc/cargo-generate-rpm/README.md".into() 544 | )] 545 | ); 546 | 547 | assert_eq!( 548 | expand_glob( 549 | "README.md", 550 | "/usr/share/doc/cargo-generate-rpm/", // specifying directory 551 | 0 552 | ) 553 | .unwrap(), 554 | vec![( 555 | PathBuf::from("README.md"), 556 | "/usr/share/doc/cargo-generate-rpm/README.md".into() 557 | )] 558 | ); 559 | } 560 | } 561 | -------------------------------------------------------------------------------- /src/config/metadata.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::ExtraMetadataSource; 2 | use crate::error::{ConfigError, FileAnnotatedError}; 3 | use crate::Error; 4 | use cargo_toml::Manifest; 5 | use rpm::Scriptlet; 6 | use std::fs; 7 | use std::path::PathBuf; 8 | use toml::value::Table; 9 | use toml::Value; 10 | 11 | mod toml_dotted_bare_key_parser { 12 | use crate::error::DottedBareKeyLexError; 13 | 14 | pub(super) fn parse_dotted_bare_keys(input: &str) -> Result, DottedBareKeyLexError> { 15 | let mut keys = Vec::new(); 16 | 17 | let mut pos = 0; 18 | while pos < input.len() { 19 | if let Some(key_end) = input 20 | .bytes() 21 | .enumerate() 22 | .skip(pos) 23 | .take_while(|(_, b)| { 24 | // a bare key may contain a-zA-Z0-9_- 25 | b.is_ascii_alphanumeric() || *b == b'_' || *b == b'-' 26 | }) 27 | .last() 28 | .map(|(i, _)| i) 29 | { 30 | keys.push(&input[pos..=key_end]); 31 | if key_end == input.len() - 1 { 32 | break; 33 | } else { 34 | pos = key_end + 1; 35 | } 36 | } else { 37 | return Err(match input.as_bytes()[pos] { 38 | v @ (b'\"' | b'\'') => DottedBareKeyLexError::QuotedKey(v as char), 39 | b'.' => DottedBareKeyLexError::InvalidDotChar, 40 | v => DottedBareKeyLexError::InvalidChar(v as char), 41 | }); 42 | } 43 | 44 | match input.as_bytes()[pos] { 45 | b'.' => { 46 | if pos == input.len() - 1 { 47 | return Err(DottedBareKeyLexError::InvalidDotChar); 48 | } else { 49 | // keys.push(Token::Dot); 50 | pos += 1; 51 | } 52 | } 53 | v => return Err(DottedBareKeyLexError::InvalidChar(v as char)), 54 | } 55 | } 56 | 57 | Ok(keys) 58 | } 59 | 60 | #[test] 61 | fn test_parse_dotted_bare_keys() { 62 | assert_eq!(parse_dotted_bare_keys("name"), Ok(vec!["name"])); 63 | assert_eq!( 64 | parse_dotted_bare_keys("physical.color"), 65 | Ok(vec!["physical", "color"]) 66 | ); 67 | assert_eq!( 68 | parse_dotted_bare_keys("physical.color"), 69 | Ok(vec!["physical", "color"]) 70 | ); 71 | assert_eq!( 72 | parse_dotted_bare_keys("invalid..joined"), 73 | Err(DottedBareKeyLexError::InvalidDotChar) 74 | ); 75 | assert_eq!( 76 | parse_dotted_bare_keys(".invalid.joined"), 77 | Err(DottedBareKeyLexError::InvalidDotChar) 78 | ); 79 | assert_eq!( 80 | parse_dotted_bare_keys("invalid.joined."), 81 | Err(DottedBareKeyLexError::InvalidDotChar) 82 | ); 83 | assert_eq!( 84 | parse_dotted_bare_keys("error.\"quoted key\""), 85 | Err(DottedBareKeyLexError::QuotedKey('\"')) 86 | ); 87 | assert_eq!( 88 | parse_dotted_bare_keys("error.\'quoted key\'"), 89 | Err(DottedBareKeyLexError::QuotedKey('\'')) 90 | ); 91 | assert_eq!( 92 | parse_dotted_bare_keys("a-zA-Z0-9-_.*invalid*"), 93 | Err(DottedBareKeyLexError::InvalidChar('*')) 94 | ); 95 | assert_eq!( 96 | parse_dotted_bare_keys("a. b .c"), 97 | Err(DottedBareKeyLexError::InvalidChar(' ')) 98 | ); 99 | } 100 | } 101 | 102 | pub(crate) trait TomlValueHelper<'a> { 103 | fn get_str(&self, name: &str) -> Result, ConfigError>; 104 | fn get_i64(&self, name: &str) -> Result, ConfigError>; 105 | fn get_string_or_i64(&self, name: &str) -> Result, ConfigError>; 106 | fn get_bool(&self, name: &str) -> Result, ConfigError>; 107 | fn get_table(&self, name: &str) -> Result, ConfigError>; 108 | fn get_array(&self, name: &str) -> Result, ConfigError>; 109 | } 110 | 111 | #[derive(Debug)] 112 | pub(super) struct ExtraMetaData(Table, ExtraMetadataSource); 113 | 114 | impl ExtraMetaData { 115 | pub(super) fn new( 116 | source: &ExtraMetadataSource, 117 | package_manifest: &PathBuf, 118 | ) -> Result { 119 | match source { 120 | ExtraMetadataSource::File(p, branch) => { 121 | let annot: Option = Some(p.clone()); 122 | let toml = fs::read_to_string(p)? 123 | .parse::() 124 | .map_err(|e| FileAnnotatedError(annot.clone(), e))?; 125 | let table = Self::convert_toml_txt_to_table(&toml, branch) 126 | .map_err(|e| FileAnnotatedError(annot, e))?; 127 | Ok(Self(table.clone(), source.clone())) 128 | } 129 | ExtraMetadataSource::Text(text) => { 130 | let annot: Option = None; 131 | let toml = text 132 | .parse::() 133 | .map_err(|e| FileAnnotatedError(annot.clone(), e))?; 134 | let table = Self::convert_toml_txt_to_table(&toml, &None as &_) 135 | .map_err(|e| FileAnnotatedError(annot, e))?; 136 | Ok(Self(table.clone(), source.clone())) 137 | } 138 | ExtraMetadataSource::Variant(variant) => { 139 | let annot: Option = Some(package_manifest.clone()); 140 | let toml = fs::read_to_string(package_manifest)? 141 | .parse::() 142 | .map_err(|e| FileAnnotatedError(annot.clone(), e))?; 143 | let branch = format!("package.metadata.generate-rpm.variants.{variant}"); 144 | let table = Self::convert_toml_txt_to_table(&toml, &Some(branch)) 145 | .map_err(|e| FileAnnotatedError(annot, e))?; 146 | Ok(Self(table.clone(), source.clone())) 147 | } 148 | } 149 | } 150 | 151 | fn convert_toml_txt_to_table<'a>( 152 | toml: &'a Value, 153 | branch: &Option, 154 | ) -> Result<&'a Table, ConfigError> { 155 | let root = toml 156 | .as_table() 157 | .ok_or(ConfigError::WrongType(".".to_string(), "table"))?; 158 | 159 | if let Some(branch) = branch { 160 | toml_dotted_bare_key_parser::parse_dotted_bare_keys(branch.as_ref()) 161 | .map_err(|e| ConfigError::WrongBranchPathOfToml(branch.clone(), e))? 162 | .iter() 163 | .try_fold(root, |table, key| { 164 | table.get(*key).and_then(|v| v.as_table()) 165 | }) 166 | .ok_or(ConfigError::BranchPathNotFoundInToml(branch.to_string())) 167 | } else { 168 | Ok(root) 169 | } 170 | } 171 | } 172 | 173 | pub(super) struct MetadataConfig<'a> { 174 | metadata: &'a Table, 175 | branch_path: Option, 176 | } 177 | 178 | impl<'a> MetadataConfig<'a> { 179 | pub fn new(metadata: &'a Table, branch_path: Option) -> Self { 180 | Self { 181 | metadata, 182 | branch_path, 183 | } 184 | } 185 | 186 | pub fn new_from_extra_metadata(extra_metadata: &'a ExtraMetaData) -> Self { 187 | Self::new( 188 | &extra_metadata.0, 189 | match &extra_metadata.1 { 190 | ExtraMetadataSource::File(_, Some(branch)) => Some(branch.clone()), 191 | _ => None, 192 | }, 193 | ) 194 | } 195 | 196 | pub fn new_from_manifest(manifest: &'a Manifest) -> Result { 197 | let pkg = manifest 198 | .package 199 | .as_ref() 200 | .ok_or(ConfigError::Missing("package".to_string()))?; 201 | let metadata = pkg 202 | .metadata 203 | .as_ref() 204 | .ok_or(ConfigError::Missing("package.metadata".to_string()))? 205 | .as_table() 206 | .ok_or(ConfigError::WrongType( 207 | "package.metadata".to_string(), 208 | "table", 209 | ))?; 210 | let metadata = metadata 211 | .iter() 212 | .find(|(name, _)| name.as_str() == "generate-rpm") 213 | .ok_or(ConfigError::Missing( 214 | "package.metadata.generate-rpm".to_string(), 215 | ))? 216 | .1 217 | .as_table() 218 | .ok_or(ConfigError::WrongType( 219 | "package.metadata.generate-rpm".to_string(), 220 | "table", 221 | ))?; 222 | 223 | Ok(Self { 224 | metadata, 225 | branch_path: Some("package.metadata.generate-rpm".to_string()), 226 | }) 227 | } 228 | 229 | fn create_config_error(&self, name: &str, type_name: &'static str) -> ConfigError { 230 | let toml_path = self 231 | .branch_path 232 | .as_ref() 233 | .map(|v| [v, name].join(".")) 234 | .unwrap_or(name.to_string()); 235 | ConfigError::WrongType(toml_path, type_name) 236 | } 237 | } 238 | 239 | impl<'a> TomlValueHelper<'a> for MetadataConfig<'a> { 240 | fn get_str(&self, name: &str) -> Result, ConfigError> { 241 | self.metadata 242 | .get(name) 243 | .map(|val| match val { 244 | Value::String(v) => Ok(Some(v.as_str())), 245 | _ => Err(self.create_config_error(name, "string")), 246 | }) 247 | .unwrap_or(Ok(None)) 248 | } 249 | 250 | fn get_i64(&self, name: &str) -> Result, ConfigError> { 251 | self.metadata 252 | .get(name) 253 | .map(|val| match val { 254 | Value::Integer(v) => Ok(Some(*v)), 255 | _ => Err(self.create_config_error(name, "integer")), 256 | }) 257 | .unwrap_or(Ok(None)) 258 | } 259 | 260 | fn get_string_or_i64(&self, name: &str) -> Result, ConfigError> { 261 | self.metadata 262 | .get(name) 263 | .map(|val| match val { 264 | Value::String(v) => Ok(Some(v.clone())), 265 | Value::Integer(v) => Ok(Some(v.to_string())), 266 | _ => Err(self.create_config_error(name, "string or integer")), 267 | }) 268 | .unwrap_or(Ok(None)) 269 | } 270 | 271 | fn get_bool(&self, name: &str) -> Result, ConfigError> { 272 | self.metadata 273 | .get(name) 274 | .map(|val| match val { 275 | Value::Boolean(v) => Ok(Some(*v)), 276 | _ => Err(self.create_config_error(name, "bool")), 277 | }) 278 | .unwrap_or(Ok(None)) 279 | } 280 | 281 | fn get_table(&self, name: &str) -> Result, ConfigError> { 282 | self.metadata 283 | .get(name) 284 | .map(|val| match val { 285 | Value::Table(v) => Ok(Some(v)), 286 | _ => Err(self.create_config_error(name, "string or integer")), 287 | }) 288 | .unwrap_or(Ok(None)) 289 | } 290 | 291 | fn get_array(&self, name: &str) -> Result, ConfigError> { 292 | self.metadata 293 | .get(name) 294 | .map(|val| match val { 295 | Value::Array(v) => Ok(Some(v.as_slice())), 296 | _ => Err(self.create_config_error(name, "array")), 297 | }) 298 | .unwrap_or(Ok(None)) 299 | } 300 | } 301 | 302 | pub(super) struct CompoundMetadataConfig<'a> { 303 | config: &'a [MetadataConfig<'a>], 304 | } 305 | 306 | impl<'a> CompoundMetadataConfig<'a> { 307 | pub(super) fn new(config: &'a [MetadataConfig<'a>]) -> Self { 308 | Self { config } 309 | } 310 | 311 | fn get(&self, func: F) -> Result, ConfigError> 312 | where 313 | F: Fn(&MetadataConfig<'a>) -> Result, ConfigError>, 314 | { 315 | for item in self.config.iter().rev() { 316 | match func(item) { 317 | v @ (Ok(Some(_)) | Err(_)) => return v, 318 | Ok(None) => continue, 319 | } 320 | } 321 | Ok(None) 322 | } 323 | 324 | /// Returns a configured scriptlet, 325 | /// 326 | pub(super) fn get_scriptlet( 327 | &self, 328 | name: &str, 329 | content: impl Into, 330 | ) -> Result, ConfigError> { 331 | let flags_key = format!("{name}_flags"); 332 | let prog_key = format!("{name}_prog"); 333 | 334 | let mut scriptlet = Scriptlet::new(content); 335 | 336 | if let Some(flags) = self.get_i64(flags_key.as_str())? { 337 | scriptlet = scriptlet.flags(rpm::ScriptletFlags::from_bits_retain(flags as u32)); 338 | } 339 | 340 | if let Some(prog) = self.get_array(prog_key.as_str())? { 341 | let prog = prog.iter().filter_map(|p| p.as_str()); 342 | scriptlet = scriptlet.prog(prog.collect()); 343 | } 344 | 345 | Ok(Some(scriptlet)) 346 | } 347 | } 348 | 349 | impl<'a> TomlValueHelper<'a> for CompoundMetadataConfig<'a> { 350 | fn get_str(&self, name: &str) -> Result, ConfigError> { 351 | self.get(|v| v.get_str(name)) 352 | } 353 | 354 | fn get_i64(&self, name: &str) -> Result, ConfigError> { 355 | self.get(|v| v.get_i64(name)) 356 | } 357 | 358 | fn get_string_or_i64(&self, name: &str) -> Result, ConfigError> { 359 | self.get(|v| v.get_string_or_i64(name)) 360 | } 361 | 362 | fn get_bool(&self, name: &str) -> Result, ConfigError> { 363 | self.get(|v| v.get_bool(name)) 364 | } 365 | 366 | fn get_table(&self, name: &str) -> Result, ConfigError> { 367 | self.get(|v| v.get_table(name)) 368 | } 369 | 370 | fn get_array(&self, name: &str) -> Result, ConfigError> { 371 | self.get(|v| v.get_array(name)) 372 | } 373 | } 374 | 375 | #[cfg(test)] 376 | mod test { 377 | use cargo_toml::Value; 378 | use toml::toml; 379 | 380 | use super::*; 381 | 382 | #[test] 383 | fn test_metadata_config() { 384 | let metadata = toml! { 385 | str = "str" 386 | int = 256 387 | bool = false 388 | table = { int = 128 } 389 | array = [ 1, 2 ] 390 | }; 391 | let metadata_config = MetadataConfig { 392 | metadata: &metadata, 393 | branch_path: None, 394 | }; 395 | 396 | assert_eq!(metadata_config.get_str("str").unwrap(), Some("str")); 397 | assert_eq!(metadata_config.get_i64("int").unwrap(), Some(256)); 398 | assert_eq!( 399 | metadata_config.get_string_or_i64("str").unwrap(), 400 | Some("str".to_string()) 401 | ); 402 | assert_eq!( 403 | metadata_config.get_string_or_i64("int").unwrap(), 404 | Some("256".to_string()) 405 | ); 406 | assert_eq!(metadata_config.get_bool("bool").unwrap(), Some(false)); 407 | assert_eq!( 408 | metadata_config.get_table("table").unwrap(), 409 | "int = 128".parse::().unwrap().as_table() 410 | ); 411 | assert_eq!( 412 | metadata_config.get_array("array").unwrap().unwrap(), 413 | [Value::Integer(1), Value::Integer(2)] 414 | ); 415 | 416 | assert_eq!(metadata_config.get_str("not-exist").unwrap(), None); 417 | assert!(matches!( 418 | metadata_config.get_str("int"), 419 | Err(ConfigError::WrongType(v, "string")) if v == "int" 420 | )); 421 | assert!(matches!( 422 | metadata_config.get_string_or_i64("array"), 423 | Err(ConfigError::WrongType(v, "string or integer")) if v == "array" 424 | )); 425 | 426 | let metadata_config = MetadataConfig { 427 | metadata: &metadata, 428 | branch_path: Some("branch".to_string()), 429 | }; 430 | assert!(matches!( 431 | metadata_config.get_str("int"), 432 | Err(ConfigError::WrongType(v, "string")) if v == "branch.int" 433 | )); 434 | assert!(matches!( 435 | metadata_config.get_string_or_i64("array"), 436 | Err(ConfigError::WrongType(v, "string or integer")) if v == "branch.array" 437 | )); 438 | } 439 | 440 | #[test] 441 | fn test_compound_metadata_config() { 442 | let metadata = [ 443 | toml! { 444 | a = 1 445 | b = 2 446 | }, 447 | toml! { 448 | b = 3 449 | c = 4 450 | }, 451 | ]; 452 | let metadata_config = metadata 453 | .iter() 454 | .map(|v| MetadataConfig { 455 | metadata: v, 456 | branch_path: None, 457 | }) 458 | .collect::>(); 459 | let metadata = CompoundMetadataConfig { 460 | config: metadata_config.as_slice(), 461 | }; 462 | assert_eq!(metadata.get_i64("a").unwrap(), Some(1)); 463 | assert_eq!(metadata.get_i64("b").unwrap(), Some(3)); 464 | assert_eq!(metadata.get_i64("c").unwrap(), Some(4)); 465 | assert_eq!(metadata.get_i64("not-exist").unwrap(), None); 466 | } 467 | 468 | #[test] 469 | fn test_get_scriptlet_config() { 470 | let metadata = toml! { 471 | test_script_flags = 0b011 472 | test_script_prog = ["/bin/blah/bash", "-c"] 473 | }; 474 | 475 | let metadata_config = MetadataConfig { 476 | metadata: &metadata, 477 | branch_path: None, 478 | }; 479 | 480 | let metadata = CompoundMetadataConfig { 481 | config: &[metadata_config], 482 | }; 483 | 484 | let scriptlet = metadata 485 | .get_scriptlet("test_script", "echo hello world") 486 | .expect("should be able to parse") 487 | .expect("should be valid scriptlet"); 488 | 489 | assert_eq!( 490 | scriptlet.flags, 491 | Some(rpm::ScriptletFlags::EXPAND | rpm::ScriptletFlags::QFORMAT) 492 | ); 493 | assert_eq!( 494 | scriptlet.program, 495 | Some(vec!["/bin/blah/bash".to_string(), "-c".to_string()]) 496 | ); 497 | assert_eq!(scriptlet.script.as_str(), "echo hello world"); 498 | } 499 | } 500 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use cargo_toml::Error as CargoTomlError; 4 | use cargo_toml::Manifest; 5 | use rpm::Dependency; 6 | use toml::value::Table; 7 | 8 | use crate::auto_req::{find_requires, AutoReqMode}; 9 | use crate::build_target::BuildTarget; 10 | use crate::cli::{Cli, ExtraMetadataSource}; 11 | use crate::error::{ConfigError, Error}; 12 | use file_info::FileInfo; 13 | use metadata::{CompoundMetadataConfig, ExtraMetaData, MetadataConfig, TomlValueHelper}; 14 | 15 | mod file_info; 16 | mod metadata; 17 | 18 | #[derive(Debug)] 19 | pub struct BuilderConfig<'a> { 20 | build_target: &'a BuildTarget, 21 | args: &'a Cli, 22 | } 23 | 24 | impl<'a> BuilderConfig<'a> { 25 | pub fn new(build_target: &'a BuildTarget, args: &'a Cli) -> BuilderConfig<'a> { 26 | BuilderConfig { build_target, args } 27 | } 28 | } 29 | 30 | #[derive(Debug)] 31 | pub struct Config { 32 | manifest: Manifest, 33 | manifest_path: PathBuf, 34 | extra_metadata: Vec, 35 | } 36 | 37 | impl Config { 38 | pub fn new( 39 | project_base_path: &Path, 40 | workspace_base_path: Option<&Path>, 41 | extra_metadata_src: &[ExtraMetadataSource], 42 | ) -> Result { 43 | let manifest_path = Self::create_cargo_toml_path(project_base_path)?; 44 | 45 | let manifest = if let Some(p) = workspace_base_path { 46 | // HACK when workspace used, manifest is generated from slice directly instead of 47 | // `from_path_with_metadata`. Because it call `inherit_workspace`, which yields an error 48 | // in case `edition.workspace = true` specified in the project manifest file. 49 | // TODO future fix when https://gitlab.com/crates.rs/cargo_toml/-/issues/20 fixed 50 | 51 | let cargo_toml_content = std::fs::read(&manifest_path) 52 | .map_err(|e| Error::FileIo(manifest_path.clone(), e))?; 53 | let mut manifest = Manifest::from_slice_with_metadata(&cargo_toml_content)?; 54 | 55 | let workspace_manifest_path = Self::create_cargo_toml_path(p)?; 56 | let workspace_manifest = 57 | Manifest::from_path(&workspace_manifest_path).map_err(|err| match err { 58 | CargoTomlError::Io(e) => { 59 | Error::FileIo(workspace_manifest_path.to_path_buf(), e) 60 | } 61 | _ => Error::CargoToml(err), 62 | })?; 63 | manifest.complete_from_path_and_workspace( 64 | manifest_path.as_path(), 65 | Some((&workspace_manifest, p)), 66 | )?; 67 | manifest 68 | } else { 69 | Manifest::from_path(&manifest_path).map_err(|err| match err { 70 | CargoTomlError::Io(e) => Error::FileIo(manifest_path.to_path_buf(), e), 71 | _ => Error::CargoToml(err), 72 | })? 73 | }; 74 | 75 | let extra_metadata = extra_metadata_src 76 | .iter() 77 | .map(|v| ExtraMetaData::new(v, &manifest_path)) 78 | .collect::, _>>()?; 79 | 80 | Ok(Config { 81 | manifest, 82 | manifest_path, 83 | extra_metadata, 84 | }) 85 | } 86 | 87 | pub(crate) fn create_cargo_toml_path>(base_path: P) -> Result { 88 | let path = base_path.as_ref().join("Cargo.toml"); 89 | path.canonicalize().map_err(|e| Error::FileIo(path, e)) 90 | } 91 | 92 | fn table_to_dependencies(table: &Table) -> Result, ConfigError> { 93 | let mut dependencies = Vec::new(); 94 | for (key, value) in table { 95 | let ver = value 96 | .as_str() 97 | .ok_or(ConfigError::WrongDependencyVersion(key.clone()))? 98 | .trim(); 99 | 100 | if ver.is_empty() { 101 | dependencies.push(Dependency::any(key)); 102 | continue; 103 | } 104 | 105 | for ver_comp in ver.split(',') { 106 | let ver_vec = ver_comp.split_whitespace().collect::>(); 107 | let dependency = match ver_vec.as_slice() { 108 | ["*"] => Ok(Dependency::any(key)), 109 | ["<", ver] => Ok(Dependency::less(key.as_str(), ver.trim())), 110 | ["<=", ver] => Ok(Dependency::less_eq(key.as_str(), ver.trim())), 111 | ["=", ver] => Ok(Dependency::eq(key.as_str(), ver.trim())), 112 | [">", ver] => Ok(Dependency::greater(key.as_str(), ver.trim())), 113 | [">=", ver] => Ok(Dependency::greater_eq(key.as_str(), ver.trim())), 114 | _ => Err(ConfigError::WrongDependencyVersion(key.clone())), 115 | }?; 116 | dependencies.push(dependency); 117 | } 118 | } 119 | Ok(dependencies) 120 | } 121 | 122 | pub fn create_rpm_builder(&self, cfg: BuilderConfig) -> Result { 123 | let mut metadata_config = Vec::new(); 124 | metadata_config.push(MetadataConfig::new_from_manifest(&self.manifest)?); 125 | for v in &self.extra_metadata { 126 | metadata_config.push(MetadataConfig::new_from_extra_metadata(v)); 127 | } 128 | let metadata = CompoundMetadataConfig::new(metadata_config.as_slice()); 129 | 130 | let pkg = self 131 | .manifest 132 | .package 133 | .as_ref() 134 | .ok_or(ConfigError::Missing("package".to_string()))?; 135 | let name = metadata.get_str("name")?.unwrap_or(pkg.name.as_str()); 136 | let version = match metadata.get_str("version")? { 137 | Some(v) => v, 138 | None => pkg.version.get()?, 139 | }; 140 | let license = match (metadata.get_str("license")?, pkg.license.as_ref()) { 141 | (Some(v), _) => v, 142 | (None, None) => Err(ConfigError::Missing("package.license".to_string()))?, 143 | (None, Some(v)) => v.get()?, 144 | }; 145 | let arch = cfg.build_target.binary_arch(); 146 | let desc = match ( 147 | metadata.get_str("summary")?, 148 | metadata.get_str("description")?, 149 | pkg.description.as_ref(), 150 | ) { 151 | (Some(v), _, _) => v, 152 | (None, Some(v), _) => v, 153 | (None, None, Some(v)) => v.get()?, 154 | (None, None, None) => Err(ConfigError::Missing("package.description".to_string()))?, 155 | }; 156 | let assets = metadata 157 | .get_array("assets")? 158 | .ok_or(ConfigError::Missing("package.assets".to_string()))?; 159 | let files = FileInfo::new(assets)?; 160 | let parent = self.manifest_path.parent().unwrap(); 161 | 162 | let mut builder = rpm::PackageBuilder::new(name, version, license, arch.as_str(), desc) 163 | .compression(cfg.args.payload_compress); 164 | builder = if let Some(t) = cfg.args.source_date { 165 | builder.source_date(t) 166 | } else if let Ok(t) = std::env::var("SOURCE_DATE_EPOCH") { 167 | let t = t 168 | .parse::() 169 | .map_err(|err| Error::EnvError("SOURCE_DATE_EPOCH", err.to_string()))?; 170 | builder.source_date(t) 171 | } else { 172 | builder 173 | }; 174 | 175 | let mut expanded_file_paths = vec![]; 176 | for (idx, file) in files.iter().enumerate() { 177 | let entries = file.generate_rpm_file_entry(cfg.build_target, parent, idx)?; 178 | for (file_source, options) in entries { 179 | expanded_file_paths.push(file_source.clone()); 180 | builder = builder.with_file(file_source, options)?; 181 | } 182 | } 183 | 184 | if let Some(release) = metadata.get_string_or_i64("release")? { 185 | builder = builder.release(release); 186 | } 187 | if let Some(epoch) = metadata.get_i64("epoch")? { 188 | builder = builder.epoch(epoch as u32); 189 | } 190 | 191 | if let Some(pre_install_script) = metadata.get_str("pre_install_script")? { 192 | let scriptlet = metadata.get_scriptlet( 193 | "pre_install_script", 194 | load_script_if_path(pre_install_script, parent, cfg.build_target)?, 195 | )?; 196 | 197 | if let Some(scriptlet) = scriptlet { 198 | builder = builder.pre_install_script(scriptlet); 199 | } 200 | } 201 | 202 | if let Some(pre_uninstall_script) = metadata.get_str("pre_uninstall_script")? { 203 | let scriptlet = metadata.get_scriptlet( 204 | "pre_uninstall_script", 205 | load_script_if_path(pre_uninstall_script, parent, cfg.build_target)?, 206 | )?; 207 | 208 | if let Some(scriptlet) = scriptlet { 209 | builder = builder.pre_uninstall_script(scriptlet); 210 | } 211 | } 212 | 213 | if let Some(post_install_script) = metadata.get_str("post_install_script")? { 214 | let scriptlet = metadata.get_scriptlet( 215 | "post_install_script", 216 | load_script_if_path(post_install_script, parent, cfg.build_target)?, 217 | )?; 218 | 219 | if let Some(scriptlet) = scriptlet { 220 | builder = builder.post_install_script(scriptlet); 221 | } 222 | } 223 | 224 | if let Some(post_uninstall_script) = metadata.get_str("post_uninstall_script")? { 225 | let scriptlet = metadata.get_scriptlet( 226 | "post_uninstall_script", 227 | load_script_if_path(post_uninstall_script, parent, cfg.build_target)?, 228 | )?; 229 | 230 | if let Some(scriptlet) = scriptlet { 231 | builder = builder.post_uninstall_script(scriptlet); 232 | } 233 | } 234 | 235 | if let Some(pre_trans_script) = metadata.get_str("pre_trans_script")? { 236 | let scriptlet = metadata.get_scriptlet( 237 | "pre_trans_script", 238 | load_script_if_path(pre_trans_script, parent, cfg.build_target)?, 239 | )?; 240 | 241 | if let Some(scriptlet) = scriptlet { 242 | builder = builder.pre_trans_script(scriptlet); 243 | } 244 | } 245 | 246 | if let Some(post_trans_script) = metadata.get_str("post_trans_script")? { 247 | let scriptlet = metadata.get_scriptlet( 248 | "post_trans_script", 249 | load_script_if_path(post_trans_script, parent, cfg.build_target)?, 250 | )?; 251 | 252 | if let Some(scriptlet) = scriptlet { 253 | builder = builder.post_trans_script(scriptlet); 254 | } 255 | } 256 | 257 | if let Some(pre_untrans_script) = metadata.get_str("pre_untrans_script")? { 258 | let scriptlet = metadata.get_scriptlet( 259 | "pre_untrans_script", 260 | load_script_if_path(pre_untrans_script, parent, cfg.build_target)?, 261 | )?; 262 | 263 | if let Some(scriptlet) = scriptlet { 264 | builder = builder.pre_untrans_script(scriptlet); 265 | } 266 | } 267 | 268 | if let Some(post_untrans_script) = metadata.get_str("post_untrans_script")? { 269 | let scriptlet = metadata.get_scriptlet( 270 | "post_untrans_script", 271 | load_script_if_path(post_untrans_script, parent, cfg.build_target)?, 272 | )?; 273 | 274 | if let Some(scriptlet) = scriptlet { 275 | builder = builder.post_untrans_script(scriptlet); 276 | } 277 | } 278 | 279 | if let Some(url) = match ( 280 | metadata.get_str("url")?, 281 | pkg.homepage.as_ref(), 282 | pkg.repository.as_ref(), 283 | ) { 284 | (Some(v), _, _) => Some(v), 285 | (None, Some(v), _) => Some(v.get()?.as_str()), 286 | (None, None, Some(v)) => Some(v.get()?.as_str()), 287 | (None, None, None) => None, 288 | } { 289 | builder = builder.url(url); 290 | } 291 | 292 | if let Some(vendor) = metadata.get_str("vendor")? { 293 | builder = builder.vendor(vendor); 294 | } 295 | 296 | if metadata.get_bool("require-sh")?.unwrap_or(true) { 297 | builder = builder.requires(Dependency::any("/bin/sh".to_string())); 298 | } 299 | 300 | if let Some(requires) = metadata.get_table("requires")? { 301 | for dependency in Self::table_to_dependencies(requires)? { 302 | builder = builder.requires(dependency); 303 | } 304 | } 305 | 306 | let meta_aut_req = metadata.get_str("auto-req")?; 307 | let auto_req = match (&cfg.args.auto_req, meta_aut_req) { 308 | (crate::cli::AutoReqMode::Auto, Some("no" | "disabled")) => AutoReqMode::Disabled, 309 | (v, _) => AutoReqMode::from(v.clone()), 310 | }; 311 | 312 | for requires in find_requires(expanded_file_paths, auto_req)? { 313 | builder = builder.requires(Dependency::any(requires)); 314 | } 315 | if let Some(obsoletes) = metadata.get_table("obsoletes")? { 316 | for dependency in Self::table_to_dependencies(obsoletes)? { 317 | builder = builder.obsoletes(dependency); 318 | } 319 | } 320 | if let Some(conflicts) = metadata.get_table("conflicts")? { 321 | for dependency in Self::table_to_dependencies(conflicts)? { 322 | builder = builder.conflicts(dependency); 323 | } 324 | } 325 | if let Some(provides) = metadata.get_table("provides")? { 326 | for dependency in Self::table_to_dependencies(provides)? { 327 | builder = builder.provides(dependency); 328 | } 329 | } 330 | if let Some(recommends) = metadata.get_table("recommends")? { 331 | for dependency in Self::table_to_dependencies(recommends)? { 332 | builder = builder.recommends(dependency); 333 | } 334 | } 335 | if let Some(supplements) = metadata.get_table("supplements")? { 336 | for dependency in Self::table_to_dependencies(supplements)? { 337 | builder = builder.supplements(dependency); 338 | } 339 | } 340 | if let Some(suggests) = metadata.get_table("suggests")? { 341 | for dependency in Self::table_to_dependencies(suggests)? { 342 | builder = builder.suggests(dependency); 343 | } 344 | } 345 | if let Some(enhances) = metadata.get_table("enhances")? { 346 | for dependency in Self::table_to_dependencies(enhances)? { 347 | builder = builder.enhances(dependency); 348 | } 349 | } 350 | 351 | Ok(builder) 352 | } 353 | } 354 | 355 | pub(crate) fn load_script_if_path>( 356 | asset: &str, 357 | parent: P, 358 | build_target: &BuildTarget, 359 | ) -> std::io::Result { 360 | let relpath = file_info::get_asset_rel_path(asset, build_target); 361 | 362 | if Path::new(&relpath).exists() { 363 | return std::fs::read_to_string(relpath); 364 | } else if let Some(p) = parent.as_ref().join(&relpath).to_str() { 365 | if Path::new(&p).exists() { 366 | return std::fs::read_to_string(p); 367 | } 368 | } 369 | 370 | Ok(asset.to_string()) 371 | } 372 | 373 | #[cfg(test)] 374 | mod test { 375 | use toml::value::Value; 376 | 377 | use super::*; 378 | 379 | #[test] 380 | fn test_config_new() { 381 | let config = Config::new(Path::new("."), None, &[]).unwrap(); 382 | let pkg = config.manifest.package.unwrap(); 383 | assert_eq!(pkg.name, "cargo-generate-rpm"); 384 | 385 | assert!(matches!(Config::new(Path::new("not_exist_dir"), None, &[]), 386 | Err(Error::FileIo(path, error)) if path == PathBuf::from("not_exist_dir/Cargo.toml") && error.kind() == std::io::ErrorKind::NotFound)); 387 | assert!( 388 | matches!(Config::new(Path::new(""), Some(Path::new("not_exist_dir")), &[]), 389 | Err(Error::FileIo(path, error)) if path == PathBuf::from("not_exist_dir/Cargo.toml") && error.kind() == std::io::ErrorKind::NotFound) 390 | ); 391 | } 392 | 393 | #[test] 394 | fn test_config_new_with_workspace() { 395 | let tempdir = tempfile::tempdir().unwrap(); 396 | 397 | let workspace_dir = tempdir.path().join("workspace"); 398 | let project_dir = workspace_dir.join("bar"); 399 | 400 | std::fs::create_dir(&workspace_dir).unwrap(); 401 | std::fs::write( 402 | workspace_dir.join("Cargo.toml"), 403 | r#" 404 | [workspace] 405 | members = ["bar"] 406 | 407 | [workspace.package] 408 | version = "1.2.3" 409 | authors = ["Nice Folks"] 410 | description = "A short description of my package" 411 | documentation = "https://example.com/bar" 412 | "#, 413 | ) 414 | .unwrap(); 415 | std::fs::create_dir(&project_dir).unwrap(); 416 | std::fs::write( 417 | project_dir.join("Cargo.toml"), 418 | r#" 419 | [package] 420 | name = "bar" 421 | version.workspace = true 422 | authors.workspace = true 423 | description.workspace = true 424 | documentation.workspace = true 425 | "#, 426 | ) 427 | .unwrap(); 428 | 429 | let config = 430 | Config::new(project_dir.as_path(), Some(workspace_dir.as_path()), &[]).unwrap(); 431 | let pkg = config.manifest.package.unwrap(); 432 | assert_eq!(pkg.name, "bar"); 433 | assert_eq!(pkg.version.get().unwrap(), "1.2.3"); 434 | 435 | assert!( 436 | matches!(Config::new(Path::new("not_exist_dir"), Some(workspace_dir.as_path()), &[]), 437 | Err(Error::FileIo(path, error)) if path == PathBuf::from("not_exist_dir/Cargo.toml") && error.kind() == std::io::ErrorKind::NotFound) 438 | ); 439 | assert!( 440 | matches!(Config::new(project_dir.as_path(), Some(Path::new("not_exist_dir")), &[]), 441 | Err(Error::FileIo(path, error)) if path == PathBuf::from("not_exist_dir/Cargo.toml") && error.kind() == std::io::ErrorKind::NotFound) 442 | ); 443 | } 444 | 445 | #[test] 446 | fn test_new() { 447 | let cargo_toml_path = std::env::current_dir().unwrap().join("Cargo.toml"); 448 | 449 | let config = Config::new(Path::new(""), None, &[]).unwrap(); 450 | assert_eq!(config.manifest.package.unwrap().name, "cargo-generate-rpm"); 451 | assert_eq!(config.manifest_path, cargo_toml_path); 452 | 453 | let config = Config::new(std::env::current_dir().unwrap().as_path(), None, &[]).unwrap(); 454 | assert_eq!(config.manifest.package.unwrap().name, "cargo-generate-rpm"); 455 | assert_eq!(config.manifest_path, cargo_toml_path); 456 | } 457 | 458 | #[test] 459 | fn test_table_to_dependencies() { 460 | // Verify various dependency version strings 461 | let mut table = Table::new(); 462 | [ 463 | ("any1", ""), 464 | ("any2", "*"), 465 | ("less", "< 1.0"), 466 | ("lesseq", "<= 1.0"), 467 | ("eq", "= 1.0"), 468 | ("greater", "> 1.0"), 469 | ("greatereq", ">= 1.0"), 470 | ] 471 | .iter() 472 | .for_each(|(k, v)| { 473 | table.insert(k.to_string(), Value::String(v.to_string())); 474 | }); 475 | assert_eq!( 476 | Config::table_to_dependencies(&table).unwrap(), 477 | vec![ 478 | rpm::Dependency::any("any1"), 479 | rpm::Dependency::any("any2"), 480 | rpm::Dependency::eq("eq", "1.0"), 481 | rpm::Dependency::greater("greater", "1.0"), 482 | rpm::Dependency::greater_eq("greatereq", "1.0"), 483 | rpm::Dependency::less("less", "1.0"), 484 | rpm::Dependency::less_eq("lesseq", "1.0"), 485 | ] 486 | ); 487 | 488 | // Inserting an invalid dependency version, expecting an error. 489 | table.insert("error".to_string(), Value::Integer(1)); 490 | assert!(matches!( 491 | Config::table_to_dependencies(&table), 492 | Err(ConfigError::WrongDependencyVersion(_)) 493 | )); 494 | 495 | // Not a valid version format, expecting an error. 496 | table.clear(); 497 | table.insert("error".to_string(), Value::String("1".to_string())); 498 | assert!(matches!( 499 | Config::table_to_dependencies(&table), 500 | Err(ConfigError::WrongDependencyVersion(_)) 501 | )); 502 | 503 | // invalid version constraint, expecting an error. 504 | table.clear(); 505 | table.insert("error".to_string(), Value::String("!= 1".to_string())); 506 | assert!(matches!( 507 | Config::table_to_dependencies(&table), 508 | Err(ConfigError::WrongDependencyVersion(_)) 509 | )); 510 | 511 | // a malformed version string, expecting an error. 512 | table.clear(); 513 | table.insert("error".to_string(), Value::String("> 1 1".to_string())); 514 | assert!(matches!( 515 | Config::table_to_dependencies(&table), 516 | Err(ConfigError::WrongDependencyVersion(_)) 517 | )); 518 | 519 | // Verify multiple-comparison dependency rule 520 | // 521 | table.clear(); 522 | table.insert( 523 | "multi_comp".to_string(), 524 | Value::String(">= 1.2, < 3.0".to_string()), 525 | ); 526 | table.insert( 527 | "single_comp".to_string(), 528 | Value::String("> 4.5".to_string()), 529 | ); 530 | assert_eq!( 531 | Config::table_to_dependencies(&table).unwrap(), 532 | vec![ 533 | rpm::Dependency::greater_eq("multi_comp", "1.2"), 534 | rpm::Dependency::less("multi_comp", "3.0"), 535 | rpm::Dependency::greater("single_comp", "4.5"), 536 | ] 537 | ); 538 | 539 | // empty in the multiple-comparison rule, expecting an error. 540 | table.insert("error".to_string(), Value::String(">= 1.2, ".to_string())); 541 | assert!(matches!( 542 | Config::table_to_dependencies(&table), 543 | Err(ConfigError::WrongDependencyVersion(_)) 544 | )); 545 | 546 | // Inserting an invalid dependency version in the multiple-version rule, expecting an error. 547 | table.clear(); 548 | table.insert("error".to_string(), Value::String(">= 1.2, 3".to_string())); 549 | assert!(matches!( 550 | Config::table_to_dependencies(&table), 551 | Err(ConfigError::WrongDependencyVersion(_)) 552 | )); 553 | } 554 | 555 | #[test] 556 | fn test_config_create_rpm_builder() { 557 | let config = Config::new(Path::new("."), None, &[]).unwrap(); 558 | let args = crate::cli::Cli { 559 | ..Default::default() 560 | }; 561 | let target = BuildTarget::new(&args); 562 | let cfg = BuilderConfig::new(&target, &args); 563 | let builder = config.create_rpm_builder(cfg); 564 | 565 | assert!(if Path::new("target/release/cargo-generate-rpm").exists() { 566 | builder.is_ok() 567 | } else { 568 | matches!(builder, Err(Error::Config(ConfigError::AssetFileNotFound(path))) if path.to_str() == Some("target/release/cargo-generate-rpm")) 569 | }); 570 | } 571 | } 572 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use cargo_toml::Error as CargoTomlError; 2 | use std::error::Error as StdError; 3 | use std::ffi::OsString; 4 | use std::fmt::{Debug, Display, Formatter}; 5 | use std::io::Error as IoError; 6 | use std::path::PathBuf; 7 | use toml::de::Error as TomlDeError; 8 | 9 | #[derive(thiserror::Error, Debug, Clone, PartialEq, Eq, Hash)] 10 | pub enum DottedBareKeyLexError { 11 | #[error("invalid key-joint character `.'")] 12 | InvalidDotChar, 13 | #[error("invalid character `{0}' and quoted key is not supported")] 14 | QuotedKey(char), 15 | #[error("invalid character `{0}'")] 16 | InvalidChar(char), 17 | } 18 | 19 | #[derive(thiserror::Error, Debug, Clone)] 20 | pub enum ConfigError { 21 | #[error("Missing field: {0}")] 22 | Missing(String), 23 | #[error("Field {0} must be {1}")] 24 | WrongType(String, &'static str), 25 | #[error("Invalid Glob at {0}: {1}")] 26 | AssetGlobInvalid(usize, &'static str), 27 | #[error("Glob at {0}-th asset found {1} which doesn't appear to be in {2}")] 28 | AssetGlobPathInvalid(usize, String, String), 29 | #[error("Failed reading {0}-th asset")] 30 | AssetReadFailed(usize), 31 | #[error("{1} of {0}-th asset is undefined")] 32 | AssetFileUndefined(usize, &'static str), 33 | #[error("{1} of {0}-th asset must be {2}")] 34 | AssetFileWrongType(usize, &'static str, &'static str), 35 | #[error("Asset file not found: {0}")] 36 | AssetFileNotFound(PathBuf), 37 | #[error("Invalid dependency version specified for {0}")] 38 | WrongDependencyVersion(String), 39 | #[error("Invalid branch path `{0}'")] 40 | WrongBranchPathOfToml(String, #[source] DottedBareKeyLexError), 41 | #[error("Branch `{0}' not found")] 42 | BranchPathNotFoundInToml(String), 43 | #[error("Field {1} for file {0} has the following error: {2}")] 44 | AssetFileRpm(usize, &'static str, #[source] std::rc::Rc), 45 | } 46 | 47 | #[derive(thiserror::Error, Debug)] 48 | pub struct FileAnnotatedError(pub Option, #[source] pub E); 49 | 50 | impl Display for FileAnnotatedError { 51 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 52 | match &self.0 { 53 | None => Display::fmt(&self.1, f), 54 | Some(path) => write!(f, "{}: {}", path.as_path().display(), self.1), 55 | } 56 | } 57 | } 58 | 59 | #[derive(thiserror::Error, Debug)] 60 | pub enum AutoReqError { 61 | #[error("Failed to execute `{file}`: {1}", file = .0.clone().into_string().unwrap_or_default())] 62 | ProcessError(OsString, #[source] IoError), 63 | #[error(transparent)] 64 | Io(#[from] IoError), 65 | } 66 | 67 | #[derive(thiserror::Error, Debug)] 68 | pub enum Error { 69 | #[error("Cargo.toml: {0}")] 70 | CargoToml(#[from] CargoTomlError), 71 | #[error(transparent)] 72 | Config(#[from] ConfigError), 73 | #[error("Invalid value of environment variable {0}: {1}")] 74 | #[allow(clippy::enum_variant_names)] // Allow bad terminology for compatibility 75 | EnvError(&'static str, String), 76 | #[error(transparent)] 77 | ParseTomlFile(#[from] FileAnnotatedError), 78 | #[error(transparent)] 79 | ExtraConfig(#[from] FileAnnotatedError), 80 | #[error(transparent)] 81 | AutoReq(#[from] AutoReqError), 82 | #[error(transparent)] 83 | Rpm(#[from] rpm::Error), 84 | #[error("{1}: {0}")] 85 | FileIo(PathBuf, #[source] IoError), 86 | #[error(transparent)] 87 | Io(#[from] IoError), 88 | } 89 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use crate::{build_target::BuildTarget, config::BuilderConfig}; 2 | use cli::Cli; 3 | use std::{ 4 | fs, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | mod auto_req; 9 | mod build_target; 10 | mod cli; 11 | mod config; 12 | mod error; 13 | 14 | use config::Config; 15 | use error::Error; 16 | 17 | fn determine_output_dir( 18 | output: Option<&PathBuf>, 19 | file_name: &str, 20 | build_target: BuildTarget, 21 | ) -> PathBuf { 22 | match output.as_ref().map(PathBuf::from) { 23 | Some(path) if path.is_dir() => path.join(file_name), 24 | Some(path) => path, 25 | None => build_target.target_path("generate-rpm").join(file_name), 26 | } 27 | } 28 | 29 | fn run() -> Result<(), Error> { 30 | let (args, matches) = Cli::get_matches_and_try_parse().unwrap_or_else(|e| e.exit()); 31 | 32 | let build_target = BuildTarget::new(&args); 33 | let extra_metadata = args.extra_metadata(&matches); 34 | 35 | let config = if let Some(p) = &args.package { 36 | Config::new(Path::new(p), Some(Path::new("")), &extra_metadata)? 37 | } else { 38 | Config::new(Path::new(""), None, &extra_metadata)? 39 | }; 40 | let rpm_pkg = config 41 | .create_rpm_builder(BuilderConfig::new(&build_target, &args))? 42 | .build()?; 43 | 44 | let pkg_name = rpm_pkg.metadata.get_name()?; 45 | let pkg_version = rpm_pkg.metadata.get_version()?; 46 | let pkg_release = rpm_pkg 47 | .metadata 48 | .get_release() 49 | .map(|v| format!("-{}", v)) 50 | .unwrap_or_default(); 51 | let pkg_arch = rpm_pkg 52 | .metadata 53 | .get_arch() 54 | .map(|v| format!(".{}", v)) 55 | .unwrap_or_default(); 56 | let file_name = format!("{pkg_name}-{pkg_version}{pkg_release}{pkg_arch}.rpm"); 57 | 58 | let target_file_name = determine_output_dir(args.output.as_ref(), &file_name, build_target); 59 | 60 | if let Some(parent_dir) = target_file_name.parent() { 61 | if !parent_dir.exists() { 62 | fs::create_dir_all(parent_dir) 63 | .map_err(|err| Error::FileIo(parent_dir.to_path_buf(), err))?; 64 | } 65 | } 66 | let mut f = fs::File::create(&target_file_name) 67 | .map_err(|err| Error::FileIo(target_file_name.to_path_buf(), err))?; 68 | rpm_pkg.write(&mut f)?; 69 | 70 | Ok(()) 71 | } 72 | 73 | fn main() { 74 | if let Err(err) = run() { 75 | eprintln!("{err}"); 76 | std::process::exit(1); 77 | } 78 | } 79 | 80 | #[cfg(test)] 81 | mod tests { 82 | use super::*; 83 | // Test the three cases of determining the output file name: 84 | // 1. Output is a directory 85 | // 2. Output is a file 86 | // 3. Output is not specified 87 | #[test] 88 | fn test_output_is_dir() { 89 | let tempdir = tempfile::tempdir().unwrap(); 90 | let pathbufbinding = &tempdir.path().to_path_buf(); 91 | 92 | let output = Some(pathbufbinding); 93 | let file_name = "test.rpm"; 94 | let build_target = BuildTarget::new(&crate::cli::Cli::default()); 95 | 96 | let target_file_name = determine_output_dir(output, file_name, build_target); 97 | assert_eq!(target_file_name, tempdir.path().join("test.rpm")); 98 | } 99 | #[test] 100 | fn test_output_is_file() { 101 | let tempdir = tempfile::tempdir().unwrap(); 102 | let pathbufbinding = &tempdir.path().to_path_buf(); 103 | let temppath = pathbufbinding.join("foo.rpm"); 104 | 105 | let output = Some(&temppath); 106 | let file_name = "test.rpm"; 107 | let build_target = BuildTarget::new(&crate::cli::Cli::default()); 108 | 109 | let target_file_name = determine_output_dir(output, file_name, build_target); 110 | assert_eq!(target_file_name, temppath); 111 | } 112 | 113 | #[test] 114 | fn test_no_output_specified() { 115 | let output = None; 116 | let file_name = "test.rpm"; 117 | let build_target = BuildTarget::new(&crate::cli::Cli::default()); 118 | 119 | let target_file_name = determine_output_dir(output, file_name, build_target); 120 | assert_eq!( 121 | target_file_name, 122 | PathBuf::from("target/generate-rpm/test.rpm") 123 | ); 124 | } 125 | } 126 | --------------------------------------------------------------------------------