mirror of
https://github.com/nix-community/nix-init.git
synced 2024-10-03 19:38:30 +03:00
initial commit
This commit is contained in:
commit
e2ea2cee18
12
.github/dependabot.yml
vendored
Normal file
12
.github/dependabot.yml
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
38
.github/workflows/ci.yml
vendored
Normal file
38
.github/workflows/ci.yml
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
name: ci
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: build
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [macos-latest, ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install nix
|
||||
uses: cachix/install-nix-action@v18
|
||||
|
||||
- name: Nix build
|
||||
run: nix build
|
||||
|
||||
clippy-rustfmt:
|
||||
name: clippy-rustfmt
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install nix
|
||||
uses: cachix/install-nix-action@v18
|
||||
|
||||
- name: "Cargo: clippy, fmt"
|
||||
run: |
|
||||
rustup toolchain install stable --profile minimal -c clippy
|
||||
rustup toolchain install nightly --profile minimal -c rustfmt
|
||||
nix develop -c cargo +stable clippy -- -D warnings
|
||||
cargo +nightly fmt -- --check
|
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
/cache
|
||||
/result
|
||||
/result-*
|
||||
/target
|
1
CHANGELOG.md
Normal file
1
CHANGELOG.md
Normal file
@ -0,0 +1 @@
|
||||
# Changelog
|
2025
Cargo.lock
generated
Normal file
2025
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
57
Cargo.toml
Normal file
57
Cargo.toml
Normal file
@ -0,0 +1,57 @@
|
||||
[package]
|
||||
name = "nix-init"
|
||||
version = "0.1.0"
|
||||
authors = ["figsoda <figsoda@pm.me>"]
|
||||
edition = "2021"
|
||||
description = "Generate Nix packages with hash prefetching, license detection, and more"
|
||||
readme = "README.md"
|
||||
homepage = "https://github.com/nix-community/nix-init"
|
||||
repository = "https://github.com/nix-community/nix-init"
|
||||
license = "MPL-2.0"
|
||||
keywords = ["cli", "interactive", "generate", "nix", "package"]
|
||||
categories = ["command-line-utilities"]
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.68"
|
||||
askalono = "0.4.6"
|
||||
bstr = "1.1.0"
|
||||
expand = "0.2.1"
|
||||
indoc = "1.0.8"
|
||||
once_cell = "1.17.0"
|
||||
owo-colors = "3.5.0"
|
||||
paste = "1.0.11"
|
||||
reqwest = { version = "0.11.13", default-features = false, features = ["json"] }
|
||||
rustc-hash = "1.1.0"
|
||||
semver = "1.0.16"
|
||||
serde = { version = "1.0.152", features = ["derive"] }
|
||||
serde_json = "1.0.91"
|
||||
toml = "0.5.10"
|
||||
url = "2.3.1"
|
||||
|
||||
[dependencies.clap]
|
||||
version = "4.1.1"
|
||||
features = ["cargo", "derive", "unicode", "wrap_help"]
|
||||
|
||||
[dependencies.rustyline]
|
||||
version = "10.1.0"
|
||||
git = "https://github.com/kkawakam/rustyline"
|
||||
default-features = false
|
||||
features = ["derive", "with-fuzzy"]
|
||||
|
||||
[dependencies.tokio]
|
||||
version = "1.24.2"
|
||||
features = ["macros", "process", "rt-multi-thread"]
|
||||
|
||||
[build-dependencies]
|
||||
askalono = "0.4.6"
|
||||
clap = { version = "4.1.1", features = ["derive"] }
|
||||
clap_complete = "4.1.0"
|
||||
clap_mangen = "0.2.7"
|
||||
|
||||
[features]
|
||||
default = ["reqwest/rustls-tls"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = "abort"
|
||||
codegen-units = 1
|
373
LICENSE
Normal file
373
LICENSE
Normal file
@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
43
README.md
Normal file
43
README.md
Normal file
@ -0,0 +1,43 @@
|
||||
# nix-init
|
||||
|
||||
[![release](https://img.shields.io/github/v/release/nix-community/nix-init?logo=github&style=flat-square)](https://github.com/nix-community/nix-init/releases)
|
||||
[![version](https://img.shields.io/crates/v/nix-init?logo=rust&style=flat-square)](https://crates.io/crates/nix-init)
|
||||
[![deps](https://deps.rs/repo/github/nix-community/nix-init/status.svg?style=flat-square&compact=true)](https://deps.rs/repo/github/nix-community/nix-init)
|
||||
[![license](https://img.shields.io/badge/license-MPL--2.0-blue?style=flat-square)](https://www.mozilla.org/en-US/MPL/2.0)
|
||||
[![ci](https://img.shields.io/github/actions/workflow/status/nix-community/nix-init/ci.yml?label=ci&logo=github-actions&style=flat-square)](https://github.com/nix-community/nix-init/actions?query=workflow:ci)
|
||||
|
||||
Generate Nix packages from URLs (WIP)
|
||||
|
||||
- Hash prefetching powered by [nurl]
|
||||
- Dependency inference for Rust packages using the [Riff](https://github.com/DeterminateSystems/riff) registry
|
||||
- Interactive prompts with fuzzy tab completions
|
||||
- License detection
|
||||
- Supported builders
|
||||
- `stdenv.mkDerivation`
|
||||
- `rustPlatform.buildRustPackage`
|
||||
- `buildGoModule`
|
||||
- Supported fetchers
|
||||
- `fetchCrate`
|
||||
- `fetchFromGitHub`
|
||||
- `fetchFromGitLab`
|
||||
- All other fetchers supported by [nurl](https://github.com/nix-community/nurl) are also supported, you just have to specify the tags manually
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
Usage: nix-init [OPTIONS] <OUTPUT>
|
||||
|
||||
Arguments:
|
||||
<OUTPUT> The path to output the generated file to
|
||||
|
||||
Options:
|
||||
-u, --url <URL> Specify the URL
|
||||
-h, --help Print help
|
||||
-V, --version Print version
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
See [CHANGELOG.md](CHANGELOG.md)
|
||||
|
||||
[nurl]: https://github.com/nix-community/nurl
|
46
build.rs
Normal file
46
build.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use askalono::Store;
|
||||
use clap::{CommandFactory, ValueEnum};
|
||||
use clap_complete::{generate_to, Shell};
|
||||
use clap_mangen::Man;
|
||||
|
||||
use std::{
|
||||
env,
|
||||
fs::{create_dir_all, File},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
include!("src/cli.rs");
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-env-changed=GEN_ARTIFACTS");
|
||||
println!("cargo:rerun-if-env-changed=SPDX_LICENSE_LIST_DATA");
|
||||
|
||||
// by default, the cache will not be rebuilt
|
||||
// remove the file to rebuild the cache
|
||||
let cache = Path::new("cache/askalono-cache.zstd");
|
||||
if !cache.is_file() {
|
||||
create_dir_all("cache").unwrap();
|
||||
let mut store = Store::new();
|
||||
store
|
||||
.load_spdx(
|
||||
env::var_os("SPDX_LICENSE_LIST_DATA").unwrap().as_ref(),
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
store.to_cache(File::create(cache).unwrap()).unwrap();
|
||||
}
|
||||
|
||||
if let Some(dir) = env::var_os("GEN_ARTIFACTS") {
|
||||
let out = &Path::new(&dir);
|
||||
create_dir_all(out).unwrap();
|
||||
let cmd = &mut Opts::command();
|
||||
|
||||
Man::new(cmd.clone())
|
||||
.render(&mut File::create(out.join("nix-init.1")).unwrap())
|
||||
.unwrap();
|
||||
|
||||
for shell in Shell::value_variants() {
|
||||
generate_to(*shell, cmd, "nix-init", out).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
27
flake.lock
Normal file
27
flake.lock
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1673796341,
|
||||
"narHash": "sha256-1kZi9OkukpNmOaPY7S5/+SlCDOuYnP3HkXHvNDyLQcc=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6dccdc458512abce8d19f74195bb20fdb067df50",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
85
flake.nix
Normal file
85
flake.nix
Normal file
@ -0,0 +1,85 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs }:
|
||||
let
|
||||
inherit (nixpkgs.lib) genAttrs importTOML licenses optionals maintainers;
|
||||
inherit (importTOML (self + "/Cargo.toml")) package;
|
||||
|
||||
forEachSystem = genAttrs [
|
||||
"aarch64-darwin"
|
||||
"aarch64-linux"
|
||||
"x86_64-darwin"
|
||||
"x86_64-linux"
|
||||
];
|
||||
in
|
||||
{
|
||||
devShells = forEachSystem (system:
|
||||
let
|
||||
inherit (nixpkgs.legacyPackages.${system}) callPackage mkShell spdx-license-list-data;
|
||||
in
|
||||
{
|
||||
default = mkShell {
|
||||
NIX_LICENSES = callPackage ./src/licenses.nix { };
|
||||
SPDX_LICENSE_LIST_DATA = "${spdx-license-list-data.json}/json/details";
|
||||
};
|
||||
});
|
||||
|
||||
formatter = forEachSystem
|
||||
(system: nixpkgs.legacyPackages.${system}.nixpkgs-fmt);
|
||||
|
||||
herculesCI.ciSystems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
];
|
||||
|
||||
packages = forEachSystem (system:
|
||||
let
|
||||
inherit (nixpkgs.legacyPackages.${system})
|
||||
callPackage darwin installShellFiles makeWrapper nurl pkg-config rustPlatform spdx-license-list-data stdenv zstd;
|
||||
in
|
||||
{
|
||||
default = rustPlatform.buildRustPackage {
|
||||
pname = "nix-init";
|
||||
inherit (package) version;
|
||||
|
||||
src = self;
|
||||
|
||||
cargoLock = {
|
||||
allowBuiltinFetchGit = true;
|
||||
lockFile = self + "/Cargo.lock";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
installShellFiles
|
||||
makeWrapper
|
||||
pkg-config
|
||||
];
|
||||
|
||||
buildInputs = [ zstd ] ++ optionals stdenv.isDarwin [
|
||||
darwin.apple_sdk.frameworks.Security
|
||||
];
|
||||
|
||||
postInstall = ''
|
||||
wrapProgram $out/bin/nix-init \
|
||||
--prefix PATH : ${nurl}/bin/nurl
|
||||
installManPage artifacts/nix-init.1
|
||||
installShellCompletion artifacts/nix-init.{bash,fish} --zsh artifacts/_nix-init
|
||||
'';
|
||||
|
||||
GEN_ARTIFACTS = "artifacts";
|
||||
NIX_LICENSES = callPackage ./src/licenses.nix { };
|
||||
SPDX_LICENSE_LIST_DATA = "${spdx-license-list-data.json}/json/details";
|
||||
ZSTD_SYS_USE_PKG_CONFIG = true;
|
||||
|
||||
meta = {
|
||||
inherit (package) description;
|
||||
license = licenses.mpl20;
|
||||
maintainers = with maintainers; [ figsoda ];
|
||||
};
|
||||
};
|
||||
});
|
||||
};
|
||||
}
|
13
rustfmt.toml
Normal file
13
rustfmt.toml
Normal file
@ -0,0 +1,13 @@
|
||||
unstable_features = true
|
||||
|
||||
condense_wildcard_suffixes = true
|
||||
format_code_in_doc_comments = true
|
||||
imports_granularity = "Crate"
|
||||
newline_style = "Unix"
|
||||
normalize_comments = true
|
||||
normalize_doc_attributes = true
|
||||
overflow_delimited_expr = false
|
||||
reorder_impl_items = true
|
||||
spaces_around_ranges = true
|
||||
use_field_init_shorthand = true
|
||||
use_try_shorthand = true
|
16
src/cli.rs
Normal file
16
src/cli.rs
Normal file
@ -0,0 +1,16 @@
|
||||
use clap::Parser;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Generate Nix packages with hash prefetching, license detection, and more
|
||||
/// https://github.com/nix-community/nix-init
|
||||
#[derive(Parser)]
|
||||
#[command(version, verbatim_doc_comment)]
|
||||
pub struct Opts {
|
||||
/// The path to output the generated file to
|
||||
pub output: PathBuf,
|
||||
|
||||
/// Specify the URL
|
||||
#[arg(short, long)]
|
||||
pub url: Option<String>,
|
||||
}
|
92
src/fetcher/crates_io.rs
Normal file
92
src/fetcher/crates_io.rs
Normal file
@ -0,0 +1,92 @@
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
fetcher::{json, PackageInfo, Revisions, Version},
|
||||
prompt::Completion,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CrateInfo {
|
||||
#[serde(rename = "crate")]
|
||||
krate: Crate,
|
||||
versions: Vec<CrateVersion>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Crate {
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CrateVersion {
|
||||
num: String,
|
||||
yanked: bool,
|
||||
}
|
||||
|
||||
pub async fn get_package_info(cl: &Client, pname: &str) -> PackageInfo {
|
||||
let mut completions = Vec::new();
|
||||
let mut versions = Default::default();
|
||||
|
||||
let Some(info) = json::<CrateInfo>(cl, format!("https://crates.io/api/v1/crates/{pname}")).await else {
|
||||
return PackageInfo {
|
||||
pname: pname.into(),
|
||||
revisions: Revisions {
|
||||
latest: "".into(),
|
||||
completions,
|
||||
versions,
|
||||
},
|
||||
description: "".into(),
|
||||
};
|
||||
};
|
||||
|
||||
let mut crate_versions = info
|
||||
.versions
|
||||
.into_iter()
|
||||
.filter_map(|CrateVersion { num, yanked }| (!yanked).then_some(num))
|
||||
.peekable();
|
||||
|
||||
let (mut found_latest, mut latest) = if let Some(version) = crate_versions.next() {
|
||||
completions.push(Completion {
|
||||
display: version.clone(),
|
||||
replacement: version.clone(),
|
||||
});
|
||||
versions.insert(version.clone(), Version::Tag);
|
||||
|
||||
(
|
||||
version
|
||||
.parse::<semver::Version>()
|
||||
.map_or(false, |version| version.pre.is_empty()),
|
||||
version,
|
||||
)
|
||||
} else {
|
||||
(false, "".into())
|
||||
};
|
||||
|
||||
for version in crate_versions {
|
||||
if !found_latest
|
||||
&& version
|
||||
.parse::<semver::Version>()
|
||||
.map_or(false, |version| version.pre.is_empty())
|
||||
{
|
||||
found_latest = true;
|
||||
latest = version.clone();
|
||||
}
|
||||
|
||||
completions.push(Completion {
|
||||
display: version.clone(),
|
||||
replacement: version.clone(),
|
||||
});
|
||||
versions.insert(version, Version::Tag);
|
||||
}
|
||||
|
||||
PackageInfo {
|
||||
pname: pname.into(),
|
||||
revisions: Revisions {
|
||||
latest,
|
||||
completions,
|
||||
versions,
|
||||
},
|
||||
description: info.krate.description,
|
||||
}
|
||||
}
|
160
src/fetcher/github.rs
Normal file
160
src/fetcher/github.rs
Normal file
@ -0,0 +1,160 @@
|
||||
use reqwest::Client;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
fetcher::{json, PackageInfo, Revisions, Version},
|
||||
prompt::Completion,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Repo {
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LatestRelease {
|
||||
tag_name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Tag {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Commit {
|
||||
sha: String,
|
||||
commit: CommitInfo,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CommitInfo {
|
||||
committer: Committer,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Committer {
|
||||
date: String,
|
||||
}
|
||||
|
||||
pub async fn get_package_info(cl: &Client, owner: &str, repo: &str) -> PackageInfo {
|
||||
let root = format!("https://api.github.com/repos/{owner}/{repo}");
|
||||
|
||||
let (description, latest_release, tags, commits) = tokio::join!(
|
||||
async {
|
||||
json(cl, &root)
|
||||
.await
|
||||
.map_or_else(String::new, |repo: Repo| repo.description)
|
||||
},
|
||||
async {
|
||||
json(cl, format!("{root}/releases/latest"))
|
||||
.await
|
||||
.map(|latest_release: LatestRelease| latest_release.tag_name)
|
||||
},
|
||||
async { json::<Vec<_>>(cl, format!("{root}/tags")).await },
|
||||
async { json::<Vec<_>>(cl, format!("{root}/commits")).await },
|
||||
);
|
||||
|
||||
let mut completions = vec![];
|
||||
let mut versions = FxHashMap::default();
|
||||
|
||||
let mut latest = if let Some(latest) = &latest_release {
|
||||
versions.insert(latest.clone(), Version::Latest);
|
||||
completions.push(Completion {
|
||||
display: format!("{latest} (latest release)"),
|
||||
replacement: latest.clone(),
|
||||
});
|
||||
latest.clone()
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
|
||||
if let Some(tags) = tags {
|
||||
if latest.is_empty() {
|
||||
if let Some(Tag { name }) = tags.first() {
|
||||
latest = name.clone();
|
||||
}
|
||||
}
|
||||
|
||||
for Tag { name } in tags {
|
||||
if matches!(&latest_release, Some(tag) if tag == &name) {
|
||||
continue;
|
||||
}
|
||||
completions.push(Completion {
|
||||
display: format!("{name} (tag)"),
|
||||
replacement: name.clone(),
|
||||
});
|
||||
versions.insert(name, Version::Tag);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(commits) = commits {
|
||||
let mut commits = commits.into_iter().take(12);
|
||||
|
||||
if let Some(Commit { sha, commit }) = commits.next() {
|
||||
if latest.is_empty() {
|
||||
latest = sha.clone();
|
||||
}
|
||||
|
||||
let date = &commit.committer.date[0 .. 10];
|
||||
let msg = commit.message.lines().next().unwrap_or_default();
|
||||
|
||||
completions.push(Completion {
|
||||
display: format!("{sha} ({date} - HEAD) {msg}"),
|
||||
replacement: sha.clone(),
|
||||
});
|
||||
versions.insert(
|
||||
sha,
|
||||
Version::Head {
|
||||
date: date.into(),
|
||||
msg: msg.into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for Commit { sha, commit } in commits {
|
||||
let date = &commit.committer.date[0 .. 10];
|
||||
let msg = commit.message.lines().next().unwrap_or_default();
|
||||
completions.push(Completion {
|
||||
display: format!("{sha} ({date}) {msg}"),
|
||||
replacement: sha.clone(),
|
||||
});
|
||||
versions.insert(
|
||||
sha,
|
||||
Version::Commit {
|
||||
date: date.into(),
|
||||
msg: msg.into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PackageInfo {
|
||||
pname: repo.into(),
|
||||
description,
|
||||
revisions: Revisions {
|
||||
latest,
|
||||
completions,
|
||||
versions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_version(cl: &Client, owner: &str, repo: &str, rev: &str) -> Option<Version> {
|
||||
let Commit { sha, commit } = json(
|
||||
cl,
|
||||
format!("https://api.github.com/repos/{owner}/{repo}/commits/{rev}"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Some(if sha.starts_with(rev) {
|
||||
Version::Commit {
|
||||
date: commit.committer.date[0 .. 10].into(),
|
||||
msg: "".into(),
|
||||
}
|
||||
} else {
|
||||
Version::Tag
|
||||
})
|
||||
}
|
190
src/fetcher/gitlab.rs
Normal file
190
src/fetcher/gitlab.rs
Normal file
@ -0,0 +1,190 @@
|
||||
use reqwest::Client;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
fetcher::{json, PackageInfo, Version},
|
||||
prompt::Completion,
|
||||
Revisions,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Repo {
|
||||
#[serde(default)]
|
||||
description: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LatestRelease {
|
||||
tag_name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Tag {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Commit {
|
||||
id: String,
|
||||
committed_date: String,
|
||||
title: String,
|
||||
}
|
||||
|
||||
pub async fn get_package_info(
|
||||
cl: &Client,
|
||||
domain: &str,
|
||||
group: &Option<String>,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
) -> PackageInfo {
|
||||
let root = get_api_root(domain, group, owner, repo);
|
||||
|
||||
let (description, latest_release, tags, commits) = tokio::join!(
|
||||
async {
|
||||
json(cl, &root)
|
||||
.await
|
||||
.map_or_else(String::new, |repo: Repo| repo.description)
|
||||
},
|
||||
async {
|
||||
json(cl, format!("{root}/releases/permalink/latest"))
|
||||
.await
|
||||
.map(|latest_release: LatestRelease| latest_release.tag_name)
|
||||
},
|
||||
async { json::<Vec<_>>(cl, format!("{root}/repository/tags")).await },
|
||||
async { json::<Vec<_>>(cl, format!("{root}/repository/commits")).await },
|
||||
);
|
||||
|
||||
let mut completions = vec![];
|
||||
let mut versions = FxHashMap::default();
|
||||
|
||||
let mut latest = if let Some(latest) = &latest_release {
|
||||
versions.insert(latest.clone(), Version::Latest);
|
||||
completions.push(Completion {
|
||||
display: format!("{latest} (latest release)"),
|
||||
replacement: latest.clone(),
|
||||
});
|
||||
latest.clone()
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
|
||||
if let Some(tags) = tags {
|
||||
if latest.is_empty() {
|
||||
if let Some(Tag { name }) = tags.first() {
|
||||
latest = name.clone();
|
||||
}
|
||||
}
|
||||
|
||||
for Tag { name } in tags {
|
||||
if matches!(&latest_release, Some(tag) if tag == &name) {
|
||||
continue;
|
||||
}
|
||||
completions.push(Completion {
|
||||
display: format!("{name} (tag)"),
|
||||
replacement: name.clone(),
|
||||
});
|
||||
versions.insert(name, Version::Tag);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(commits) = commits {
|
||||
let mut commits = commits.into_iter().take(12);
|
||||
|
||||
if let Some(Commit {
|
||||
id,
|
||||
committed_date,
|
||||
title,
|
||||
}) = commits.next()
|
||||
{
|
||||
if latest.is_empty() {
|
||||
latest = id.clone();
|
||||
}
|
||||
|
||||
let date = &committed_date[0 .. 10];
|
||||
|
||||
completions.push(Completion {
|
||||
display: format!("{id} ({date} - HEAD) {title}"),
|
||||
replacement: id.clone(),
|
||||
});
|
||||
versions.insert(
|
||||
id,
|
||||
Version::Head {
|
||||
date: date.into(),
|
||||
msg: title,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for Commit {
|
||||
id,
|
||||
committed_date,
|
||||
title,
|
||||
} in commits
|
||||
{
|
||||
let date = &committed_date[0 .. 10];
|
||||
completions.push(Completion {
|
||||
display: format!("{id} ({date}) {title}"),
|
||||
replacement: id.clone(),
|
||||
});
|
||||
versions.insert(
|
||||
id,
|
||||
Version::Commit {
|
||||
date: date.into(),
|
||||
msg: title,
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
PackageInfo {
|
||||
pname: repo.into(),
|
||||
description,
|
||||
revisions: Revisions {
|
||||
latest,
|
||||
completions,
|
||||
versions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_version(
|
||||
cl: &Client,
|
||||
domain: &str,
|
||||
group: &Option<String>,
|
||||
owner: &str,
|
||||
repo: &str,
|
||||
rev: &str,
|
||||
) -> Option<Version> {
|
||||
let Commit {
|
||||
id, committed_date, ..
|
||||
} = json(
|
||||
cl,
|
||||
format!(
|
||||
"{}/repository/commits/{rev}",
|
||||
get_api_root(domain, group, owner, repo),
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Some(if id.starts_with(rev) {
|
||||
Version::Commit {
|
||||
date: committed_date[0 .. 10].into(),
|
||||
msg: "".into(),
|
||||
}
|
||||
} else {
|
||||
Version::Tag
|
||||
})
|
||||
}
|
||||
|
||||
fn get_api_root(domain: &str, group: &Option<String>, owner: &str, repo: &str) -> String {
|
||||
let mut root = format!("https://{domain}/api/v4/projects/");
|
||||
if let Some(group) = group {
|
||||
root.push_str(group);
|
||||
root.push_str("%2F")
|
||||
}
|
||||
root.push_str(owner);
|
||||
root.push_str("%2F");
|
||||
root.push_str(repo);
|
||||
root
|
||||
}
|
88
src/fetcher/mod.rs
Normal file
88
src/fetcher/mod.rs
Normal file
@ -0,0 +1,88 @@
|
||||
mod crates_io;
|
||||
mod github;
|
||||
mod gitlab;
|
||||
|
||||
use reqwest::{Client, IntoUrl};
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::prompt::Completion;
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "fetcher", content = "args", rename_all = "camelCase")]
|
||||
pub enum Fetcher {
|
||||
FetchCrate {
|
||||
pname: String,
|
||||
},
|
||||
FetchFromGitHub {
|
||||
owner: String,
|
||||
repo: String,
|
||||
},
|
||||
FetchFromGitLab {
|
||||
#[serde(default = "default_gitlab_domain")]
|
||||
domain: String,
|
||||
group: Option<String>,
|
||||
owner: String,
|
||||
repo: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn default_gitlab_domain() -> String {
|
||||
"gitlab.com".into()
|
||||
}
|
||||
|
||||
pub enum Version {
|
||||
Latest,
|
||||
Tag,
|
||||
Head { date: String, msg: String },
|
||||
Commit { date: String, msg: String },
|
||||
}
|
||||
|
||||
pub struct Revisions {
|
||||
pub latest: String,
|
||||
pub completions: Vec<Completion>,
|
||||
pub versions: FxHashMap<String, Version>,
|
||||
}
|
||||
|
||||
pub struct PackageInfo {
|
||||
pub pname: String,
|
||||
pub revisions: Revisions,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
impl Fetcher {
|
||||
pub async fn get_package_info(&self, cl: &Client) -> PackageInfo {
|
||||
match self {
|
||||
Fetcher::FetchCrate { pname } => crates_io::get_package_info(cl, pname).await,
|
||||
Fetcher::FetchFromGitHub { owner, repo } => {
|
||||
github::get_package_info(cl, owner, repo).await
|
||||
}
|
||||
Fetcher::FetchFromGitLab {
|
||||
domain,
|
||||
group,
|
||||
owner,
|
||||
repo,
|
||||
} => gitlab::get_package_info(cl, domain, group, owner, repo).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_version(&self, cl: &Client, rev: &str) -> Option<Version> {
|
||||
match self {
|
||||
Fetcher::FetchCrate { .. } => Some(Version::Tag),
|
||||
Fetcher::FetchFromGitHub { owner, repo } => {
|
||||
github::get_version(cl, owner, repo, rev).await
|
||||
}
|
||||
Fetcher::FetchFromGitLab {
|
||||
domain,
|
||||
group,
|
||||
owner,
|
||||
repo,
|
||||
} => gitlab::get_version(cl, domain, group, owner, repo, rev).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn json<T: for<'a> Deserialize<'a>>(cl: &Client, url: impl IntoUrl) -> Option<T> {
|
||||
cl.get(url).send().await.ok()?.json().await.ok()
|
||||
}
|
250
src/inputs.rs
Normal file
250
src/inputs.rs
Normal file
@ -0,0 +1,250 @@
|
||||
use anyhow::Result;
|
||||
use paste::paste;
|
||||
use rustc_hash::FxHashMap;
|
||||
use serde::Deserialize;
|
||||
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
io::{Read, Write},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AllInputs {
|
||||
pub native_build_inputs: Inputs,
|
||||
pub build_inputs: Inputs,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Inputs {
|
||||
pub always: BTreeSet<String>,
|
||||
darwin: BTreeSet<String>,
|
||||
aarch64_darwin: BTreeSet<String>,
|
||||
x86_64_darwin: BTreeSet<String>,
|
||||
linux: BTreeSet<String>,
|
||||
aarch64_linux: BTreeSet<String>,
|
||||
x86_64_linux: BTreeSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CargoLock {
|
||||
package: Vec<CargoPackage>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct CargoPackage {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RiffRegistry {
|
||||
language: RiffLanguages,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RiffLanguages {
|
||||
rust: RiffLanguage,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RiffLanguage {
|
||||
dependencies: FxHashMap<String, RiffDependency>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(default)]
|
||||
struct RiffDependency {
|
||||
#[serde(flatten)]
|
||||
inputs: RiffInputs,
|
||||
targets: RiffTargets,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(default)]
|
||||
struct RiffTargets {
|
||||
#[serde(rename = "aarch64-apple-darwin")]
|
||||
aarch64_darwin: RiffInputs,
|
||||
#[serde(rename = "aarch64-unknown-linux-gnu")]
|
||||
aarch64_linux: RiffInputs,
|
||||
#[serde(rename = "x86_64-apple-darwin")]
|
||||
x86_64_darwin: RiffInputs,
|
||||
#[serde(rename = "x86_64-unknown-linux-gnu")]
|
||||
x86_64_linux: RiffInputs,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
#[serde(default, rename_all = "kebab-case")]
|
||||
struct RiffInputs {
|
||||
native_build_inputs: Vec<String>,
|
||||
build_inputs: Vec<String>,
|
||||
}
|
||||
|
||||
pub async fn load_riff_dependencies(inputs: &mut AllInputs, mut lock: impl Read) {
|
||||
let (Some(lock), Some(mut registry)) = tokio::join!(
|
||||
async {
|
||||
let mut buf = String::new();
|
||||
lock.read_to_string(&mut buf).ok()?;
|
||||
toml::from_str::<CargoLock>(&buf).ok()
|
||||
},
|
||||
async {
|
||||
reqwest::get("https://registry.riff.determinate.systems/riff-registry.json")
|
||||
.await
|
||||
.ok()?
|
||||
.json::<RiffRegistry>()
|
||||
.await
|
||||
.ok()
|
||||
},
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
for dep in lock.package {
|
||||
let Some(dep) = registry.language.rust.dependencies.remove(&dep.name) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
for input in dep.inputs.native_build_inputs {
|
||||
inputs.native_build_inputs.always.insert(input);
|
||||
}
|
||||
for input in dep.inputs.build_inputs {
|
||||
inputs.build_inputs.always.insert(input);
|
||||
}
|
||||
|
||||
macro_rules! load {
|
||||
($inputs:ident, $sys:ident) => {
|
||||
paste! {
|
||||
for input in dep.targets.[<aarch64_ $sys>].$inputs {
|
||||
if inputs.$inputs.always.contains(&input)
|
||||
|| inputs.$inputs.$sys.contains(&input) {
|
||||
continue;
|
||||
} else if inputs.$inputs.[<x86_64_ $sys>].remove(&input) {
|
||||
inputs.$inputs.$sys.insert(input);
|
||||
} else {
|
||||
inputs.$inputs.[<aarch64_ $sys>].insert(input);
|
||||
}
|
||||
}
|
||||
|
||||
for input in dep.targets.[<x86_64_ $sys>].$inputs {
|
||||
if inputs.$inputs.always.contains(&input)
|
||||
|| inputs.$inputs.$sys.contains(&input) {
|
||||
continue;
|
||||
} else if inputs.$inputs.[<aarch64_ $sys>].remove(&input) {
|
||||
inputs.$inputs.$sys.insert(input);
|
||||
} else {
|
||||
inputs.$inputs.[<x86_64_ $sys>].insert(input);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
load!(native_build_inputs, darwin);
|
||||
load!(native_build_inputs, linux);
|
||||
load!(build_inputs, darwin);
|
||||
load!(build_inputs, linux);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_all_lambda_inputs<const N: usize>(
|
||||
out: &mut impl Write,
|
||||
inputs: &AllInputs,
|
||||
written: [&'static str; N],
|
||||
) -> Result<(bool, bool)> {
|
||||
let written = &mut written.into_iter().map(Into::into).collect();
|
||||
Ok((
|
||||
write_lambda_inputs(out, written, &inputs.native_build_inputs)?,
|
||||
write_lambda_inputs(out, written, &inputs.build_inputs)?,
|
||||
))
|
||||
}
|
||||
|
||||
fn write_lambda_inputs(
|
||||
out: &mut impl Write,
|
||||
written: &mut BTreeSet<String>,
|
||||
inputs: &Inputs,
|
||||
) -> Result<bool> {
|
||||
let mut non_empty = false;
|
||||
|
||||
for input in inputs
|
||||
.always
|
||||
.iter()
|
||||
.filter_map(|input| input.split('.').next())
|
||||
{
|
||||
non_empty = true;
|
||||
if written.insert(input.into()) {
|
||||
writeln!(out, ", {input}")?;
|
||||
}
|
||||
}
|
||||
|
||||
for input in [
|
||||
&inputs.darwin,
|
||||
&inputs.aarch64_darwin,
|
||||
&inputs.x86_64_darwin,
|
||||
&inputs.linux,
|
||||
&inputs.aarch64_linux,
|
||||
&inputs.x86_64_linux,
|
||||
]
|
||||
.into_iter()
|
||||
.flat_map(IntoIterator::into_iter)
|
||||
.filter_map(|input| input.split('.').next())
|
||||
{
|
||||
non_empty = true;
|
||||
if written.insert("stdenv".into()) {
|
||||
writeln!(out, ", stdenv")?;
|
||||
}
|
||||
if written.insert(input.into()) {
|
||||
writeln!(out, ", {input}")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(non_empty)
|
||||
}
|
||||
|
||||
pub fn write_inputs(out: &mut impl Write, inputs: &Inputs, name: &'static str) -> Result<()> {
|
||||
write!(out, " {name} =")?;
|
||||
|
||||
let mut inputs = [
|
||||
("", &inputs.always),
|
||||
("lib.optionals stdenv.isDarwin ", &inputs.darwin),
|
||||
(
|
||||
"lib.optionals (stdenv.isDarwin && stdenv.isAarch64) ",
|
||||
&inputs.aarch64_darwin,
|
||||
),
|
||||
(
|
||||
"lib.optionals (stdenv.isDarwin && stdenv.isx86_64) ",
|
||||
&inputs.x86_64_darwin,
|
||||
),
|
||||
("lib.optionals stdenv.isLinux ", &inputs.linux),
|
||||
(
|
||||
"lib.optionals (stdenv.isLinux && stdenv.isAarch64) ",
|
||||
&inputs.aarch64_linux,
|
||||
),
|
||||
(
|
||||
"lib.optionals (stdenv.isLinux && stdenv.isx86_64) ",
|
||||
&inputs.x86_64_linux,
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.filter(|(_, inputs)| !inputs.is_empty());
|
||||
|
||||
if let Some((prefix, inputs)) = inputs.next() {
|
||||
write!(out, " {prefix}")?;
|
||||
write_input_list(out, inputs)?;
|
||||
}
|
||||
|
||||
for (prefix, inputs) in inputs {
|
||||
write!(out, " ++ {prefix}")?;
|
||||
write_input_list(out, inputs)?;
|
||||
}
|
||||
|
||||
writeln!(out, ";\n")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_input_list(out: &mut impl Write, inputs: &BTreeSet<String>) -> Result<()> {
|
||||
writeln!(out, "[")?;
|
||||
for input in inputs {
|
||||
writeln!(out, " {input}")?;
|
||||
}
|
||||
write!(out, " ]")?;
|
||||
Ok(())
|
||||
}
|
22
src/licenses.nix
Normal file
22
src/licenses.nix
Normal file
@ -0,0 +1,22 @@
|
||||
{ lib, writeText }:
|
||||
|
||||
let
|
||||
inherit (builtins) concatLists concatStringsSep length;
|
||||
inherit (lib) flip licenses mapAttrsToList optional;
|
||||
|
||||
inserts = concatLists
|
||||
(flip mapAttrsToList licenses
|
||||
(k: v: optional (v ? spdxId) '' xs.insert("${v.spdxId}", "${k}");''));
|
||||
in
|
||||
|
||||
writeText "licenses.rs" ''
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn get_nix_licenses() -> FxHashMap<&'static str, &'static str> {
|
||||
let mut xs = HashMap::with_capacity_and_hasher(${toString (length inserts)}, Default::default());
|
||||
${concatStringsSep "\n " inserts}
|
||||
xs
|
||||
}
|
||||
''
|
1
src/licenses.rs
Normal file
1
src/licenses.rs
Normal file
@ -0,0 +1 @@
|
||||
include!(env!("NIX_LICENSES"));
|
555
src/main.rs
Normal file
555
src/main.rs
Normal file
@ -0,0 +1,555 @@
|
||||
mod cli;
|
||||
mod fetcher;
|
||||
mod inputs;
|
||||
mod licenses;
|
||||
mod prompt;
|
||||
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use askalono::{Match, Store, TextData};
|
||||
use bstr::ByteVec;
|
||||
use clap::Parser;
|
||||
use expand::expand;
|
||||
use indoc::{formatdoc, writedoc};
|
||||
use once_cell::sync::Lazy;
|
||||
use reqwest::Client;
|
||||
use rustc_hash::FxHashMap;
|
||||
use rustyline::{config::Configurer, CompletionType, Editor};
|
||||
use serde::Deserialize;
|
||||
use tokio::process::Command;
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fs::{read_dir, read_to_string, File},
|
||||
io::{BufRead, Write},
|
||||
path::PathBuf,
|
||||
process::Output,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cli::Opts,
|
||||
fetcher::{Fetcher, PackageInfo, Revisions, Version},
|
||||
inputs::{load_riff_dependencies, write_all_lambda_inputs, write_inputs, AllInputs},
|
||||
licenses::get_nix_licenses,
|
||||
prompt::{prompt, Prompter},
|
||||
};
|
||||
|
||||
static LICENSE_STORE: Lazy<Option<Store>> =
|
||||
Lazy::new(|| Store::from_cache(include_bytes!("../cache/askalono-cache.zstd") as &[_]).ok());
|
||||
static NIX_LICENSES: Lazy<FxHashMap<&'static str, &'static str>> = Lazy::new(get_nix_licenses);
|
||||
|
||||
const FAKE_HASH: &str = "sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum MaybeFetcher {
|
||||
Known(Fetcher),
|
||||
Unknown { fetcher: String },
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct BuildResult {
|
||||
outputs: Outputs,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Outputs {
|
||||
out: String,
|
||||
}
|
||||
|
||||
pub enum BuildType {
|
||||
BuildGoModule,
|
||||
BuildRustPackage,
|
||||
MkDerivation,
|
||||
MkDerivationCargo,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let opts = Opts::parse();
|
||||
|
||||
tokio::spawn(async {
|
||||
Lazy::force(&LICENSE_STORE);
|
||||
});
|
||||
tokio::spawn(async {
|
||||
Lazy::force(&NIX_LICENSES);
|
||||
});
|
||||
|
||||
let mut out = File::create(opts.output)?;
|
||||
writeln!(out, "{{ lib")?;
|
||||
|
||||
let mut editor = Editor::new()?;
|
||||
editor.set_completion_type(CompletionType::Fuzzy);
|
||||
editor.set_max_history_size(0)?;
|
||||
|
||||
let url = match opts.url {
|
||||
Some(url) => url,
|
||||
None => editor.readline(&prompt("Enter url"))?,
|
||||
};
|
||||
|
||||
let Output { stdout, status, .. } = Command::new("nurl").arg(&url).arg("-p").output().await?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("command exited with {status}");
|
||||
}
|
||||
|
||||
let fetcher = serde_json::from_slice(&stdout)?;
|
||||
let (pname, rev, version, desc) = if let MaybeFetcher::Known(fetcher) = &fetcher {
|
||||
let cl = Client::builder().user_agent("Mozilla/5.0").build().unwrap();
|
||||
|
||||
let PackageInfo {
|
||||
pname,
|
||||
description,
|
||||
revisions,
|
||||
} = fetcher.get_package_info(&cl).await;
|
||||
|
||||
let rev_msg = prompt(format_args!(
|
||||
"Enter tag or revision (defaults to {})",
|
||||
revisions.latest
|
||||
));
|
||||
editor.set_helper(Some(Prompter::Revision(revisions)));
|
||||
|
||||
let rev = editor.readline(&rev_msg)?;
|
||||
|
||||
let Some(Prompter::Revision(revisions)) = editor.helper_mut() else {
|
||||
unreachable!();
|
||||
};
|
||||
let rev = if rev.is_empty() {
|
||||
revisions.latest.clone()
|
||||
} else {
|
||||
rev
|
||||
};
|
||||
|
||||
let version = match match revisions.versions.remove(&rev) {
|
||||
Some(version) => Some(version),
|
||||
None => fetcher.get_version(&cl, &rev).await,
|
||||
} {
|
||||
Some(Version::Latest | Version::Tag) => {
|
||||
rev[rev.find(char::is_numeric).unwrap_or_default() ..].into()
|
||||
}
|
||||
Some(Version::Head { date, .. } | Version::Commit { date, .. }) => {
|
||||
format!("unstable-{date}")
|
||||
}
|
||||
None => "".into(),
|
||||
};
|
||||
|
||||
editor.set_helper(Some(Prompter::NonEmpty));
|
||||
(
|
||||
Some(pname),
|
||||
rev,
|
||||
editor.readline_with_initial(&prompt("Enter version"), (&version, ""))?,
|
||||
description,
|
||||
)
|
||||
} else {
|
||||
let pname = url.parse::<url::Url>().ok().and_then(|url| {
|
||||
url.path_segments()
|
||||
.and_then(|xs| xs.last())
|
||||
.map(|pname| pname.strip_suffix(".git").unwrap_or(pname).into())
|
||||
});
|
||||
|
||||
editor.set_helper(Some(Prompter::NonEmpty));
|
||||
(
|
||||
pname,
|
||||
editor.readline(&prompt("Enter tag or revision"))?,
|
||||
editor.readline(&prompt("Enter version"))?,
|
||||
"".into(),
|
||||
)
|
||||
};
|
||||
|
||||
let pname = if let Some(pname) = pname {
|
||||
editor.readline_with_initial(
|
||||
&prompt("Enter pname"),
|
||||
(
|
||||
&pname
|
||||
.to_lowercase()
|
||||
.replace(|c: char| c.is_ascii_punctuation(), "-"),
|
||||
"",
|
||||
),
|
||||
)?
|
||||
} else {
|
||||
editor.readline(&prompt("Enter pname"))?
|
||||
};
|
||||
|
||||
let mut cmd = Command::new("nurl");
|
||||
cmd.arg(&url).arg(&rev);
|
||||
|
||||
let src_expr = {
|
||||
if let MaybeFetcher::Known(Fetcher::FetchCrate { .. }) = fetcher {
|
||||
let Output { stdout, status, .. } = cmd.arg("-H").output().await?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("command exited with {status}");
|
||||
}
|
||||
|
||||
formatdoc!(
|
||||
r#"
|
||||
fetchCrate {{
|
||||
inherit pname version;
|
||||
hash = "{}";
|
||||
}}"#,
|
||||
String::from_utf8(stdout)?.trim_end(),
|
||||
)
|
||||
} else {
|
||||
if rev == version {
|
||||
cmd.arg("-o").arg("rev").arg("version");
|
||||
} else if rev.contains(&version) {
|
||||
cmd.arg("-O")
|
||||
.arg("rev")
|
||||
.arg(rev.replacen(&version, "${version}", 1));
|
||||
}
|
||||
|
||||
let Output { stdout, status, .. } = cmd.arg("-i").arg("2").output().await?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("command exited with {status}");
|
||||
}
|
||||
|
||||
String::from_utf8(stdout)?
|
||||
}
|
||||
};
|
||||
|
||||
let Output { stdout, status, .. } = Command::new("nix")
|
||||
.arg("build")
|
||||
.arg("--extra-experimental-features")
|
||||
.arg("nix-command")
|
||||
.arg("--impure")
|
||||
.arg("--no-link")
|
||||
.arg("--json")
|
||||
.arg("--expr")
|
||||
.arg(format!(
|
||||
"let pname={pname:?};version={version:?};in(import<nixpkgs>{{}}).{src_expr}"
|
||||
))
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !status.success() {
|
||||
bail!("command exited with {status}");
|
||||
}
|
||||
|
||||
let src = serde_json::from_slice::<Vec<BuildResult>>(&stdout)?
|
||||
.into_iter()
|
||||
.next()
|
||||
.context("failed to build source")?
|
||||
.outputs
|
||||
.out;
|
||||
let src_dir = PathBuf::from(&src);
|
||||
|
||||
let mut choices = Vec::new();
|
||||
let has_cargo = src_dir.join("Cargo.toml").is_file();
|
||||
let has_cmake = src_dir.join("CMakeLists.txt").is_file();
|
||||
let has_go = src_dir.join("go.mod").is_file();
|
||||
let has_meson = src_dir.join("meson.build").is_file();
|
||||
|
||||
if has_cargo {
|
||||
if has_meson {
|
||||
choices.push((
|
||||
BuildType::MkDerivationCargo,
|
||||
"stdenv.mkDerivation + rustPlatform.cargoSetupHook",
|
||||
));
|
||||
choices.push((BuildType::BuildRustPackage, "rustPlatform.buildRustPackage"));
|
||||
} else {
|
||||
choices.push((BuildType::BuildRustPackage, "rustPlatform.buildRustPackage"));
|
||||
choices.push((
|
||||
BuildType::MkDerivationCargo,
|
||||
"stdenv.mkDerivation + rustPlatform.cargoSetupHook",
|
||||
));
|
||||
}
|
||||
}
|
||||
if has_go {
|
||||
choices.push((BuildType::BuildGoModule, "buildGoModule"));
|
||||
}
|
||||
choices.push((BuildType::MkDerivation, "stdenv.mkDerivation"));
|
||||
|
||||
editor.set_helper(Some(Prompter::Build(choices)));
|
||||
let choice = editor.readline(&prompt("How should this package be built?"))?;
|
||||
let Some(Prompter::Build(choices)) = editor.helper_mut() else {
|
||||
unreachable!();
|
||||
};
|
||||
let (choice, _) = choice
|
||||
.parse()
|
||||
.ok()
|
||||
.and_then(|i: usize| choices.get(i))
|
||||
.unwrap_or_else(|| &choices[0]);
|
||||
|
||||
let mut inputs = AllInputs::default();
|
||||
match choice {
|
||||
BuildType::BuildGoModule => {
|
||||
writeln!(out, ", buildGoModule")?;
|
||||
}
|
||||
BuildType::BuildRustPackage => {
|
||||
writeln!(out, ", rustPlatform")?;
|
||||
}
|
||||
BuildType::MkDerivation => {
|
||||
writeln!(out, ", stdenv")?;
|
||||
if has_cargo {
|
||||
inputs
|
||||
.native_build_inputs
|
||||
.always
|
||||
.extend(["cargo".into(), "rustc".into()]);
|
||||
}
|
||||
if has_cmake {
|
||||
inputs.native_build_inputs.always.insert("cmake".into());
|
||||
}
|
||||
if has_meson {
|
||||
inputs
|
||||
.native_build_inputs
|
||||
.always
|
||||
.extend(["meson".into(), "ninja".into()]);
|
||||
}
|
||||
}
|
||||
BuildType::MkDerivationCargo => {
|
||||
writeln!(out, ", stdenv")?;
|
||||
if has_cmake {
|
||||
inputs.native_build_inputs.always.insert("cmake".into());
|
||||
}
|
||||
if has_meson {
|
||||
inputs
|
||||
.native_build_inputs
|
||||
.always
|
||||
.extend(["meson".into(), "ninja".into()]);
|
||||
}
|
||||
inputs.native_build_inputs.always.extend([
|
||||
"rustPlatform.cargoSetupHook".into(),
|
||||
"rustPlatform.rust.cargo".into(),
|
||||
"rustPlatform.rust.rustc".into(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
match fetcher {
|
||||
MaybeFetcher::Known(fetcher) => {
|
||||
writeln!(
|
||||
out,
|
||||
", {}",
|
||||
match fetcher {
|
||||
Fetcher::FetchCrate { .. } => "fetchCrate",
|
||||
Fetcher::FetchFromGitHub { .. } => "fetchFromGitHub",
|
||||
Fetcher::FetchFromGitLab { .. } => "fetchFromGitLab",
|
||||
},
|
||||
)?;
|
||||
}
|
||||
MaybeFetcher::Unknown { fetcher } => {
|
||||
writeln!(out, ", {fetcher}")?;
|
||||
}
|
||||
}
|
||||
|
||||
let (native_build_inputs, build_inputs) = match choice {
|
||||
BuildType::BuildGoModule => {
|
||||
let hash = if src_dir.join("vendor").is_dir() {
|
||||
"null".into()
|
||||
} else {
|
||||
fod_hash(format!(
|
||||
r#"(import<nixpkgs>{{}}).buildGoModule{{pname={pname:?};version={version:?};src={src};vendorHash="{FAKE_HASH}";}}"#,
|
||||
))
|
||||
.await
|
||||
.map_or_else(|| format!(r#""{FAKE_HASH}""#), |hash| format!(r#""{hash}""#))
|
||||
};
|
||||
|
||||
let res = write_all_lambda_inputs(&mut out, &inputs, ["rustPlatform"])?;
|
||||
writedoc!(
|
||||
out,
|
||||
r#"
|
||||
}}:
|
||||
|
||||
buildGoModule rec {{
|
||||
pname = {pname:?};
|
||||
version = {version:?};
|
||||
|
||||
src = {src_expr};
|
||||
|
||||
vendorHash = {hash};
|
||||
|
||||
"#,
|
||||
)?;
|
||||
res
|
||||
}
|
||||
|
||||
BuildType::BuildRustPackage | BuildType::MkDerivationCargo => {
|
||||
let hash = if let Ok(lock) = File::open(src_dir.join("Cargo.lock")) {
|
||||
let (hash, ()) = tokio::join!(
|
||||
fod_hash(format!(
|
||||
r#"(import<nixpkgs>{{}}).rustPlatform.fetchCargoTarball{{name="{pname}-{version}";src={src};hash="{FAKE_HASH}";}}"#,
|
||||
)),
|
||||
load_riff_dependencies(&mut inputs, &lock),
|
||||
);
|
||||
|
||||
hash.unwrap_or_else(|| FAKE_HASH.into())
|
||||
} else {
|
||||
FAKE_HASH.into()
|
||||
};
|
||||
|
||||
if matches!(choice, BuildType::BuildRustPackage) {
|
||||
let res = write_all_lambda_inputs(&mut out, &inputs, ["rustPlatform"])?;
|
||||
writedoc!(
|
||||
out,
|
||||
r#"
|
||||
}}:
|
||||
|
||||
rustPlatform.buildRustPackage rec {{
|
||||
pname = {pname:?};
|
||||
version = {version:?};
|
||||
|
||||
src = {src_expr};
|
||||
|
||||
cargoHash = "{hash}";
|
||||
|
||||
"#,
|
||||
)?;
|
||||
res
|
||||
} else {
|
||||
let res = write_all_lambda_inputs(&mut out, &inputs, ["stdenv"])?;
|
||||
writedoc!(
|
||||
out,
|
||||
r#"
|
||||
}}:
|
||||
|
||||
stdenv.mkDerivation rec {{
|
||||
pname = {pname:?};
|
||||
version = {version:?};
|
||||
|
||||
src = {src_expr};
|
||||
|
||||
cargoDeps = rustPlatform.fetchCargoTarball {{
|
||||
inherit src;
|
||||
name = "${{pname}}-${{version}}";
|
||||
hash = "{hash}";
|
||||
}};
|
||||
|
||||
"#,
|
||||
)?;
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
BuildType::MkDerivation => {
|
||||
let res = write_all_lambda_inputs(&mut out, &inputs, ["stdenv"])?;
|
||||
writedoc!(
|
||||
out,
|
||||
r#"
|
||||
}}:
|
||||
|
||||
stdenv.mkDerivation rec {{
|
||||
pname = {pname:?};
|
||||
version = {version:?};
|
||||
|
||||
src = {src_expr};
|
||||
|
||||
"#,
|
||||
)?;
|
||||
res
|
||||
}
|
||||
};
|
||||
|
||||
if native_build_inputs {
|
||||
write_inputs(&mut out, &inputs.native_build_inputs, "nativeBuildInputs")?;
|
||||
}
|
||||
if build_inputs {
|
||||
write_inputs(&mut out, &inputs.build_inputs, "buildInputs")?;
|
||||
}
|
||||
|
||||
let desc = desc.trim();
|
||||
write!(out, " ")?;
|
||||
writedoc!(
|
||||
out,
|
||||
r"
|
||||
meta = with lib; {{
|
||||
description = {:?};
|
||||
homepage = {url:?};
|
||||
license = ",
|
||||
desc.strip_suffix('.').unwrap_or(desc),
|
||||
)?;
|
||||
|
||||
if let Some(store) = &*LICENSE_STORE {
|
||||
let nix_licenses = &*NIX_LICENSES;
|
||||
let mut licenses = Vec::new();
|
||||
|
||||
for entry in read_dir(src_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if !path.is_file() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = entry.file_name();
|
||||
let name = <Vec<u8> as ByteVec>::from_os_str_lossy(&name);
|
||||
if !matches!(
|
||||
name.to_ascii_lowercase()[..],
|
||||
expand!([@b"license", ..] | [@b"licence", ..] | [@b"copying", ..]),
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(text) = read_to_string(path) else { continue; };
|
||||
let Match { score, name, .. } = store.analyze(&TextData::from(text));
|
||||
if let Some(license) = nix_licenses.get(name) {
|
||||
licenses.push((score, license));
|
||||
}
|
||||
}
|
||||
|
||||
licenses.dedup_by_key(|(_, license)| *license);
|
||||
|
||||
if let [(_, license)] = &licenses[..] {
|
||||
write!(out, "licenses.{license}")?;
|
||||
} else {
|
||||
licenses.sort_by(|x, y| match x.0.partial_cmp(&y.0) {
|
||||
None | Some(Ordering::Equal) => x.1.cmp(y.1),
|
||||
Some(cmp) => cmp,
|
||||
});
|
||||
|
||||
let n = match licenses.iter().position(|(score, _)| score < &0.75) {
|
||||
Some(0) => 1,
|
||||
Some(n) => n,
|
||||
None => licenses.len(),
|
||||
};
|
||||
|
||||
write!(out, "with licenses; [ ")?;
|
||||
for (_, license) in licenses.into_iter().take(n) {
|
||||
write!(out, "{license} ")?;
|
||||
}
|
||||
write!(out, "]")?;
|
||||
}
|
||||
}
|
||||
|
||||
writedoc!(
|
||||
out,
|
||||
"
|
||||
;
|
||||
maintainers = with maintainers; [ ];
|
||||
}};
|
||||
}}
|
||||
",
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fod_hash(expr: String) -> Option<String> {
|
||||
let Output { stderr, status, .. } = Command::new("nix")
|
||||
.arg("build")
|
||||
.arg("--extra-experimental-features")
|
||||
.arg("nix-command")
|
||||
.arg("--impure")
|
||||
.arg("--no-link")
|
||||
.arg("--expr")
|
||||
.arg(expr)
|
||||
.output()
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
if status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut lines = stderr.lines();
|
||||
loop {
|
||||
let Ok(line) = lines.next()? else { continue; };
|
||||
if !line.trim_start().starts_with("specified:") {
|
||||
continue;
|
||||
}
|
||||
let Ok(line) = lines.next()? else { continue; };
|
||||
let Some(hash) = line.trim_start().strip_prefix("got:") else {
|
||||
continue;
|
||||
};
|
||||
return Some(hash.trim().to_owned());
|
||||
}
|
||||
}
|
166
src/prompt.rs
Normal file
166
src/prompt.rs
Normal file
@ -0,0 +1,166 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use owo_colors::{OwoColorize, Style};
|
||||
use rustyline::{
|
||||
completion::{Candidate, Completer},
|
||||
hint::{Hint, Hinter},
|
||||
validate::{ValidationContext, ValidationResult, Validator},
|
||||
Context, Helper, Highlighter,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
fetcher::{Revisions, Version},
|
||||
BuildType,
|
||||
};
|
||||
|
||||
#[derive(Helper, Highlighter)]
|
||||
pub enum Prompter {
|
||||
Revision(Revisions),
|
||||
NonEmpty,
|
||||
Build(Vec<(BuildType, &'static str)>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Completion {
|
||||
pub display: String,
|
||||
pub replacement: String,
|
||||
}
|
||||
|
||||
impl Candidate for Completion {
|
||||
fn display(&self) -> &str {
|
||||
&self.display
|
||||
}
|
||||
|
||||
fn replacement(&self) -> &str {
|
||||
&self.replacement
|
||||
}
|
||||
}
|
||||
|
||||
impl Completer for Prompter {
|
||||
type Candidate = Completion;
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
_: &str,
|
||||
_: usize,
|
||||
_: &Context<'_>,
|
||||
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
|
||||
match self {
|
||||
Prompter::Revision(revisions) => Ok((0, revisions.completions.clone())),
|
||||
Prompter::NonEmpty => Ok((0, Vec::new())),
|
||||
Prompter::Build(choices) => Ok((
|
||||
0,
|
||||
choices
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, &(_, msg))| Completion {
|
||||
display: msg.into(),
|
||||
replacement: i.to_string(),
|
||||
})
|
||||
.collect(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SimpleHint(String);
|
||||
|
||||
impl Hint for SimpleHint {
|
||||
fn display(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
fn completion(&self) -> Option<&str> {
|
||||
Some("")
|
||||
}
|
||||
}
|
||||
|
||||
impl Hinter for Prompter {
|
||||
type Hint = SimpleHint;
|
||||
|
||||
fn hint(&self, line: &str, _: usize, _: &Context<'_>) -> Option<Self::Hint> {
|
||||
match self {
|
||||
Prompter::Revision(revisions) => {
|
||||
let style = Style::new().blue().italic();
|
||||
if line.is_empty() {
|
||||
return Some(SimpleHint(
|
||||
format_args!(" {}", revisions.latest)
|
||||
.style(style)
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
revisions.versions.get(line).map(|version| {
|
||||
SimpleHint(match version {
|
||||
Version::Latest => " (latest release)".style(style).to_string(),
|
||||
Version::Tag => " (tag)".style(style).to_string(),
|
||||
Version::Head { date, msg } => format_args!(" ({date} - HEAD) {msg}")
|
||||
.style(style)
|
||||
.to_string(),
|
||||
Version::Commit { date, msg } => {
|
||||
format_args!(" ({date}) {msg}").style(style).to_string()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Prompter::NonEmpty => None,
|
||||
|
||||
Prompter::Build(choices) => Some(SimpleHint(if line.is_empty() {
|
||||
format_args!(" ({})", choices[0].1)
|
||||
.blue()
|
||||
.italic()
|
||||
.to_string()
|
||||
} else if let Some((_, msg)) = line.parse().ok().and_then(|i: usize| choices.get(i)) {
|
||||
format_args!(" ({msg})").blue().italic().to_string()
|
||||
} else {
|
||||
" press <tab> to see options".yellow().italic().to_string()
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Validator for Prompter {
|
||||
fn validate(&self, ctx: &mut ValidationContext) -> rustyline::Result<ValidationResult> {
|
||||
Ok(match self {
|
||||
Prompter::Revision(revisions) => {
|
||||
if ctx.input().is_empty() {
|
||||
if revisions.latest.is_empty() {
|
||||
ValidationResult::Invalid(None)
|
||||
} else {
|
||||
ValidationResult::Valid(Some(revisions.latest.clone()))
|
||||
}
|
||||
} else {
|
||||
ValidationResult::Valid(None)
|
||||
}
|
||||
}
|
||||
|
||||
Prompter::NonEmpty => {
|
||||
if ctx.input().is_empty() {
|
||||
ValidationResult::Invalid(None)
|
||||
} else {
|
||||
ValidationResult::Valid(None)
|
||||
}
|
||||
}
|
||||
|
||||
Prompter::Build(choices) => {
|
||||
let input = ctx.input();
|
||||
if input.is_empty() {
|
||||
ValidationResult::Valid(Some(choices[0].1.into()))
|
||||
} else if let Some(&(_, choice)) = input
|
||||
.parse::<usize>()
|
||||
.ok()
|
||||
.and_then(|choice| choices.get(choice))
|
||||
{
|
||||
ValidationResult::Valid(Some(choice.into()))
|
||||
} else {
|
||||
ValidationResult::Invalid(None)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prompt(prompt: impl Display) -> String {
|
||||
format!("{}\n{} ", prompt.bold(), "❯".blue())
|
||||
}
|
Loading…
Reference in New Issue
Block a user