Showing preview only (476K chars total). Download the full file or copy to clipboard to get everything.
Repository: mario-eth/soldeer
Branch: main
Commit: e4aac2865953
Files: 56
Total size: 455.1 KB
Directory structure:
gitextract_fq4q0vwy/
├── .config/
│ └── nextest.toml
├── .github/
│ ├── CODE_OF_CONDUCT.md
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml
│ │ ├── config.yml
│ │ ├── feature_request.yml
│ │ └── registry_request.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── dependabot.yml
│ └── workflows/
│ ├── release.yml
│ └── rust.yml
├── .gitignore
├── .vscode/
│ └── settings.json
├── CHANGELOG.md
├── CONTRIBUTING.md
├── Cargo.toml
├── LICENSE
├── README.md
├── USAGE.md
├── clippy.toml
├── crates/
│ ├── cli/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── main.rs
│ ├── commands/
│ │ ├── Cargo.toml
│ │ ├── src/
│ │ │ ├── commands/
│ │ │ │ ├── clean.rs
│ │ │ │ ├── init.rs
│ │ │ │ ├── install.rs
│ │ │ │ ├── login.rs
│ │ │ │ ├── mod.rs
│ │ │ │ ├── push.rs
│ │ │ │ ├── uninstall.rs
│ │ │ │ └── update.rs
│ │ │ ├── lib.rs
│ │ │ └── utils.rs
│ │ └── tests/
│ │ ├── tests-clean.rs
│ │ ├── tests-init.rs
│ │ ├── tests-install.rs
│ │ ├── tests-login.rs
│ │ ├── tests-push.rs
│ │ ├── tests-uninstall.rs
│ │ └── tests-update.rs
│ └── core/
│ ├── Cargo.toml
│ └── src/
│ ├── auth.rs
│ ├── config.rs
│ ├── download.rs
│ ├── errors.rs
│ ├── install.rs
│ ├── lib.rs
│ ├── lock/
│ │ └── forge.rs
│ ├── lock.rs
│ ├── push.rs
│ ├── registry.rs
│ ├── remappings.rs
│ ├── update.rs
│ └── utils.rs
├── flake.nix
├── release-plz.toml
└── rustfmt.toml
================================================
FILE CONTENTS
================================================
================================================
FILE: .config/nextest.toml
================================================
[profile.default]
retries = { backoff = "exponential", count = 2, delay = "2s", jitter = true }
slow-timeout = { period = "1m", terminate-after = 3 }
fail-fast = false
================================================
FILE: .github/CODE_OF_CONDUCT.md
================================================
The Soldeer project adheres to the
[Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct).
This code of conduct describes the minimum behavior expected from all contributors.
Instances of violations of the Code of Conduct can contact the project maintainers on the
[Contributors' Telegram Chat](https://t.me/+tn6gOCJseD83OTZk).
================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: 🐛 Bug Report
description: Report an issue found in Soldeer.
labels: ['bug']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to report a bug!
Please fill out the sections below to help us reproduce and fix the bug as quickly as possible.
- type: checkboxes
attributes:
label: 'I have checked the following:'
options:
- label: 'I have searched the issues of this repository and believe that this is not a duplicate.'
required: true
- label: 'I have checked that the bug is reproducible with the latest version of Soldeer.'
required: true
- type: input
id: version
attributes:
label: Soldeer Version
description: What is the result of running `soldeer version` or `forge soldeer version`
placeholder: soldeer x.y.z
validations:
required: true
- type: textarea
id: what-happened
attributes:
label: What Happened?
description: Describe the issue you are experiencing. You can run `soldeer` commands with the `-vvv` flag to see debug logs.
placeholder: A clear and concise description of what the bug is.
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected Behavior
description: Describe what you expected to happen.
placeholder: A clear and concise description of what you expected to happen in such a case.
validations:
required: false
- type: textarea
id: reproduction
attributes:
label: Reproduction Steps
description: Provide a detailed list of steps to reproduce the issue.
placeholder: |
1. Insert the "..." options into the config file
2. Run the command `...`
3. Observe that ... happens
validations:
required: false
- type: textarea
id: configuration
attributes:
label: Configuration
description: Provide the relevant sections of your `foundry.toml` or `soldeer.toml` file
render: toml
placeholder: |
[soldeer]
# Insert the relevant configuration options here
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: true
contact_links:
- name: Soldeer Contributors Telegram
url: https://t.me/+tn6gOCJseD83OTZk
about: Please ask and answer questions here.
================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: 💡 Feature Request
description: Suggest a feature for Soldeer
labels: ['enhancement']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to suggest a feature!
Please fill out the sections below to help us understand your request.
- type: checkboxes
attributes:
label: 'I have checked the following:'
options:
- label: 'I have searched the issues of this repository and believe that this is not a duplicate.'
required: true
- type: textarea
id: problem
attributes:
label: Problem
description: What problem are you facing that you believe this feature would solve?
placeholder: A clear and concise description of what the problem is.
validations:
required: true
- type: textarea
id: solution
attributes:
label: Solution
description: Describe the solution you'd like to see.
placeholder: A clear and concise description of what you want to happen.
validations:
required: true
- type: textarea
id: context
attributes:
label: Additional Context
description: Add any other context or screenshots about the feature request here.
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/registry_request.yml
================================================
name: 📦 Registry Addition
description: Suggest a missing package for the Soldeer registry.
labels: ['add-dependency']
assignees: ['mario-eth']
body:
- type: markdown
attributes:
value: |
Thanks for taking the time to suggest a package for the Soldeer registry!
Please fill out the sections below to help us understand your request.
- type: checkboxes
attributes:
label: 'I have checked the following:'
options:
- label: 'I have searched the issues of this repository and believe that this is not a duplicate.'
required: true
- type: input
id: package-name
attributes:
label: Package Name
description: What is the name of the package you would like to see added to the registry?
placeholder: soldeer-package-name
validations:
required: true
- type: input
id: project-url
attributes:
label: Project URL
description: Provide a link to the package repository or documentation.
placeholder: https://github.com/...
validations:
required: true
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any context to help us understand why this package should be added.
validations:
required: false
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
<!--
Before submitting a PR, please read https://github.com/mario-eth/soldeer/blob/main/CONTRIBUTING.md
1. Give the PR a descriptive title.
Examples of good title:
- fix(core): missing validation for ...
- docs(commands): update doc-comments for command ...
- feat(core): add option to ...
Examples of bad title:
- fix #7123
- update docs
- fix bugs
2. Ensure there is a related issue and it is referenced in the PR text ("Closes #123").
3. Ensure there are tests that cover the changes.
4. Ensure `cargo nextest run` passes.
5. Ensure code is formatted with `cargo +nightly fmt -- --check`.
6. Ensure `cargo +nightly clippy --all --all-targets --all-features -- -D warnings` passes.
7. Open as a draft PR if your work is still in progress.
-->
================================================
FILE: .github/dependabot.yml
================================================
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
# Check for updates every Monday
schedule:
interval: "weekly"
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
permissions:
pull-requests: write
contents: write
on:
push:
branches:
- main
jobs:
# Release unpublished packages.
release-plz-release:
name: Release-plz release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Generate GitHub token
uses: actions/create-github-app-token@v2
id: generate-token
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.generate-token.outputs.token }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: release-plz/action@v0.5
with:
command: release
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
# CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
# Create a PR with the new versions and changelog, preparing the next release.
release-plz-pr:
name: Release-plz PR
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
concurrency:
group: release-plz-${{ github.ref }}
cancel-in-progress: false
steps:
- name: Generate GitHub token
uses: actions/create-github-app-token@v2
id: generate-token
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.generate-token.outputs.token }}
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Run release-plz
uses: release-plz/action@v0.5
with:
command: release-pr
env:
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
# CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
================================================
FILE: .github/workflows/rust.yml
================================================
name: Rust
on:
push:
branches: ['main']
pull_request:
env:
CARGO_TERM_COLOR: always
jobs:
build-test:
strategy:
matrix:
platform: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@nextest
- name: Install Foundry
uses: foundry-rs/foundry-toolchain@v1
- name: Run tests
run: cargo nextest run
doctests:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- run: cargo test --workspace --doc
feature-checks:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cargo-hack
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- name: cargo hack
run: cargo hack check --feature-powerset --depth 2
clippy:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- run: cargo clippy --workspace --all-targets --all-features
env:
RUSTFLAGS: -Dwarnings
docs:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
- run: cargo doc --workspace --all-features --no-deps --document-private-items
env:
RUSTDOCFLAGS: '--cfg docsrs -D warnings'
fmt:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
with:
components: rustfmt
- run: cargo fmt --all --check
================================================
FILE: .gitignore
================================================
/target
dependencies/
.dependency_reading.toml
remappings.txt
crawler/target/
*.DS_Store*
package-lock.json
package.json
repositories.db
crawler/node_modules/
crawler/zipped/*
crawler/zipped/
src/soldeer.toml
*soldeer.lock
test/*
!emptyfile
!emptyfile2
test_push_sensitive
test_push_skip_sensitive
.soldeer/
================================================
FILE: .vscode/settings.json
================================================
{
"git.ignoreLimitWarning": true,
"editor.formatOnSave": true,
"rust-analyzer.rustfmt.extraArgs": ["+nightly"],
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
},
"rust-analyzer.cargo.features": "all"
}
================================================
FILE: CHANGELOG.md
================================================
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## `soldeer` - [0.11.0](https://github.com/mario-eth/soldeer/compare/v0.10.1...v0.11.0) - 2026-04-16
### Fixed
- *(commands)* do not init logging backend in the library crate ([#350](https://github.com/mario-eth/soldeer/pull/350))
## `soldeer-commands` - [0.11.0](https://github.com/mario-eth/soldeer/compare/soldeer-commands-v0.10.1...soldeer-commands-v0.11.0) - 2026-04-16
### Fixed
- *(commands)* do not init logging backend in the library crate ([#350](https://github.com/mario-eth/soldeer/pull/350))
### Other
- *(deps)* update dependencies ([#355](https://github.com/mario-eth/soldeer/pull/355))
## `soldeer-core` - [0.11.0](https://github.com/mario-eth/soldeer/compare/soldeer-core-v0.10.1...soldeer-core-v0.11.0) - 2026-04-16
### Other
- *(deps)* update dependencies ([#355](https://github.com/mario-eth/soldeer/pull/355))
- *(install)* concurrent subdependencies install ([#352](https://github.com/mario-eth/soldeer/pull/352))
## `soldeer-core` - [0.10.1](https://github.com/mario-eth/soldeer/compare/soldeer-core-v0.10.0...soldeer-core-v0.10.1) - 2026-02-16
### Added
- *(core)* support foundry.lock file ([#347](https://github.com/mario-eth/soldeer/pull/347))
## `soldeer-core` - [0.10.0](https://github.com/mario-eth/soldeer/compare/soldeer-core-v0.9.0...soldeer-core-v0.10.0) - 2025-12-03
### Added
- *(config)* [**breaking**] allow to specify the project root path for dependencies ([#341](https://github.com/mario-eth/soldeer/pull/341))
## `soldeer` - [0.9.0](https://github.com/mario-eth/soldeer/compare/v0.8.0...v0.9.0) - 2025-10-16
### Other
- update Cargo.lock dependencies
## `soldeer-commands` - [0.9.0](https://github.com/mario-eth/soldeer/compare/soldeer-commands-v0.8.0...soldeer-commands-v0.9.0) - 2025-10-16
### Added
- detect project root ([#333](https://github.com/mario-eth/soldeer/pull/333))
- *(commands)* add `soldeer clean` command ([#332](https://github.com/mario-eth/soldeer/pull/332))
## `soldeer-core` - [0.9.0](https://github.com/mario-eth/soldeer/compare/soldeer-core-v0.8.0...soldeer-core-v0.9.0) - 2025-10-16
### Added
- detect project root ([#333](https://github.com/mario-eth/soldeer/pull/333))
### Other
- *(deps)* update deps ([#336](https://github.com/mario-eth/soldeer/pull/336))
## `soldeer-commands` - [0.8.0](https://github.com/mario-eth/soldeer/compare/soldeer-commands-v0.7.1...soldeer-commands-v0.8.0) - 2025-09-29
### Added
- add support for private packages ([#327](https://github.com/mario-eth/soldeer/pull/327))
## `soldeer-core` - [0.8.0](https://github.com/mario-eth/soldeer/compare/soldeer-core-v0.7.1...soldeer-core-v0.8.0) - 2025-09-29
### Added
- add support for private packages ([#327](https://github.com/mario-eth/soldeer/pull/327))
## `soldeer-core` - [0.7.1](https://github.com/mario-eth/soldeer/compare/soldeer-core-v0.7.0...soldeer-core-v0.7.1) - 2025-09-19
### Fixed
- *(core)* install git submodules ([#328](https://github.com/mario-eth/soldeer/pull/328))
## `soldeer` - [0.7.0](https://github.com/mario-eth/soldeer/compare/v0.6.1...v0.7.0) - 2025-09-02
### Other
- rust edition 2024 ([#319](https://github.com/mario-eth/soldeer/pull/319))
## `soldeer-commands` - [0.7.0](https://github.com/mario-eth/soldeer/compare/soldeer-commands-v0.6.1...soldeer-commands-v0.7.0) - 2025-09-02
### Added
- *(registry)* use new API endpoints ([#318](https://github.com/mario-eth/soldeer/pull/318))
- add support for CLI tokens ([#311](https://github.com/mario-eth/soldeer/pull/311))
### Fixed
- *(cmd)* avoid panicking if logger was already initialized ([#312](https://github.com/mario-eth/soldeer/pull/312))
### Other
- rust edition 2024 ([#319](https://github.com/mario-eth/soldeer/pull/319))
## `soldeer-core` - [0.7.0](https://github.com/mario-eth/soldeer/compare/soldeer-core-v0.6.1...soldeer-core-v0.7.0) - 2025-09-02
### Added
- *(registry)* use new API endpoints ([#318](https://github.com/mario-eth/soldeer/pull/318))
- add support for CLI tokens ([#311](https://github.com/mario-eth/soldeer/pull/311))
### Fixed
- *(cmd)* avoid panicking if logger was already initialized ([#312](https://github.com/mario-eth/soldeer/pull/312))
### Other
- rust edition 2024 ([#319](https://github.com/mario-eth/soldeer/pull/319))
## `soldeer-core` - [0.6.1](https://github.com/mario-eth/soldeer/compare/soldeer-core-v0.6.0...soldeer-core-v0.6.1) - 2025-07-23
### Other
- add nix flake and fix clippy ([#301](https://github.com/mario-eth/soldeer/pull/301))
- remove bzip2 support ([#298](https://github.com/mario-eth/soldeer/pull/298))
## `soldeer` - [0.6.0](https://github.com/mario-eth/soldeer/compare/v0.5.4...v0.6.0) - 2025-07-10
### Other
- update Cargo.lock dependencies
## `soldeer-commands` - [0.6.0](https://github.com/mario-eth/soldeer/compare/soldeer-commands-v0.5.4...soldeer-commands-v0.6.0) - 2025-07-10
### Added
- *(commands)* if adding a dependency which is already present, re-install all ([#289](https://github.com/mario-eth/soldeer/pull/289))
### Fixed
- *(core)* recursive subdependencies install ([#288](https://github.com/mario-eth/soldeer/pull/288))
- *(commands)* canonicalize path in push command ([#284](https://github.com/mario-eth/soldeer/pull/284))
## `soldeer-core` - [0.6.0](https://github.com/mario-eth/soldeer/compare/soldeer-core-v0.5.4...soldeer-core-v0.6.0) - 2025-07-10
### Added
- *(core)* remove forge requirement for recursive install ([#281](https://github.com/mario-eth/soldeer/pull/281))
### Fixed
- *(core)* recursive subdependencies install ([#288](https://github.com/mario-eth/soldeer/pull/288))
- *(commands)* canonicalize path in push command ([#284](https://github.com/mario-eth/soldeer/pull/284))
## `soldeer` - [0.5.4](https://github.com/mario-eth/soldeer/compare/v0.5.3...v0.5.4) - 2025-04-27
### Other
- update Cargo.lock dependencies
## `soldeer-core` - [0.5.4](https://github.com/mario-eth/soldeer/compare/soldeer-core-v0.5.3...soldeer-core-v0.5.4) - 2025-04-27
### Fixed
- *(registry)* version resolution when no SemVer ([#271](https://github.com/mario-eth/soldeer/pull/271))
## `soldeer` - [0.5.3](https://github.com/mario-eth/soldeer/compare/v0.5.2...v0.5.3) - 2025-03-18
### Changed
- fix(core): remove hardcoded git domains by @puuuuh in https://github.com/mario-eth/soldeer/pull/244
- refactor!: logging by @beeb in https://github.com/mario-eth/soldeer/pull/242
- fix(push): ensure version is non-empty when pushing to registry by @kubkon in https://github.com/mario-eth/soldeer/pull/247
- feat!: improve toml validation by @beeb in https://github.com/mario-eth/soldeer/pull/248
- chore(deps): update deps by @beeb in https://github.com/mario-eth/soldeer/pull/257
## `soldeer` - [0.5.2](https://github.com/mario-eth/soldeer/compare/v0.5.1...v0.5.2) - 2024-11-21
### Changed
- fix(core): gitignore config for integrity checksum by @beeb in #233
## `soldeer` - [0.5.1](https://github.com/mario-eth/soldeer/compare/v0.5.0...v0.5.1) - 2024-11-13
### Changed
- fix(core): keep duplicate and orphan remappings by @beeb in #226
## `soldeer` - [0.5.0](https://github.com/mario-eth/soldeer/compare/v0.4.1...v0.5.0) - 2024-11-07
### Changed
- 185 add cli args to skip interaction for all commands by @mario-eth in #218
## `soldeer` - [0.4.1](https://github.com/mario-eth/soldeer/compare/v0.4.0...v0.4.1) - 2024-10-11
### Changed
- updated readme by @mario-eth in #209
- fix(core): all commands add the `[dependencies]` table in config if m… by @mario-eth in #214
- Add core version by @mario-eth in #210
## `soldeer` - [0.4.0](https://github.com/mario-eth/soldeer/compare/v0.3.4...v0.4.0) - 2024-10-07
### Changed
- refactor!: v0.4.0 main rewrite by @beeb in #150
- docs(core): document `auth` and `config` modules by @beeb in #175
- feat: format multiline remappings array by @beeb in #174
- docs(core): add documentation by @beeb in #177
- docs(core): add documentation by @beeb in #178
- docs(core): update and utils modules by @beeb in #179
- test(commands): init integration tests by @beeb in #180
- refactor!: minor refactor and integration tests by @beeb in #186
- test(commands): add integration test (install/uninstall) by @beeb in #190
- feat(core): improve remappings matching by @beeb in #191
- fix(core): updating git dependencies by @beeb in #192
- feat(commands): update libs in foundry config during init by @beeb in #193
- refactor: remove all unwraps by @beeb in #194
- ci: speed up test by using cargo-nextest by @beeb in #196
- perf: lock-free synchronization, add rayon by @crypdoughdoteth in #198
- feat(cli): add banner by @xyizko in #199
- refactor: use new syntax for bon builders by @beeb in #200
- ci: add nextest config by @beeb in #201
- test(commands): integration tests for push by @beeb in #197
- fix(core): `path_matches` semver comparison by @beeb in #205
- fix(cli): respect environment and tty preference for color by @beeb in #206
- test(commands): fix tests when run with `cargo test` by @beeb in #207
## `soldeer` - [0.3.4](https://github.com/mario-eth/soldeer/compare/v0.3.3...v0.3.4) - 2024-09-04
### Changed
- Moving the canonicalization to respect windows slashing by @mario-eth in #172
## `soldeer` - [0.3.3](https://github.com/mario-eth/soldeer/compare/v0.3.2...v0.3.3) - 2024-09-04
### Changed
- chore(deps): bump zip-extract to 0.2.0 by @DaniPopes in #161
- fix(config): preserve existing remappings by @beeb in #171
## `soldeer` - [0.3.2](https://github.com/mario-eth/soldeer/compare/v0.3.1...v0.3.2) - 2024-08-29
### Changed
- hotfix os independent bytes by @mario-eth in #163
- remappings_generated -> remappings_generate typo by @0xCalibur in #164
- fix(utils): always consider relative path in hashing by @beeb in #168
## `soldeer` - [0.3.1](https://github.com/mario-eth/soldeer/compare/v0.3.0...v0.3.1) - 2024-08-27
### Changed
- Hotfix on OS independent bytes on hashing
## `soldeer` - [0.3.0](https://github.com/mario-eth/soldeer/compare/v0.2.19...v0.3.0) - 2024-08-27
### Changed
- Updated readme and version by @mario-eth in #104
- 89 add soldeer uninstall by @mario-eth in #105
- Feat/soldeer init by @Solthodox in #56
- style(fmt): update formatter configuration and improve consistency by @beeb in #111
- refactor!: cleanup, more idiomatic rust by @beeb in #113
- perf(lock): better handling of missing lockfile by @beeb in #114
- refactor!: big rewrite by @beeb in #118
- fix(config)!: fix remappings logic and logging by @beeb in #125
- chore: update deps and remove serde_derive by @beeb in #129
- Handling dependency name sanitization by @mario-eth in #127
- fix: parallel downloads order by @beeb in #133
- Recursive Dependencies by @mario-eth in #136
- Removing transform git to http by @mario-eth in #137
- Hotfixes and extra tests before 0.3.0 by @mario-eth in #139
- Hotfixes after refactor and extra tests by @mario-eth in #141
- feat: add integrity checksum to lockfile by @beeb in #132
- chore: update logo by @beeb in #143
- chore: enable some more lints by @DaniPopes in #160
- chore(deps): replace simple-home-dir with home by @DaniPopes in #157
- chore: remove unused dev dep env_logger by @DaniPopes in #159
- chore(deps): replace `once_cell` with `std::sync` by @DaniPopes in #158
- Using git branch/tag to pull dependencies by @mario-eth in #147
================================================
FILE: CONTRIBUTING.md
================================================
## Contributing to Soldeer
Thanks for your interest in improving Soldeer!
There are multiple opportunities to contribute at any level. It doesn't matter if you are just getting started with Rust
or are the most weathered expert, we can use your help.
This document will help you get started. **Do not let the document intimidate you**.
It should be considered as a guide to help you navigate the process.
The [Contributors' Telegram Chat][telegram] is available for any concerns you may have that are
not covered in this guide.
### Code of Conduct
The Soldeer project adheres to the [Rust Code of Conduct][rust-coc]. This code of conduct describes the _minimum_
behavior expected from all contributors.
Instances of violations of the Code of Conduct can contact the project maintainers on the
[Contributors' Telegram Chat][telegram].
### Ways to contribute
There are fundamentally four ways an individual can contribute:
1. **By opening an issue:** For example, if you believe that you have uncovered a bug
in Soldeer, creating a new issue in the issue tracker is the way to report it.
2. **By adding context:** Providing additional context to existing issues,
such as screenshots and code snippets, which help resolve issues.
3. **By resolving issues:** Typically this is done in the form of either
demonstrating that the issue reported is not a problem after all, or more often,
by opening a pull request that fixes the underlying problem, in a concrete and
reviewable manner.
**Anybody can participate in any stage of contribution**. We urge you to participate in the discussion
around bugs and participate in reviewing PRs.
### Contributions Related to Spelling and Grammar
At this time, we will not be accepting contributions that only fix spelling or grammatical errors in documentation, code
or elsewhere.
### Asking for help
If you have reviewed existing documentation and still have questions, or you are having problems, you can get help in
the following ways:
- **Asking in the support Telegram:** The [Soldeer Support Telegram][telegram] is a fast and easy way to ask questions.
As Soldeer is still in heavy development, the documentation can be a bit scattered.
### Submitting a bug report
When filing a new bug report in the issue tracker, you will be presented with a basic form to fill out.
If you believe that you have uncovered a bug, please fill out the form to the best of your ability. Do not worry if you
cannot answer every detail; just fill in what you can. Contributors will ask follow-up questions if something is
unclear.
The most important pieces of information we need in a bug report are:
- The Soldeer version you are on (and that it is up to date)
- The platform you are on (Windows, macOS, an M1 Mac or Linux)
- Code snippets if this is happening in relation to testing or building code
- Concrete steps to reproduce the bug
In order to rule out the possibility of the bug being in your project, the code snippets should be as minimal
as possible. It is better if you can reproduce the bug with a small snippet as opposed to an entire project!
See [this guide][mcve] on how to create a minimal, complete, and verifiable example.
### Submitting a feature request
When adding a feature request in the issue tracker, you will be presented with a basic form to fill out.
Please include as detailed of an explanation as possible of the feature you would like, adding additional context if
necessary.
If you have examples of other tools that have the feature you are requesting, please include them as well.
### Resolving an issue
Pull requests are the way concrete changes are made to the code, documentation, and dependencies of Soldeer.
Please also make sure that the following commands pass if you have changed the code:
```sh
cargo check --all
cargo test --all --all-features
cargo +nightly fmt -- --check
cargo +nightly clippy --all --all-targets --all-features -- -D warnings
```
If you are working in VSCode, we recommend you install the [rust-analyzer](https://rust-analyzer.github.io/) extension,
and use the following VSCode user settings:
```json
"editor.formatOnSave": true,
"rust-analyzer.rustfmt.extraArgs": ["+nightly"],
"[rust]": {
"editor.defaultFormatter": "rust-lang.rust-analyzer"
}
```
#### Adding tests
If the change being proposed alters code, it is either adding new functionality to Soldeer, or fixing existing, broken
functionality.
In both of these cases, the pull request should include one or more tests to ensure that Soldeer does not regress
in the future.
Types of tests include:
- **Unit tests**: Functions which have very specific tasks should be unit tested.
- **Integration tests**: For general purpose, far reaching functionality, integration tests should be added.
The best way to add a new integration test is to look at existing ones and follow the style.
#### Commits
It is a recommended best practice to keep your changes as logically grouped as possible within individual commits. There
is no limit to the number of commits any single pull request may have, and many contributors find it easier to review
changes that are split across multiple commits.
That said, if you have a number of commits that are "checkpoints" and don't represent a single logical change, please
squash those together.
Please adhere to the [Conventional Commits][conventional-commits] format for commit messages
and PR titles. Prefer all-lowercase descriptions when possible.
The following types should be used:
- **build**: changes that affect the build system or external dependencies (example scope: cargo)
- **chore**: tool configuration, metadata, manifest changes, dependencies updates, miscellaneous changes (anything that doesn't fit the other types)
- **ci**: changes to the CI configuration files and scripts (GitHub Actions)
- **docs**: documentation-only changes (doc comments, mdbook)
- **feat**: a new feature
- **fix**: a bug fix
- **perf**: a code change that improves performance
- **refactor**: a code change that neither fixes a bug nor adds a feature
- **revert**: reverting an older commit or change
- **style**: changes that do not affect the meaning of the code (whitespace, formatting, etc.)
- **test**: adding or modifying tests (no change to lib/binary source code allowed)
#### Opening the pull request
From within GitHub, opening a new pull request will present you with a template that should be filled out. Please try
your best at filling out the details, but feel free to skip parts if you're not sure what to put.
Make sure to use the [Conventional Commits][conventional-commits] format described above for
your PR title.
#### Discuss and update
You will probably get feedback or requests for changes to your pull request.
This is a big part of the submission process, so don't be discouraged! Some contributors may sign off on the pull
request right away, others may have more detailed comments or feedback.
This is a necessary part of the process in order to evaluate whether the changes are correct and necessary.
**Any community member can review a PR, so you might get conflicting feedback**.
Keep an eye out for comments from code owners to provide guidance on conflicting feedback.
#### Reviewing pull requests
**Any Soldeer community member is welcome to review any pull request**.
All contributors who choose to review and provide feedback on pull requests have a responsibility to both the project
and individual making the contribution. Reviews and feedback must be helpful, insightful, and geared towards improving
the contribution as opposed to simply blocking it. If there are reasons why you feel the PR should not be merged,
explain what those are. Do not expect to be able to block a PR from advancing simply because you say "no" without
giving an explanation. Be open to having your mind changed. Be open to working _with_ the contributor to make the pull
request better.
Reviews that are dismissive or disrespectful of the contributor or any other reviewers are strictly counter to the Code
of Conduct.
When reviewing a pull request, the primary goals are for the codebase to improve and for the person submitting the
request to succeed.
**Even if a pull request is not merged, the submitter should come away from the experience feeling like their effort was not unappreciated**.
Every PR from a new contributor is an opportunity to grow the community.
##### Review a bit at a time
Do not overwhelm new contributors.
It is tempting to micro-optimize and make everything about relative performance, perfect grammar, or exact style
matches. Do not succumb to that temptation..
Focus first on the most significant aspects of the change:
1. Does this change make sense for Soldeer?
2. Does this change make Soldeer better, even if only incrementally?
3. Are there clear bugs or larger scale issues that need attending?
4. Are the commit messages readable and correct? If it contains a breaking change, is it clear enough?
Note that only **incremental** improvement is needed to land a PR. This means that the PR does not need to be perfect,
only better than the status quo. Follow-up PRs may be opened to continue iterating.
When changes are necessary, _request_ them, do not _demand_ them, and
**do not assume that the submitter already knows how to add a test or run a benchmark**.
Specific performance optimization techniques, coding styles and conventions change over time. The first impression you
give to a new contributor never does.
Nits (requests for small changes that are not essential) are fine, but try to avoid stalling the pull request. Most nits
can typically be fixed by the Soldeer maintainers merging the pull request, but they can also be an opportunity for the
contributor to learn a bit more about the project.
It is always good to clearly indicate nits when you comment, e.g.:
`Nit: change foo() to bar(). But this is not blocking`.
If your comments were addressed but were not folded after new commits, or if they proved to be mistaken, please,
[hide them][hiding-a-comment] with the appropriate reason to keep the conversation flow concise and relevant.
##### Be aware of the person behind the code
Be aware that _how_ you communicate requests and reviews in your feedback can have a significant impact on the success
of the pull request. Yes, we may merge a particular change that makes Soldeer better, but the individual might just not
want to have anything to do with Soldeer ever again. The goal is not just having good code.
##### Abandoned or stale pull requests
If a pull request appears to be abandoned or stalled, it is polite to first check with the contributor to see if they
intend to continue the work before checking if they would mind if you took it over (especially if it just has nits
left). When doing so, it is courteous to give the original contributor credit for the work they started, either by
preserving their name and e-mail address in the commit log, or by using the `Author: ` or `Co-authored-by: ` metadata
tag in the commits.
_Adapted from the [ethers-rs contributing guide](https://github.com/gakonst/ethers-rs/blob/master/CONTRIBUTING.md)_.
[telegram]: https://t.me/+tn6gOCJseD83OTZk
[rust-coc]: https://www.rust-lang.org/policies/code-of-conduct
[mcve]: https://stackoverflow.com/help/mcve
[hiding-a-comment]: https://help.github.com/articles/managing-disruptive-comments/#hiding-a-comment
[conventional-commits]: https://www.conventionalcommits.org/en/v1.0.0
================================================
FILE: Cargo.toml
================================================
[workspace]
members = ["crates/cli", "crates/core", "crates/commands"]
resolver = "2"
[workspace.package]
authors = ["m4rio"]
categories = ["development-tools"]
description = "A minimal Solidity package manager written in Rust, best used with Foundry"
edition = "2024"
exclude = ["tests/"]
homepage = "https://soldeer.xyz"
keywords = ["solidity", "package-manager", "foundry"]
license = "MIT"
readme = "./README.md"
repository = "https://github.com/mario-eth/soldeer"
rust-version = "1.88"
version = "0.11.0"
[workspace.lints.clippy]
dbg-macro = "warn"
manual-string-new = "warn"
uninlined-format-args = "warn"
use-self = "warn"
redundant-clone = "warn"
unwrap_used = "warn"
[workspace.lints.rust]
rust-2018-idioms = "warn"
unreachable-pub = "warn"
unused-must-use = "warn"
redundant-lifetimes = "warn"
[workspace.dependencies]
bon = "3.0.0"
clap = { version = "4.5.9", features = ["derive"] }
cliclack = "0.5.4"
derive_more = { version = "2.0.1", features = ["from", "display", "from_str"] }
log = { version = "0.4.25", features = ["kv"] }
mockito = "1.5.0"
path-slash = "0.2.1"
rayon = "1.10.0"
reqwest = "0.13.2"
temp-env = { version = "0.3.6", features = ["async_closure"] }
testdir = "0.10.0"
thiserror = "2.0.3"
tokio = { version = "1.38.0", features = [
"io-util",
"macros",
"process",
"rt-multi-thread",
] }
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2023 mario-eth
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# Soldeer ![Rust][rust-badge] [![License: MIT][license-badge]][license]
[rust-badge]: https://img.shields.io/badge/Built%20with%20-Rust-e43716.svg
[license]: https://opensource.org/licenses/MIT
[license-badge]: https://img.shields.io/badge/License-MIT-blue.svg
<p align="center">
<img src="https://github.com/mario-eth/soldeer/raw/main/logo/soldeer_logo_outline_512.png" />
</p>
Soldeer is a package manager for Solidity built in Rust and integrated into Foundry.
Solidity development started to become more and more complex. The need for a package manager was evident.
This project was started to solve the following issues:
- git submodules in Foundry are not a good solution for managing dependencies
- npmjs was built for the JS ecosystem, not for Solidity
- github versioning of the releases is a pain and not all the projects are using it correctly
## Installation (Foundry)
Soldeer is already integrated into Foundry. You can use it by running the following command:
```bash
forge soldeer [COMMAND]
```
To check which version of Soldeer is packaged with your Foundry install, run `forge soldeer version`.
## Installation (standalone)
Soldeer is available on [crates.io](https://crates.io/crates/soldeer) and can be installed with:
```bash
cargo install soldeer
```
### Verify installation
```bash
soldeer help
```
## Compile from Source
Clone this repository, then run `cargo build --release` inside the root.
The `soldeer` binary will be located inside the `target/release/` folder.
## Usage
Check out the [usage guide](https://github.com/mario-eth/soldeer/blob/main/USAGE.md) or
[Foundry Book](https://book.getfoundry.sh/projects/soldeer).
## Changelog
Please see the [changelog](https://github.com/mario-eth/soldeer/blob/main/CHANGES.md) for more information about each release.
## Contributing
See the [contribution guide](https://github.com/mario-eth/soldeer/blob/main/CONTRIBUTING.md) for more information.
================================================
FILE: USAGE.md
================================================
# Usage Guide
`Soldeer` is straightforward to use. It can either be invoked from the `forge` tool provided by Foundry, or installed as
a standalone executable named `soldeer`.
Dependencies and configuration options can be specified inside Foundry's `foundry.toml` config file, or inside a
dedicated `soldeer.toml` file.
In the following sections, commands can be prefixed with `forge` to use the built-in version packaged with Foundry.
## Initializing a New Project
```bash
[forge] soldeer init [--clean]
```
The `init` command can be used to setup a project for use with Soldeer. The command will generate or modify the
project's config file (`foundry.toml` or `soldeer.toml`) and perform optional removal of Foundry-style submodule
dependencies with the `--clean` flag.
This command automatically adds the latest `forge-std` dependency to your project.
Note that Soldeer installs dependencies into a folder named `dependencies`. There is currently no way to customize this
path.
## Adding Dependencies
### From the Soldeer Registry
```bash
[forge] soldeer install <NAME>~<VERSION>
```
This command searches the Soldeer registry at [https://soldeer.xyz](https://soldeer.xyz) for the specified dependency by
name and version. If a match is found, a ZIP file containing the package source will be downloaded and unzipped into the
`dependencies` directory.
The command also adds the dependency to the project's config file and creates the necessary
[remappings](https://book.getfoundry.sh/projects/dependencies#remapping-dependencies) if configured to do so.
#### Version Requirement
The `VERSION` argument is a version requirement string and can use operators and wildcards to match a range of versions.
By default, if no operator is provided, it defaults to `=` which means "exactly this version".
Examples:
```
1.2.3 // exactly 1.2.3, equivalent to `=1.2.3`
>=1.2.3 // any version greater than or equal to 1.2.3, including any 2.x version or more
^1.2.3 // the patch and minor version can increase, but not the major
1 // any version >=1.0.0 but <2.0.0
1.2 // any version >=1.2.0 but <2.0.0
~1.2.3 // only the patch number can increase
>1.2.3,<1.4.0 // multiple requirements can be separated by a comma
```
Note that this only makes sense when used with the Soldeer registry, as it provides a list of available versions to
select from. Dependencies specified with a custom URL do not use the version requirement string in this way.
### With a Custom URL
#### ZIP file
```bash
[forge] soldeer install <NAME>~<VERSION> --url <ZIP_URL>
```
If the URL to a ZIP file is provided, the registry is not used and the file is downloaded from the URL directly. Note
that a version must still be provided, but it can be freely chosen.
#### Git Repository
```bash
[forge] soldeer install <NAME>~<VERSION> --git <GIT_URL>
```
If the URL to a git repository is provided, then the repository will be cloned into the `dependencies` folder with the
`git` CLI available on the system. HTTPS and SSH-style URLs are supported (see examples below).
Cloning a specific identifier can be done with the `--rev <COMMIT>`, `--branch <BRANCH>` or `--tag <TAG>` arguments. If
omitted, then the default branch is checked out.
Some examples:
```bash
[forge] soldeer install test-project~v1 --git git@github.com:test/test.git
[forge] soldeer install test-project~v1 --git git@gitlab.com:test/test.git
```
```bash
[forge] soldeer install test-project~v1 --git https://github.com/test/test.git
[forge] soldeer install test-project~v1 --git https://gitlab.com/test/test.git
```
```bash
[forge] soldeer install test-project~v1 --git git@github.com:test/test.git --rev 345e611cd84bfb4e62c583fa1886c1928bc1a464
[forge] soldeer install test-project~v1 --git git@github.com:test/test.git --branch dev
[forge] soldeer install test-project~v1 --git git@github.com:test/test.git --tag v1
```
Note that a version must still be provided, but it can be freely chosen.
## Installing Existing Dependencies
```bash
[forge] soldeer install
```
When invoked without arguments, the `install` command installs the project's existing dependencies by looking at the
configuration file (`soldeer.toml`/`foundry.toml`) and lockfile `soldeer.lock` if present.
Dependencies which are already present inside the `dependencies` folder are not downloaded again. For dependencies with
a version range specified in the config file, the exact version that is written in the lockfile is used, even if a newer
version exists on the registry. To update the lockfile to use the latest supported version, use `soldeer update`.
### Recursive Installation
With the `--recursive-deps` flag, Soldeer will install the dependencies of each installed dependency, recursively. This
is done internally by running `git submodule update --init --recursive` and/or installing Soldeer dependencies inside of
the dependency's folder. This behavior can also be enabled permanently via the config file.
#### Specifying the Project Root for a Dependency
If recursive installation is enabled, Soldeer must find a `foundry.toml` or `soldeer.toml` config file within the
dependency's directory to know which subdependencies to install.
In case that config file is not located at the root of the dependency's directory (meaning at the root of a git
repository or at the root of the zip file), then the path to the folder containing that file must be specified with
`project_root`:
```toml
# foundry.toml
[dependencies]
mydep = { version = "1.0.0", project_root = "contracts" }
[soldeer]
recursive_deps = true
```
The path is a relative path, starting from the root of the dependency, to the folder containing the config file. You
should use forward slashes (`/`) as separator on all platforms.
#### Note on Sub-Dependencies
Since each dependency is free to use its own remappings, their resolution might become tricky in case of conflicting
versions.
For example:
We have a project called `my-project` with the following dependencies:
- `dependency~1`
- `openzeppelin~5.0.2` with remapping `@openzeppelin/contracts/=dependencies/openzeppelin-5.0.2/`
A contract inside `my-project` has the following import:
```solidity
@openzeppelin/contracts/token/ERC20/ERC20.sol
```
However, `dependency~1` also depends on `openzeppelin`, but it uses version `4.9.2` (with remapping
`@openzeppelin/contracts/=dependencies/openzeppelin-4.9.2/`). The contract inside `dependency-1` has the same import
path because they chose to use the same remappings path as `my-project`:
```solidity
@openzeppelin/contracts/token/ERC20/ERC20.sol
```
This situation creates ambiguity. Furthermore, if `dependency~1` were to import a file that is no longer present in
`v5`, the compiler would give an error.
As such, we recommend to always include the version requirement string as part of the remappings path. The version
requirement string does not need to target a specific version, but could e.g. target a major version:
```toml
[profile.default]
remappings = ["@openzeppelin-contracts-5/=dependencies/@openzeppelin-contracts-5.0.2/contracts/"]
[dependencies]
"@openzeppelin-contracts" = "5"
```
```solidity
import from '@openzeppelin-contracts-5/token/ERC20/ERC20.sol';
```
This approach should ensure that the correct version (or at least a compatible version) of the included file is used.
## Updating Dependencies
```bash
[forge] soldeer update
```
For dependencies from the online registry which specify a version range, the `update` command can be used to retrieve
the latest version that matches the requirements. The `soldeer.lock` lockfile is then updated accordingly. Remappings
are automatically updated to the new version if Soldeer is configured to generate remappings.
For git dependencies which specify no identifier or a branch identifier, the `update` command checks out the latest
commit on the default or specified branch.
## Removing a Dependency
```bash
[forge] soldeer uninstall <NAME>
```
The `uninstall` command removes the dependency files and entry into the config file, lockfile and remappings.
## Publishing a Package to the Repository
```bash
[forge] soldeer push <NAME>~<VERSION>
```
In order to push a new dependency to the repository, an account must first be created at
[https://soldeer.xyz](https://soldeer.xyz). Then, a project with the dependency name must be created through the
website.
Finally, the `[forge] soldeer login` command must be used to retrieve or provide an access token for the API. CLI tokens
can be generated on soldeer.xyz and should be preferred over using the email and password in the CLI, because email
login will be removed in a future version of Soldeer. Alternatively, you can provide a valid CLI token via the
`SOLDEER_API_TOKEN` environment variable.
Example:
Create a project called `my-project` and then use the `[forge] soldeer push my-project~1.0.0`. This will push the
project to the repository as version `1.0.0` and makes it available for anyone to use.
### Specifying a Path
```bash
[forge] soldeer push <NAME>~<VERSION> [PATH]
```
If the files to push are not located in the current directory, a path to the files can be provided.
### Ignoring Files
If you want to ignore certain files from the published package, you need to create one or more `.soldeerignore` files
that must contain the patterns that you want to ignore. These files can be at any level of your directory structure.
They use the `.gitignore` syntax.
Any file that matches a pattern present in `.gitignore` and `.ignore` files is also automatically excluded from the
published package.
### Dry Run
```bash
[forge] soldeer push <NAME>~<VERSION> --dry-run
```
With the `--dry-run` flag, the `push` command only creates a ZIP file containing the published package's content, but
does not upload it to the registry. The file can then be inspected to check that the contents is suitable.
We recommend that everyone runs a dry-run before pushing a new dependency to avoid publishing unwanted files.
**Warning** ⚠️
You are at risk to push sensitive files to the central repository that then can be seen by everyone. Make sure to
exclude sensitive files in the `.soldeerignore` or `.gitignore` file.
Furthermore, we've implemented a warning that gets triggered if the package contains any dotfile (a file with a name
starting with `.`). This warning can be ignored with `--skip-warnings`.
## Configuration
The `foundry.toml`/`soldeer.toml` file can have a `[soldeer]` section to configure the tool's behavior.
See the default configuration below:
```toml
[soldeer]
# whether Soldeer manages remappings
remappings_generate = true
# whether Soldeer re-generates all remappings when installing, updating or uninstalling deps
remappings_regenerate = false
# whether to suffix the remapping with the version requirement string: `name-a.b.c`
remappings_version = true
# a prefix to add to the remappings ("@" would give `@name`)
remappings_prefix = ""
# where to store the remappings ("txt" for `remappings.txt` or "config" for `foundry.toml`)
# ignored when `soldeer.toml` is used as config (uses `remappings.txt`)
remappings_location = "txt"
# whether to install sub-dependencies or not. If true this will install the dependencies of dependencies recursively.
recursive_deps = false
```
## List of Available Commands
For more commands and their usage, see `[forge] soldeer --help` and `[forge] soldeer <COMMAND> --help`.
## Remappings Caveats
If you use other dependency managers, such as git submodules or npm, ensure you don't duplicate dependencies between
soldeer and the other manager.
Remappings targeting dependencies installed without Soldeer are not modified or removed when using Soldeer commands,
unless the `--regenerate-remappings` flag is specified or the `remappings_regenerate = true` option is set.
## Dependencies Maintenance
The vision for Soldeer is that major projects such as OpenZeppelin, Solady, Uniswap would start publishing their own
packages to the Soldeer registry so that the community can easily include them and get timely updates.
Until this happens, the Soldeer maintenance team (currently m4rio.eth) will push the most popular dependencies to the
repository by relying on their npmjs or GitHub versions. We are using
[an open-source crawler tool](https://github.com/mario-eth/soldeer-crawler) to crawl and push the dependencies under the
`soldeer` organization.
For those who want an extra layer of security, the `soldeer.lock` file saves a `SHA-256` hash for each downloaded ZIP
file and the corresponding unzipped folder (see `soldeer_core::utils::hash_folder` to see how it gets generated). These
can be compared with the official releases to ensure the files were not manipulated.
**For Project Maintainers**
If you want to move your project from the Soldeer organization and take care of pushing the versions to Soldeer
yourself, please open an issue on GitHub or contact m4rio.eth on [X (formerly Twitter)](https://twitter.com/m4rio_eth).
================================================
FILE: clippy.toml
================================================
allow-unwrap-in-tests = true
================================================
FILE: crates/cli/Cargo.toml
================================================
[package]
name = "soldeer"
description.workspace = true
authors.workspace = true
categories.workspace = true
edition.workspace = true
exclude.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[lints]
workspace = true
[[bin]]
name = "soldeer"
path = "src/main.rs"
[dependencies]
env_logger = { version = "0.11.9", features = ["unstable-kv"] }
log.workspace = true
soldeer-commands = { path = "../commands", version = "0.11.0" }
tokio.workspace = true
yansi = { version = "1.0.1", features = ["detect-tty", "detect-env"] }
================================================
FILE: crates/cli/src/main.rs
================================================
//! Soldeer is a package manager for Solidity projects
use std::env;
use log::Level;
use soldeer_commands::{Args, commands::Parser as _, run};
use yansi::{Condition, Paint as _};
const HAVE_COLOR: Condition = Condition(|| {
std::env::var_os("NO_COLOR").is_none() &&
(Condition::CLICOLOR_LIVE)() &&
Condition::stdouterr_are_tty_live()
});
#[tokio::main]
async fn main() {
// disable colors if unsupported
yansi::whenever(HAVE_COLOR);
let args = Args::parse();
// setup logging
if env::var("RUST_LOG").is_ok() {
env_logger::builder().init();
} else if let Some(level) = args.verbose.log_level() &&
level > Level::Error
{
// the user requested structured logging (-v[v*])
// init logger
env_logger::Builder::new().filter_level(args.verbose.log_level_filter()).init();
}
if !args.verbose.is_present() {
banner();
}
if let Err(err) = run(args.command, args.verbose).await {
eprintln!("{}", err.to_string().red())
}
}
/// Generate and print a banner
fn banner() {
println!(
"{}",
format!(
"
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
╔═╗╔═╗╦ ╔╦╗╔═╗╔═╗╦═╗ Solidity Package Manager
╚═╗║ ║║ ║║║╣ ║╣ ╠╦╝
╚═╝╚═╝╩═╝═╩╝╚═╝╚═╝╩╚═ github.com/mario-eth/soldeer
v{} soldeer.xyz
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
",
env!("CARGO_PKG_VERSION")
)
.bright_cyan()
);
}
================================================
FILE: crates/commands/Cargo.toml
================================================
[package]
name = "soldeer-commands"
description = "High-level commands for the Soldeer CLI"
authors.workspace = true
categories.workspace = true
edition.workspace = true
exclude.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[lints]
workspace = true
[dependencies]
bon.workspace = true
clap.workspace = true
clap-verbosity-flag = "3.0.2"
cliclack.workspace = true
derive_more.workspace = true
email-address-parser = "2.0.0"
path-slash.workspace = true
rayon.workspace = true
soldeer-core = { path = "../core", version = "0.11.0" }
tokio.workspace = true
[dev-dependencies]
mockito.workspace = true
reqwest.workspace = true
temp-env.workspace = true
testdir.workspace = true
[features]
serde = ["soldeer-core/serde"]
================================================
FILE: crates/commands/src/commands/clean.rs
================================================
use crate::utils::success;
use clap::Parser;
use soldeer_core::{Result, config::Paths};
use std::fs;
/// Clean downloaded dependencies and generated artifacts
#[derive(Debug, Clone, Default, Parser, bon::Builder)]
#[builder(on(String, into))]
#[clap(after_help = "For more information, read the README.md")]
#[non_exhaustive]
pub struct Clean {
// No options for basic implementation
}
pub(crate) fn clean_command(paths: &Paths, _cmd: &Clean) -> Result<()> {
// Remove dependencies folder if it exists
if paths.dependencies.exists() {
fs::remove_dir_all(&paths.dependencies)?;
success!("Dependencies folder removed");
}
Ok(())
}
================================================
FILE: crates/commands/src/commands/init.rs
================================================
use crate::{
ConfigLocation,
utils::{Progress, remark, success},
};
use clap::Parser;
use soldeer_core::{
Result,
config::{Paths, add_to_config, read_soldeer_config, update_config_libs},
install::{InstallProgress, ensure_dependencies_dir, install_dependency},
lock::add_to_lockfile,
registry::get_latest_version,
remappings::{RemappingsAction, edit_remappings},
utils::remove_forge_lib,
};
use std::fs;
/// Convert a Foundry project to use Soldeer
#[derive(Debug, Clone, Default, Parser, bon::Builder)]
#[allow(clippy::duplicated_attributes)]
#[builder(on(String, into), on(ConfigLocation, into))]
#[clap(after_help = "For more information, read the README.md")]
#[non_exhaustive]
pub struct Init {
/// Clean the Foundry project by removing .gitmodules and the lib directory
#[arg(long, default_value_t = false)]
#[builder(default)]
pub clean: bool,
/// Specify the config location.
///
/// This prevents prompting the user if the automatic detection can't determine the config
/// location.
#[arg(long, value_enum)]
pub config_location: Option<ConfigLocation>,
}
pub(crate) async fn init_command(paths: &Paths, cmd: Init) -> Result<()> {
if cmd.clean {
remark!("Flag `--clean` was set, removing `lib` dir and submodules");
remove_forge_lib(&paths.root).await?;
}
let config = read_soldeer_config(&paths.config)?;
success!("Done reading config");
ensure_dependencies_dir(&paths.dependencies)?;
let dependency = get_latest_version("forge-std").await?;
let (progress, monitor) = InstallProgress::new();
let bars = Progress::new(format!("Installing {dependency}"), 1, monitor);
bars.start_all();
let lock = install_dependency(&dependency, None, &paths.dependencies, None, false, progress)
.await
.inspect_err(|e| {
bars.set_error(e);
})?;
bars.stop_all();
add_to_config(&dependency, &paths.config)?;
let foundry_config = paths.root.join("foundry.toml");
if foundry_config.exists() {
update_config_libs(foundry_config)?;
}
success!("Dependency added to config");
add_to_lockfile(lock, &paths.lock)?;
success!("Dependency added to lockfile");
edit_remappings(&RemappingsAction::Add(dependency), &config, paths)?;
success!("Dependency added to remappings");
let gitignore_path = paths.root.join(".gitignore");
if gitignore_path.exists() {
let mut gitignore = fs::read_to_string(&gitignore_path)?;
if !gitignore.contains("dependencies") {
gitignore.push_str("\n\n# Soldeer\n/dependencies\n");
fs::write(&gitignore_path, gitignore)?;
}
}
success!("Added `dependencies` to .gitignore");
Ok(())
}
================================================
FILE: crates/commands/src/commands/install.rs
================================================
use super::validate_dependency;
use crate::{
ConfigLocation,
utils::{Progress, remark, success, warning},
};
use clap::Parser;
use soldeer_core::{
Result,
config::{
Dependency, GitIdentifier, Paths, UrlType, add_to_config, read_config_deps,
read_soldeer_config,
},
errors::{InstallError, LockError},
install::{InstallProgress, ensure_dependencies_dir, install_dependencies, install_dependency},
lock::{add_to_lockfile, generate_lockfile_contents, read_lockfile},
remappings::{RemappingsAction, edit_remappings},
};
use std::fs;
/// Install a dependency
#[derive(Debug, Clone, Default, Parser, bon::Builder)]
#[allow(clippy::duplicated_attributes)]
#[builder(on(String, into), on(ConfigLocation, into))]
#[clap(
long_about = "Install a dependency
If used with arguments, a dependency will be added to the configuration. When used without argument, installs all dependencies that are missing.
Examples:
- Install all: soldeer install
- Add from registry: soldeer install lib_name~2.3.0
- Add with custom URL: soldeer install lib_name~2.3.0 --url https://foo.bar/lib.zip
- Add with git: soldeer install lib_name~2.3.0 --git git@github.com:foo/bar.git
- Add with git (commit): soldeer install lib_name~2.3.0 --git git@github.com:foo/bar.git --rev 05f218fb6617932e56bf5388c3b389c3028a7b73
- Add with git (tag): soldeer install lib_name~2.3.0 --git git@github.com:foo/bar.git --tag v2.3.0
- Add with git (branch): soldeer install lib_name~2.3.0 --git git@github.com:foo/bar.git --branch feature/baz",
after_help = "For more information, read the README.md"
)]
#[non_exhaustive]
pub struct Install {
/// The dependency name and version, separated by a tilde. The version is always required.
///
/// If not present, this command will install all dependencies which are missing.
#[arg(value_parser = validate_dependency, value_name = "DEPENDENCY~VERSION")]
pub dependency: Option<String>,
/// The URL to the dependency zip file.
///
/// Example: https://my-domain/dep.zip
#[arg(long = "url", requires = "dependency", conflicts_with = "git_url")]
pub zip_url: Option<String>,
/// The URL to the dependency repository.
///
/// Example: git@github.com:foo/bar.git
#[arg(long = "git", requires = "dependency", conflicts_with = "zip_url")]
pub git_url: Option<String>,
/// A Git commit hash
#[arg(long, group = "identifier", requires = "git_url")]
pub rev: Option<String>,
/// A Git tag
#[arg(long, group = "identifier", requires = "git_url")]
pub tag: Option<String>,
/// A Git branch
#[arg(long, group = "identifier", requires = "git_url")]
pub branch: Option<String>,
/// If set, this command will delete the existing remappings and re-create them
#[arg(short = 'g', long, default_value_t = false)]
#[builder(default)]
pub regenerate_remappings: bool,
/// If set, this command will install dependencies recursively (via git submodules or via
/// soldeer)
#[arg(short = 'd', long, default_value_t = false)]
#[builder(default)]
pub recursive_deps: bool,
/// Perform a clean install by re-installing all dependencies
#[arg(long, default_value_t = false)]
#[builder(default)]
pub clean: bool,
/// Specify the config location without prompting.
///
/// This prevents prompting the user if the automatic detection can't determine the config
/// location.
#[arg(long, value_enum)]
pub config_location: Option<ConfigLocation>,
}
pub(crate) async fn install_command(paths: &Paths, cmd: Install) -> Result<()> {
let mut config = read_soldeer_config(&paths.config)?;
if cmd.regenerate_remappings {
config.remappings_regenerate = true;
}
if cmd.recursive_deps {
config.recursive_deps = true;
}
success!("Done reading config");
ensure_dependencies_dir(&paths.dependencies)?;
let (dependencies, warnings) = read_config_deps(&paths.config)?;
for w in warnings {
warning!(format!("Config warning: {w}"));
}
match &cmd.dependency {
None => {
let lockfile = read_lockfile(&paths.lock)?;
success!("Done reading lockfile");
if cmd.clean {
remark!("Flag `--clean` was set, re-installing all dependencies");
fs::remove_dir_all(&paths.dependencies).map_err(|e| InstallError::IOError {
path: paths.dependencies.clone(),
source: e,
})?;
ensure_dependencies_dir(&paths.dependencies)?;
}
let (progress, monitor) = InstallProgress::new();
let bars = Progress::new("Installing dependencies", dependencies.len(), monitor);
bars.start_all();
let new_locks = install_dependencies(
&dependencies,
&lockfile.entries,
&paths.dependencies,
config.recursive_deps,
progress,
)
.await?;
bars.stop_all();
let new_lockfile_content = generate_lockfile_contents(new_locks);
if !lockfile.raw.is_empty() && new_lockfile_content != lockfile.raw {
warning!(
"Warning: the lock file is out of sync with the dependencies. Consider running `soldeer update` to re-generate the lockfile."
);
} else if lockfile.raw.is_empty() {
fs::write(&paths.lock, new_lockfile_content).map_err(LockError::IOError)?;
}
edit_remappings(&RemappingsAction::Update, &config, paths)?;
success!("Updated remappings");
}
Some(dependency) => {
let identifier = match (&cmd.rev, &cmd.branch, &cmd.tag) {
(Some(rev), None, None) => Some(GitIdentifier::from_rev(rev)),
(None, Some(branch), None) => Some(GitIdentifier::from_branch(branch)),
(None, None, Some(tag)) => Some(GitIdentifier::from_tag(tag)),
(None, None, None) => None,
_ => unreachable!("clap should prevent this"),
};
let url =
cmd.zip_url.as_ref().map(UrlType::http).or(cmd.git_url.as_ref().map(UrlType::git));
let mut dep = Dependency::from_name_version(dependency, url, identifier)?;
if dependencies
.iter()
.any(|d| d.name() == dep.name() && d.version_req() == dep.version_req())
{
remark!(format!("{dep} is already installed, running `install` instead"));
Box::pin(install_command(
paths,
Install::builder()
.regenerate_remappings(cmd.regenerate_remappings)
.recursive_deps(cmd.recursive_deps)
.clean(cmd.clean)
.maybe_config_location(cmd.config_location)
.build(),
))
.await?;
return Ok(());
}
let (progress, monitor) = InstallProgress::new();
let bars = Progress::new(format!("Installing {dep}"), 1, monitor);
bars.start_all();
let lock = install_dependency(
&dep,
None,
&paths.dependencies,
None,
config.recursive_deps,
progress,
)
.await?;
bars.stop_all();
// for git deps, we need to add the commit hash before adding them to the
// config, unless a branch/tag was specified
if let Some(git_dep) = dep.as_git_mut() &&
git_dep.identifier.is_none()
{
git_dep.identifier = Some(GitIdentifier::from_rev(
&lock.as_git().expect("lock entry should be of type git").rev,
));
}
add_to_config(&dep, &paths.config)?;
success!("Dependency added to config");
add_to_lockfile(lock, &paths.lock)?;
success!("Dependency added to lockfile");
edit_remappings(&RemappingsAction::Add(dep), &config, paths)?;
success!("Dependency added to remappings");
}
}
Ok(())
}
================================================
FILE: crates/commands/src/commands/login.rs
================================================
use crate::utils::{info, remark, step, success, warning};
use clap::Parser;
use email_address_parser::{EmailAddress, ParsingOptions};
use path_slash::PathBufExt as _;
use soldeer_core::{
Result,
auth::{Credentials, check_token, execute_login, save_token},
errors::AuthError,
};
use std::path::PathBuf;
/// Log into the central repository to push packages
///
/// The credentials are saved by default into ~/.soldeer.
/// If you want to overwrite that location, use the SOLDEER_LOGIN_FILE env var.
#[derive(Debug, Clone, Default, Parser, bon::Builder)]
#[builder(on(String, into))]
#[clap(after_help = "For more information, read the README.md")]
#[non_exhaustive]
pub struct Login {
/// Specify the email without prompting.
#[arg(long, conflicts_with = "token")]
pub email: Option<String>,
/// Specify the password without prompting.
#[arg(long, conflicts_with = "token")]
pub password: Option<String>,
/// Login with a token created via soldeer.xyz.
#[arg(long)]
pub token: Option<String>,
}
pub(crate) async fn login_command(cmd: Login) -> Result<()> {
remark!("If you do not have an account, please visit soldeer.xyz to create one.");
if let Some(token) = cmd.token {
let token = token.trim();
let username = check_token(token).await?;
let token_path = save_token(token)?;
info!(format!(
"Token is valid for user {username} and was saved in: {}",
PathBuf::from_slash_lossy(&token_path).to_string_lossy() /* normalize separators */
));
return Ok(());
}
warning!(
"The option to login via email and password will be removed in a future version of Soldeer. Please update your usage by either using `soldeer login --token [YOUR CLI TOKEN]` or passing the `SOLDEER_API_TOKEN` environment variable to the `push` command."
);
let email: String = match cmd.email {
Some(email) => {
if EmailAddress::parse(&email, Some(ParsingOptions::default())).is_none() {
return Err(AuthError::InvalidCredentials.into());
}
step!(format!("Email: {email}"));
email
}
None => {
if !crate::TUI_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
return Err(AuthError::TuiDisabled.into());
}
cliclack::input("Email address")
.validate(|input: &String| {
if input.is_empty() {
Err("Email is required")
} else {
match EmailAddress::parse(input, Some(ParsingOptions::default())) {
None => Err("Invalid email address"),
Some(_) => Ok(()),
}
}
})
.interact()?
}
};
let password = match cmd.password {
Some(pw) => pw,
None => {
if !crate::TUI_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
return Err(AuthError::TuiDisabled.into());
}
cliclack::password("Password").mask('▪').interact()?
}
};
let token_path = execute_login(&Credentials { email, password }).await?;
success!("Login successful");
info!(format!(
"Token saved in: {}",
PathBuf::from_slash_lossy(&token_path).to_string_lossy() /* normalize separators */
));
Ok(())
}
================================================
FILE: crates/commands/src/commands/mod.rs
================================================
pub use clap::{Parser, Subcommand};
use clap_verbosity_flag::{LogLevel, VerbosityFilter};
use derive_more::derive::From;
pub mod clean;
pub mod init;
pub mod install;
pub mod login;
pub mod push;
pub mod uninstall;
pub mod update;
#[derive(Copy, Clone, Debug, Default)]
pub struct CustomLevel;
impl LogLevel for CustomLevel {
fn default_filter() -> VerbosityFilter {
VerbosityFilter::Error
}
fn verbose_help() -> Option<&'static str> {
Some("Use structured logging and increase verbosity")
}
fn verbose_long_help() -> Option<&'static str> {
Some(
r#"Use structured logging and increase verbosity
Pass multiple times to increase the logging level (e.g. -v, -vv, -vvv).
If omitted, then a pretty TUI output will be used.
Otherwise:
- 1 (-v): print logs with level error and warning
- 2 (-vv): print logs with level info
- 3 (-vvv): print logs with level debug
- 4 (-vvvv): print logs with level trace
"#,
)
}
fn quiet_help() -> Option<&'static str> {
Some("Disable logs and output, or reduce verbosity")
}
}
/// A minimal Solidity dependency manager
#[derive(Parser, Debug, bon::Builder)]
#[clap(name = "soldeer", author = "m4rio.eth", version)]
#[non_exhaustive]
pub struct Args {
#[clap(subcommand)]
pub command: Command,
/// Test
#[command(flatten)]
pub verbose: clap_verbosity_flag::Verbosity<CustomLevel>,
}
/// The available commands for Soldeer
#[derive(Debug, Clone, Subcommand, From)]
#[non_exhaustive]
pub enum Command {
Init(init::Init),
Install(install::Install),
Update(update::Update),
Login(login::Login),
Push(push::Push),
Uninstall(uninstall::Uninstall),
Clean(clean::Clean),
Version(Version),
}
/// Display the version of Soldeer
#[derive(Debug, Clone, Default, Parser)]
#[non_exhaustive]
pub struct Version {}
fn validate_dependency(dep: &str) -> std::result::Result<String, String> {
if dep.split('~').count() != 2 {
return Err("The dependency should be in the format <DEPENDENCY>~<VERSION>".to_string());
}
Ok(dep.to_string())
}
================================================
FILE: crates/commands/src/commands/push.rs
================================================
use super::validate_dependency;
use crate::utils::{info, remark, success, warning};
use clap::Parser;
use soldeer_core::{
Result,
errors::PublishError,
push::{filter_ignored_files, push_version, validate_name, validate_version},
utils::{canonicalize_sync, check_dotfiles},
};
use std::{env, path::PathBuf, sync::atomic::Ordering};
/// Push a dependency to the repository
#[derive(Debug, Clone, Parser, bon::Builder)]
#[allow(clippy::duplicated_attributes)]
#[builder(on(String, into), on(PathBuf, into))]
#[clap(
long_about = "Push a dependency to the soldeer.xyz repository.
You need to be logged in first (soldeer login) or provide the `SOLDEER_API_TOKEN` environment variable with a valid
CLI token generated on soldeer.xyz.
Examples:
- Current directory: soldeer push mypkg~0.1.0
- Custom directory: soldeer push mypkg~0.1.0 /path/to/dep
- Dry run: soldeer push mypkg~0.1.0 --dry-run
To ignore certain files, create a `.soldeerignore` file in the root of the project and add the files you want to ignore. The `.soldeerignore` uses the same syntax as `.gitignore`.",
after_help = "For more information, read the README.md"
)]
#[non_exhaustive]
pub struct Push {
/// The dependency name and version, separated by a tilde.
///
/// This should always be used when you want to push a dependency to the central repository: `<https://soldeer.xyz>`.
#[arg(value_parser = validate_dependency, value_name = "DEPENDENCY>~<VERSION")]
pub dependency: String,
/// Use this if the package you want to push is not in the current directory.
///
/// Example: `soldeer push mypkg~0.1.0 /path/to/dep`.
pub path: Option<PathBuf>,
/// If set, does not publish the package but generates a zip file that can be inspected.
#[arg(short, long, default_value_t = false)]
#[builder(default)]
pub dry_run: bool,
/// Use this if you want to skip the warnings that can be triggered when trying to push
/// dotfiles like .env.
#[arg(long, default_value_t = false)]
#[builder(default)]
pub skip_warnings: bool,
}
pub(crate) async fn push_command(cmd: Push) -> Result<()> {
let path = cmd.path.unwrap_or(env::current_dir()?);
let path = canonicalize_sync(&path)?;
let files_to_copy: Vec<PathBuf> = filter_ignored_files(&path);
// Check for sensitive files or directories
if !cmd.dry_run &&
!cmd.skip_warnings &&
check_dotfiles(&files_to_copy) &&
!prompt_user_for_confirmation()?
{
return Err(PublishError::UserAborted.into());
}
if cmd.dry_run {
remark!("Running in dry-run mode, a zip file will be created for inspection");
}
if cmd.skip_warnings {
warning!("Sensitive file warnings are being ignored as requested");
}
let (dependency_name, dependency_version) =
cmd.dependency.split_once('~').expect("dependency string should have name and version");
validate_name(dependency_name)?;
validate_version(dependency_version)?;
if let Some(zip_path) =
push_version(dependency_name, dependency_version, path, &files_to_copy, cmd.dry_run).await?
{
info!(format!("Zip file created at {}", zip_path.to_string_lossy()));
} else {
success!("Pushed to repository!");
}
Ok(())
}
// Function to prompt the user for confirmation
fn prompt_user_for_confirmation() -> Result<bool> {
remark!("You are about to include some sensitive files in this version");
info!(
"If you are not sure which files will be included, you can run the command with `--dry-run`and inspect the generated zip file."
);
if crate::TUI_ENABLED.load(Ordering::Relaxed) {
cliclack::confirm("Do you want to continue?")
.interact()
.map_err(|e| PublishError::IOError { path: PathBuf::new(), source: e }.into())
} else {
Ok(true)
}
}
================================================
FILE: crates/commands/src/commands/uninstall.rs
================================================
use crate::utils::success;
use clap::Parser;
use soldeer_core::{
Result, SoldeerError,
config::{Paths, delete_from_config, read_soldeer_config},
download::delete_dependency_files_sync,
lock::remove_lock,
remappings::{RemappingsAction, edit_remappings},
};
/// Uninstall a dependency
#[derive(Debug, Clone, Parser, bon::Builder)]
#[builder(on(String, into))]
#[clap(after_help = "For more information, read the README.md")]
#[non_exhaustive]
pub struct Uninstall {
/// The dependency name. Specifying a version is not necessary.
pub dependency: String,
}
pub(crate) fn uninstall_command(paths: &Paths, cmd: &Uninstall) -> Result<()> {
let config = read_soldeer_config(&paths.config)?;
success!("Done reading config");
// delete from the config file and return the dependency
let dependency = delete_from_config(&cmd.dependency, &paths.config)?;
success!("Dependency removed from config file");
edit_remappings(&RemappingsAction::Remove(dependency.clone()), &config, paths)?;
success!("Dependency removed from remappings");
// deleting the files
delete_dependency_files_sync(&dependency, &paths.dependencies)
.map_err(|e| SoldeerError::DownloadError { dep: dependency.to_string(), source: e })?;
success!("Dependency removed from disk");
remove_lock(&dependency, &paths.lock)?;
success!("Dependency removed from lockfile");
Ok(())
}
================================================
FILE: crates/commands/src/commands/update.rs
================================================
use crate::{
ConfigLocation,
utils::{Progress, success, warning},
};
use clap::Parser;
use soldeer_core::{
Result,
config::{Paths, read_config_deps, read_soldeer_config},
errors::LockError,
install::{InstallProgress, ensure_dependencies_dir},
lock::{generate_lockfile_contents, read_lockfile},
remappings::{RemappingsAction, edit_remappings},
update::update_dependencies,
};
use std::fs;
/// Update dependencies by reading the config file
#[derive(Debug, Clone, Default, Parser, bon::Builder)]
#[allow(clippy::duplicated_attributes)]
#[builder(on(String, into), on(ConfigLocation, into))]
#[clap(after_help = "For more information, read the README.md")]
#[non_exhaustive]
pub struct Update {
/// If set, this command will delete the existing remappings and re-create them
#[arg(short = 'g', long, default_value_t = false)]
#[builder(default)]
pub regenerate_remappings: bool,
/// If set, this command will install the dependencies recursively (via submodules or via
/// soldeer)
#[arg(short = 'd', long, default_value_t = false)]
#[builder(default)]
pub recursive_deps: bool,
/// Specify the config location without prompting.
///
/// This prevents prompting the user if the automatic detection can't determine the config
/// location.
#[arg(long, value_enum)]
pub config_location: Option<ConfigLocation>,
}
// TODO: add a parameter for a dependency name, where we would only update that particular
// dependency
pub(crate) async fn update_command(paths: &Paths, cmd: Update) -> Result<()> {
let mut config = read_soldeer_config(&paths.config)?;
if cmd.regenerate_remappings {
config.remappings_regenerate = true;
}
if cmd.recursive_deps {
config.recursive_deps = true;
}
success!("Done reading config");
ensure_dependencies_dir(&paths.dependencies)?;
let (dependencies, warnings) = read_config_deps(&paths.config)?;
for w in warnings {
warning!(format!("Config warning: {w}"));
}
let lockfile = read_lockfile(&paths.lock)?;
success!("Done reading lockfile");
let (progress, monitor) = InstallProgress::new();
let bars = Progress::new("Updating dependencies", dependencies.len(), monitor);
bars.start_all();
let new_locks = update_dependencies(
&dependencies,
&lockfile.entries,
&paths.dependencies,
config.recursive_deps,
progress,
)
.await?;
bars.stop_all();
let new_lockfile_content = generate_lockfile_contents(new_locks);
fs::write(&paths.lock, new_lockfile_content).map_err(LockError::IOError)?;
success!("Updated lockfile");
edit_remappings(&RemappingsAction::Update, &config, paths)?;
success!("Updated remappings");
Ok(())
}
================================================
FILE: crates/commands/src/lib.rs
================================================
//! High-level commands for the Soldeer CLI
#![cfg_attr(docsrs, feature(doc_cfg))]
pub use crate::commands::{Args, Command};
use clap::builder::PossibleValue;
pub use clap_verbosity_flag::Verbosity;
use clap_verbosity_flag::log::Level;
use commands::CustomLevel;
use derive_more::derive::FromStr;
use soldeer_core::{Result, config::Paths};
use std::{
env,
path::PathBuf,
sync::atomic::{AtomicBool, Ordering},
};
use utils::{get_config_location, intro, outro, outro_cancel, step};
pub mod commands;
pub mod utils;
static TUI_ENABLED: AtomicBool = AtomicBool::new(true);
/// The location where the Soldeer config should be stored.
///
/// This is a new type so we can implement the `ValueEnum` trait for it.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, FromStr)]
pub struct ConfigLocation(soldeer_core::config::ConfigLocation);
impl clap::ValueEnum for ConfigLocation {
fn value_variants<'a>() -> &'a [Self] {
&[
Self(soldeer_core::config::ConfigLocation::Foundry),
Self(soldeer_core::config::ConfigLocation::Soldeer),
]
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
Some(match self.0 {
soldeer_core::config::ConfigLocation::Foundry => PossibleValue::new("foundry"),
soldeer_core::config::ConfigLocation::Soldeer => PossibleValue::new("soldeer"),
})
}
}
impl From<ConfigLocation> for soldeer_core::config::ConfigLocation {
fn from(value: ConfigLocation) -> Self {
value.0
}
}
impl From<soldeer_core::config::ConfigLocation> for ConfigLocation {
fn from(value: soldeer_core::config::ConfigLocation) -> Self {
Self(value)
}
}
pub async fn run(command: Command, verbosity: Verbosity<CustomLevel>) -> Result<()> {
if let Some(level) = verbosity.log_level() &&
level <= Level::Error &&
env::var("RUST_LOG").is_err()
{
// enable TUI if no `-v` flag and no RUST_LOG is provided
TUI_ENABLED.store(true, Ordering::Relaxed);
} else {
TUI_ENABLED.store(false, Ordering::Relaxed);
}
match command {
Command::Init(cmd) => {
intro!("🦌 Soldeer Init 🦌");
step!("Initialize Foundry project to use Soldeer");
// for init, we always use the current dir as root, unless specified by env
let root = env::var("SOLDEER_PROJECT_ROOT")
.ok()
.filter(|p| !p.is_empty())
.map_or(env::current_dir()?, PathBuf::from);
let paths = Paths::with_root_and_config(
&root,
Some(get_config_location(&root, cmd.config_location)?),
)?;
commands::init::init_command(&paths, cmd).await.inspect_err(|_| {
outro_cancel!("An error occurred during initialization");
})?;
outro!("Done initializing!");
}
Command::Install(cmd) => {
intro!("🦌 Soldeer Install 🦌");
let root = Paths::get_root_path();
let paths = Paths::with_root_and_config(
&root,
Some(get_config_location(&root, cmd.config_location)?),
)?;
commands::install::install_command(&paths, cmd).await.inspect_err(|_| {
outro_cancel!("An error occurred during install");
})?;
outro!("Done installing!");
}
Command::Update(cmd) => {
intro!("🦌 Soldeer Update 🦌");
let root = Paths::get_root_path();
let paths = Paths::with_root_and_config(
&root,
Some(get_config_location(&root, cmd.config_location)?),
)?;
commands::update::update_command(&paths, cmd).await.inspect_err(|_| {
outro_cancel!("An error occurred during the update");
})?;
outro!("Done updating!");
}
Command::Uninstall(cmd) => {
intro!("🦌 Soldeer Uninstall 🦌");
let root = Paths::get_root_path();
let paths =
Paths::with_root_and_config(&root, Some(get_config_location(&root, None)?))?;
commands::uninstall::uninstall_command(&paths, &cmd).inspect_err(|_| {
outro_cancel!("An error occurred during uninstall");
})?;
outro!("Done uninstalling!");
}
Command::Clean(cmd) => {
intro!("🦌 Soldeer Clean 🦌");
let root = Paths::get_root_path();
let paths =
Paths::with_root_and_config(&root, Some(get_config_location(&root, None)?))?;
commands::clean::clean_command(&paths, &cmd).inspect_err(|_| {
outro_cancel!("An error occurred during clean");
})?;
outro!("Done cleaning!");
}
Command::Login(cmd) => {
intro!("🦌 Soldeer Login 🦌");
commands::login::login_command(cmd).await.inspect_err(|_| {
outro_cancel!("An error occurred during login");
})?;
outro!("Done logging in!");
}
Command::Push(cmd) => {
intro!("🦌 Soldeer Push 🦌");
commands::push::push_command(cmd).await.inspect_err(|_| {
outro_cancel!("An error occurred during push");
})?;
outro!("Done!");
}
Command::Version(_) => {
const VERSION: &str = env!("CARGO_PKG_VERSION");
println!("soldeer {VERSION}");
}
}
Ok(())
}
================================================
FILE: crates/commands/src/utils.rs
================================================
#![allow(unused_macros)]
//! Utils for the commands crate
use std::{fmt, path::Path};
use crate::ConfigLocation;
use cliclack::{MultiProgress, ProgressBar, multi_progress, progress_bar, select};
use soldeer_core::{Result, config::detect_config_location, install::InstallMonitoring};
/// Template for the progress bars.
pub const PROGRESS_TEMPLATE: &str = "[{elapsed_precise}] {bar:30.magenta} ({pos}/{len}) {msg}";
/// A collection of progress bars for the installation/update process.
#[derive(Clone, Default)]
pub struct Progress {
multi: Option<MultiProgress>,
versions: Option<ProgressBar>,
downloads: Option<ProgressBar>,
unzip: Option<ProgressBar>,
subdependencies: Option<ProgressBar>,
integrity: Option<ProgressBar>,
}
impl Progress {
/// Create a new progress bar object.
///
/// A title and the total number of dependencies to install must be passed as an argument.
pub fn new(title: impl fmt::Display, total: usize, mut monitor: InstallMonitoring) -> Self {
if !crate::TUI_ENABLED.load(std::sync::atomic::Ordering::Relaxed) {
tokio::task::spawn(async move { while (monitor.logs.recv().await).is_some() {} });
tokio::task::spawn(async move { while (monitor.versions.recv().await).is_some() {} });
tokio::task::spawn(async move { while (monitor.downloads.recv().await).is_some() {} });
tokio::task::spawn(async move { while (monitor.unzip.recv().await).is_some() {} });
tokio::task::spawn(
async move { while (monitor.subdependencies.recv().await).is_some() {} },
);
tokio::task::spawn(async move { while (monitor.integrity.recv().await).is_some() {} });
return Self::default();
}
let multi = multi_progress(title);
let versions = multi.add(progress_bar(total as u64).with_template(PROGRESS_TEMPLATE));
let downloads = multi.add(progress_bar(total as u64).with_template(PROGRESS_TEMPLATE));
let unzip = multi.add(progress_bar(total as u64).with_template(PROGRESS_TEMPLATE));
let subdependencies =
multi.add(progress_bar(total as u64).with_template(PROGRESS_TEMPLATE));
let integrity = multi.add(progress_bar(total as u64).with_template(PROGRESS_TEMPLATE));
tokio::task::spawn({
let multi = multi.clone();
async move {
while let Some(log) = monitor.logs.recv().await {
multi.println(log);
}
}
});
tokio::task::spawn({
let versions = versions.clone();
async move {
while let Some(dep) = monitor.versions.recv().await {
versions.inc(1);
versions.set_message(format!("Got version for {dep}"));
}
}
});
tokio::task::spawn({
let downloads = downloads.clone();
async move {
while let Some(dep) = monitor.downloads.recv().await {
downloads.inc(1);
downloads.set_message(format!("Downloaded {dep}"));
}
}
});
tokio::task::spawn({
let unzip = unzip.clone();
async move {
while let Some(dep) = monitor.unzip.recv().await {
unzip.inc(1);
unzip.set_message(format!("Unzipped {dep}"));
}
}
});
tokio::task::spawn({
let subdependencies = subdependencies.clone();
async move {
while let Some(dep) = monitor.subdependencies.recv().await {
subdependencies.inc(1);
subdependencies.set_message(format!("Installed subdeps for {dep}"));
}
}
});
tokio::task::spawn({
let integrity = integrity.clone();
async move {
while let Some(dep) = monitor.integrity.recv().await {
integrity.inc(1);
integrity.set_message(format!("Checked integrity of {dep}"));
}
}
});
Self {
multi: Some(multi),
versions: Some(versions),
downloads: Some(downloads),
unzip: Some(unzip),
subdependencies: Some(subdependencies),
integrity: Some(integrity),
}
}
/// Start all progress bars.
pub fn start_all(&self) {
self.versions.as_ref().inspect(|p| p.start("Retrieving versions..."));
self.downloads.as_ref().inspect(|p| p.start("Downloading dependencies..."));
self.unzip.as_ref().inspect(|p| p.start("Unzipping dependencies..."));
self.subdependencies.as_ref().inspect(|p| p.start("Installing subdependencies..."));
self.integrity.as_ref().inspect(|p| p.start("Checking integrity..."));
}
/// Stop all progress bars.
pub fn stop_all(&self) {
self.versions.as_ref().inspect(|p| p.stop("Done retrieving versions"));
self.downloads.as_ref().inspect(|p| p.stop("Done downloading dependencies"));
self.unzip.as_ref().inspect(|p| p.stop("Done unzipping dependencies"));
self.subdependencies.as_ref().inspect(|p| p.stop("Done installing subdependencies"));
self.integrity.as_ref().inspect(|p| p.stop("Done checking integrity"));
self.multi.as_ref().inspect(|p| p.stop());
}
pub fn set_error(&self, error: impl fmt::Display) {
self.multi.as_ref().inspect(|m| m.error(error));
}
}
/// Auto-detect config location or prompt the user for preference.
pub fn get_config_location(
root: impl AsRef<Path>,
arg: Option<ConfigLocation>,
) -> Result<soldeer_core::config::ConfigLocation> {
Ok(match arg {
Some(loc) => loc.into(),
None => match detect_config_location(root) {
Some(loc) => loc,
None => prompt_config_location()?.into(),
},
})
}
/// Prompt the user for their desired config location in case it cannot be auto-detected.
pub fn prompt_config_location() -> Result<ConfigLocation> {
Ok(select("Select how you want to configure Soldeer")
.initial_value("foundry")
.item("foundry", "Using foundry.toml", "recommended")
.item("soldeer", "Using soldeer.toml", "for non-foundry projects")
.interact()?
.parse()
.expect("all options should be valid variants of the ConfigLocation enum"))
}
macro_rules! define_cliclack_macro {
($name:ident, $path:path) => {
macro_rules! $name {
($expression:expr) => {
if $crate::TUI_ENABLED.load(::std::sync::atomic::Ordering::Relaxed) {
$path($expression).ok();
}
};
}
};
}
define_cliclack_macro!(intro, ::cliclack::intro);
define_cliclack_macro!(note, ::cliclack::note);
define_cliclack_macro!(outro, ::cliclack::outro);
define_cliclack_macro!(outro_cancel, ::cliclack::outro_cancel);
define_cliclack_macro!(outro_note, ::cliclack::outro_note);
define_cliclack_macro!(error, ::cliclack::log::error);
define_cliclack_macro!(info, ::cliclack::log::info);
define_cliclack_macro!(remark, ::cliclack::log::remark);
define_cliclack_macro!(step, ::cliclack::log::step);
define_cliclack_macro!(success, ::cliclack::log::success);
define_cliclack_macro!(warning, ::cliclack::log::warning);
#[allow(unused_imports)]
pub(crate) use error;
pub(crate) use info;
pub(crate) use intro;
#[allow(unused_imports)]
pub(crate) use note;
pub(crate) use outro;
pub(crate) use outro_cancel;
#[allow(unused_imports)]
pub(crate) use outro_note;
pub(crate) use remark;
pub(crate) use step;
pub(crate) use success;
pub(crate) use warning;
================================================
FILE: crates/commands/tests/tests-clean.rs
================================================
use soldeer_commands::{
Command, Verbosity,
commands::{clean::Clean, install::Install},
run,
};
use soldeer_core::{
config::read_config_deps,
lock::{SOLDEER_LOCK, read_lockfile},
};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::{
fs,
path::{Path, PathBuf},
};
use temp_env::async_with_vars;
use testdir::testdir;
#[allow(clippy::unwrap_used)]
fn check_clean_success(dir: &Path, config_filename: &str) {
assert!(!dir.join("dependencies").exists(), "Dependencies folder should be removed");
let config_path = dir.join(config_filename);
assert!(config_path.exists(), "Config file should be preserved");
let (deps, _) = read_config_deps(&config_path).unwrap();
assert_eq!(deps.len(), 2, "Config should still have 2 dependencies");
assert_eq!(deps[0].name(), "@openzeppelin-contracts");
assert_eq!(deps[1].name(), "solady");
}
#[allow(clippy::unwrap_used)]
fn check_artifacts_exist(dir: &Path) {
assert!(dir.join("dependencies").exists(), "Dependencies folder should exist");
assert!(dir.join(SOLDEER_LOCK).exists(), "Lock file should exist");
let lock = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
assert_eq!(lock.entries.len(), 2, "Lock file should have 2 entries");
let deps_dir = dir.join("dependencies");
let entries: Vec<_> = fs::read_dir(&deps_dir).unwrap().collect::<Result<Vec<_>, _>>().unwrap();
assert!(!entries.is_empty(), "Dependencies directory should have content");
}
#[allow(clippy::unwrap_used)]
async fn setup_project_with_dependencies(config_filename: &str) -> PathBuf {
let dir = testdir!();
let mut contents = r#"[dependencies]
"@openzeppelin-contracts" = "5.0.2"
solady = "0.0.238"
"#
.to_string();
if config_filename == "foundry.toml" {
contents = format!(
r#"[profile.default]
libs = ["dependencies"]
{contents}"#
);
}
fs::write(dir.join(config_filename), contents).unwrap();
let cmd: Command = Install::default().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
dir
}
#[tokio::test]
async fn test_clean_basic() {
let dir = setup_project_with_dependencies("soldeer.toml").await;
assert!(dir.join("dependencies").exists());
let cmd: Command = Clean::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_clean_success(&dir, "soldeer.toml");
}
#[tokio::test]
async fn test_clean_foundry_config() {
let dir = setup_project_with_dependencies("foundry.toml").await;
check_artifacts_exist(&dir);
let cmd: Command = Clean::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_clean_success(&dir, "foundry.toml");
}
#[tokio::test]
async fn test_clean_no_artifacts() {
let dir = testdir!();
fs::write(dir.join("soldeer.toml"), "[dependencies]\n").unwrap();
// Run clean on empty project (no dependencies folder or lock file)
let cmd: Command = Clean::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
// Should succeed silently
assert!(res.is_ok(), "{res:?}");
}
#[tokio::test]
async fn test_clean_restores_with_install() {
let dir = setup_project_with_dependencies("soldeer.toml").await;
let cmd: Command = Clean::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
assert!(!dir.join("dependencies").exists());
assert!(dir.join(SOLDEER_LOCK).exists(), "Lock file should remain after clean");
let cmd: Command = Install::default().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
assert!(dir.join("dependencies").exists());
let dependencies_dir = dir.join("dependencies");
let entries: Vec<_> =
fs::read_dir(dependencies_dir).unwrap().collect::<Result<Vec<_>, _>>().unwrap();
assert!(!entries.is_empty(), "Dependencies should be installed");
}
#[tokio::test]
async fn test_clean_with_complex_file_structure() {
let dir = setup_project_with_dependencies("soldeer.toml").await;
let complex_path = dir.join("dependencies").join("nested").join("deep").join("structure");
fs::create_dir_all(&complex_path).unwrap();
fs::write(complex_path.join("test.txt"), "nested content").unwrap();
// Create symlink (Unix only)
#[cfg(unix)]
{
use std::os::unix::fs::symlink;
let _ = symlink(dir.join("soldeer.toml"), dir.join("dependencies").join("config_link"));
}
// Create large file to test performance
let large_content = "x".repeat(1024 * 1024); // 1MB
fs::write(dir.join("dependencies").join("large_file.txt"), large_content).unwrap();
let cmd: Command = Clean::builder().build().into();
let res: Result<(), soldeer_core::SoldeerError> = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_clean_success(&dir, "soldeer.toml");
}
#[tokio::test]
async fn test_clean_permission_error() {
let dir = setup_project_with_dependencies("soldeer.toml").await;
#[cfg(unix)]
{
let deps_path = dir.join("dependencies");
let mut perms = fs::metadata(&deps_path).unwrap().permissions();
perms.set_mode(0o444); // Read-only
fs::set_permissions(&deps_path, perms).unwrap();
let cmd: Command = Clean::builder().build().into();
let res: Result<(), soldeer_core::SoldeerError> = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
// Should fail due to permission error
assert!(res.is_err(), "Clean should fail with permission error");
let mut perms = fs::metadata(&deps_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&deps_path, perms).unwrap();
}
#[cfg(not(unix))]
{
// On non-Unix systems, just run a successful clean
let cmd: Command = Clean::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
}
}
#[tokio::test]
async fn test_clean_with_soldeer_config_variations() {
let dir = testdir!();
let contents = r#"[soldeer]
remappings_generate = false
remappings_regenerate = true
remappings_location = "config"
[dependencies]
"@openzeppelin-contracts" = "5.0.2"
solady = "0.0.238"
"#;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let cmd: Command = Install::default().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_artifacts_exist(&dir);
let cmd: Command = Clean::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_clean_success(&dir, "soldeer.toml");
// Verify custom config is preserved
let config_content = fs::read_to_string(dir.join("soldeer.toml")).unwrap();
assert!(config_content.contains("remappings_generate = false"));
assert!(config_content.contains("remappings_location = \"config\""));
}
#[tokio::test]
async fn test_clean_multiple_times() {
let dir = setup_project_with_dependencies("soldeer.toml").await;
let cmd: Command = Clean::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let cmd: Command = Clean::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let cmd: Command = Clean::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
// Verify final state
check_clean_success(&dir, "soldeer.toml");
}
================================================
FILE: crates/commands/tests/tests-init.rs
================================================
use soldeer_commands::{Command, Verbosity, commands::init::Init, run};
use soldeer_core::{
config::{ConfigLocation, read_config_deps},
lock::{SOLDEER_LOCK, read_lockfile},
registry::get_latest_version,
utils::run_git_command,
};
use std::fs;
use temp_env::async_with_vars;
use testdir::testdir;
#[tokio::test]
async fn test_init_clean() {
let dir = testdir!();
run_git_command(
["clone", "--recursive", "https://github.com/foundry-rs/forge-template.git", "."],
Some(&dir),
)
.await
.unwrap();
fs::write(dir.join("soldeer.toml"), "[dependencies]\n").unwrap();
let cmd: Command =
Init::builder().clean(true).config_location(ConfigLocation::Soldeer).build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
assert!(!dir.join("lib").exists());
assert!(!dir.join(".gitmodules").exists());
assert!(dir.join("dependencies").exists());
let (deps, _) = read_config_deps(dir.join("soldeer.toml")).unwrap();
assert_eq!(deps.first().unwrap().name(), "forge-std");
let lock = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
assert_eq!(lock.entries.first().unwrap().name(), "forge-std");
let remappings = fs::read_to_string(dir.join("remappings.txt")).unwrap();
assert!(remappings.contains("forge-std"));
let gitignore = fs::read_to_string(dir.join(".gitignore")).unwrap();
assert!(gitignore.contains("/dependencies"));
let foundry_config = fs::read_to_string(dir.join("foundry.toml")).unwrap();
assert!(foundry_config.contains("libs = [\"dependencies\"]"));
}
#[tokio::test]
async fn test_init_no_clean() {
let dir = testdir!();
run_git_command(
["clone", "--recursive", "https://github.com/foundry-rs/forge-template.git", "."],
Some(&dir),
)
.await
.unwrap();
fs::write(dir.join("soldeer.toml"), "[dependencies]\n").unwrap();
let cmd: Command = Init::builder().config_location(ConfigLocation::Soldeer).build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
assert!(dir.join("lib").exists());
assert!(dir.join(".gitmodules").exists());
assert!(dir.join("dependencies").exists());
let (deps, _) = read_config_deps(dir.join("soldeer.toml")).unwrap();
assert_eq!(deps.first().unwrap().name(), "forge-std");
let lock = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
assert_eq!(lock.entries.first().unwrap().name(), "forge-std");
let remappings = fs::read_to_string(dir.join("remappings.txt")).unwrap();
assert!(remappings.contains("forge-std"));
let gitignore = fs::read_to_string(dir.join(".gitignore")).unwrap();
assert!(gitignore.contains("/dependencies"));
let foundry_config = fs::read_to_string(dir.join("foundry.toml")).unwrap();
assert!(foundry_config.contains("libs = [\"dependencies\"]"));
}
#[tokio::test]
async fn test_init_no_remappings() {
let dir = testdir!();
run_git_command(
["clone", "--recursive", "https://github.com/foundry-rs/forge-template.git", "."],
Some(&dir),
)
.await
.unwrap();
let contents = r"[soldeer]
remappings_generate = false
[dependencies]
";
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let cmd: Command =
Init::builder().clean(true).config_location(ConfigLocation::Soldeer).build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
assert!(!dir.join("remappings.txt").exists());
}
#[tokio::test]
async fn test_init_no_gitignore() {
let dir = testdir!();
run_git_command(
["clone", "--recursive", "https://github.com/foundry-rs/forge-template.git", "."],
Some(&dir),
)
.await
.unwrap();
fs::remove_file(dir.join(".gitignore")).unwrap();
fs::write(dir.join("soldeer.toml"), "[dependencies]\n").unwrap();
let cmd: Command =
Init::builder().clean(true).config_location(ConfigLocation::Soldeer).build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
assert!(!dir.join(".gitignore").exists());
}
#[tokio::test]
async fn test_init_select_foundry_location() {
let dir = testdir!();
let cmd: Command =
Init::builder().clean(true).config_location(ConfigLocation::Foundry).build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let forge_std = get_latest_version("forge-std").await.unwrap();
let config_path = dir.join("foundry.toml");
assert!(config_path.exists());
let contents = format!(
r#"[profile.default]
src = "src"
out = "out"
libs = ["dependencies"]
[dependencies]
forge-std = "{}"
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
"#,
forge_std.version_req()
);
assert_eq!(fs::read_to_string(config_path).unwrap(), contents);
}
#[tokio::test]
async fn test_init_select_soldeer_location() {
let dir = testdir!();
let cmd: Command =
Init::builder().clean(true).config_location(ConfigLocation::Soldeer).build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let forge_std = get_latest_version("forge-std").await.unwrap();
let config_path = dir.join("soldeer.toml");
assert!(config_path.exists());
let contents = format!(
r#"[dependencies]
forge-std = "{}"
"#,
forge_std.version_req()
);
assert_eq!(fs::read_to_string(config_path).unwrap(), contents);
}
================================================
FILE: crates/commands/tests/tests-install.rs
================================================
#![allow(clippy::unwrap_used)]
use mockito::Matcher;
use soldeer_commands::{Command, Verbosity, commands::install::Install, run};
use soldeer_core::{
SoldeerError,
config::{ConfigLocation, read_config_deps},
download::download_file,
errors::InstallError,
lock::{SOLDEER_LOCK, read_lockfile},
push::zip_file,
utils::hash_file,
};
use std::{
fs::{self},
path::{Path, PathBuf},
};
use temp_env::async_with_vars;
use testdir::testdir;
fn check_install(dir: &Path, name: &str, version_req: &str) {
assert!(dir.join("dependencies").exists());
let mut config_path = dir.join("soldeer.toml");
if !config_path.exists() {
config_path = dir.join("foundry.toml");
}
let (deps, _) = read_config_deps(config_path).unwrap();
assert_eq!(deps.first().unwrap().name(), name);
let remappings = fs::read_to_string(dir.join("remappings.txt")).unwrap();
assert!(remappings.contains(name));
let lock = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
assert_eq!(lock.entries.first().unwrap().name(), name);
let version = lock.entries.first().unwrap().version();
assert!(version.starts_with(version_req));
assert!(dir.join("dependencies").join(format!("{name}-{version}")).exists());
}
fn create_zip_monorepo(testdir: &Path) -> PathBuf {
let root = testdir.join("monorepo");
fs::create_dir(&root).unwrap();
let contracts = root.join("contracts");
fs::create_dir(&contracts).unwrap();
let mut files = Vec::new();
files.push(root.join("README.md"));
fs::write(
files.last().unwrap(),
"Root of the repo is here, foundry project is under `contracts`",
)
.unwrap();
files.push(contracts.join("foundry.toml"));
fs::write(
files.last().unwrap(),
r#"[profile.default]
libs = ["dependencies"]
remappings = ["forge-std/=dependencies/forge-std-1.11.0/src/"]
[dependencies]
forge-std = "1.11.0"
[soldeer]
remappings_location = "config"
recursive_deps = true"#,
)
.unwrap();
zip_file(&root, &files, "test").unwrap() // zip is inside the `monorepo` folder
}
fn create_zip_with_foundry_lock(testdir: &Path, branch: Option<&str>) -> PathBuf {
let root = testdir.join("foundry_lock_project");
fs::create_dir(&root).unwrap();
let lib = root.join("lib");
fs::create_dir(&lib).unwrap();
let mut files = Vec::new();
files.push(root.join("foundry.toml"));
fs::write(
files.last().unwrap(),
r#"[profile.default]
src = "src"
out = "out"
libs = ["lib"]
"#,
)
.unwrap();
files.push(root.join(".gitmodules"));
let gitmodules_content = if let Some(branch) = branch {
format!(
r#"[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
branch = {branch}
"#
)
} else {
r#"[submodule "lib/forge-std"]
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
"#
.to_string()
};
fs::write(files.last().unwrap(), gitmodules_content).unwrap();
files.push(root.join("foundry.lock"));
let foundry_lock_content = if let Some(branch) = branch {
format!(
r#"{{
"lib/forge-std": {{
"branch": {{
"name": "{branch}",
"rev": "c29afdd40a82db50a3d3709d324416be50050e5e"
}}
}}
}}"#
)
} else {
r#"{
"lib/forge-std": {
"rev": "c29afdd40a82db50a3d3709d324416be50050e5e"
}
}"#
.to_string()
};
fs::write(files.last().unwrap(), foundry_lock_content).unwrap();
zip_file(&root, &files, "test").unwrap()
}
#[tokio::test]
async fn test_install_registry_any_version() {
let dir = testdir!();
fs::write(dir.join("soldeer.toml"), "[dependencies]\n").unwrap();
let cmd: Command = Install::builder().dependency("@openzeppelin-contracts~5").build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_install(&dir, "@openzeppelin-contracts", "5");
}
#[tokio::test]
async fn test_install_registry_wildcard() {
let dir = testdir!();
fs::write(dir.join("soldeer.toml"), "[dependencies]\n").unwrap();
let cmd: Command = Install::builder().dependency("solady~*").build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_install(&dir, "solady", "");
}
#[tokio::test]
async fn test_install_registry_specific_version() {
let dir = testdir!();
fs::write(dir.join("soldeer.toml"), "[dependencies]\n").unwrap();
let cmd: Command =
Install::builder().dependency("@openzeppelin-contracts~4.9.5").build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_install(&dir, "@openzeppelin-contracts", "4.9.5");
}
#[tokio::test]
async fn test_install_custom_http() {
let dir = testdir!();
fs::write(dir.join("soldeer.toml"), "[dependencies]\n").unwrap();
let cmd: Command = Install::builder().dependency("mylib~1.0.0")
.zip_url("https://github.com/mario-eth/soldeer/archive/8585a7ec85a29889cec8d08f4770e15ec4795943.zip")
.build()
.into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_install(&dir, "mylib", "1.0.0");
let lock = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
assert_eq!(
lock.entries.first().unwrap().as_http().unwrap().url,
"https://github.com/mario-eth/soldeer/archive/8585a7ec85a29889cec8d08f4770e15ec4795943.zip"
);
assert!(&dir.join("dependencies").join("mylib-1.0.0").join("README.md").exists());
}
#[tokio::test]
async fn test_install_git_main() {
let dir = testdir!();
fs::write(dir.join("soldeer.toml"), "[dependencies]\n").unwrap();
let cmd: Command = Install::builder()
.dependency("mylib~0.1.0")
.git_url("https://github.com/beeb/test-repo.git")
.build()
.into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_install(&dir, "mylib", "0.1.0");
let lock = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
assert_eq!(
lock.entries.first().unwrap().as_git().unwrap().rev,
"d5d72fa135d28b2e8307650b3ea79115183f2406"
);
assert!(&dir.join("dependencies").join("mylib-0.1.0").join("foo.txt").exists());
}
#[tokio::test]
async fn test_install_git_commit() {
let dir = testdir!();
fs::write(dir.join("soldeer.toml"), "[dependencies]\n").unwrap();
let cmd: Command = Install::builder()
.dependency("mylib~0.1.0")
.git_url("https://github.com/beeb/test-repo.git")
.rev("78c2f6a1a54db26bab6c3f501854a1564eb3707f")
.build()
.into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_install(&dir, "mylib", "0.1.0");
let lock = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
assert_eq!(
lock.entries.first().unwrap().as_git().unwrap().rev,
"78c2f6a1a54db26bab6c3f501854a1564eb3707f"
);
assert!(!&dir.join("dependencies").join("mylib-1.0.0").join("foo.txt").exists());
}
#[tokio::test]
async fn test_install_git_tag() {
let dir = testdir!();
fs::write(dir.join("soldeer.toml"), "[dependencies]\n").unwrap();
let cmd: Command = Install::builder()
.dependency("mylib~0.1.0")
.git_url("https://github.com/beeb/test-repo.git")
.tag("v0.1.0")
.build()
.into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_install(&dir, "mylib", "0.1.0");
let lock = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
assert_eq!(
lock.entries.first().unwrap().as_git().unwrap().rev,
"78c2f6a1a54db26bab6c3f501854a1564eb3707f"
);
assert!(!&dir.join("dependencies").join("mylib-1.0.0").join("foo.txt").exists());
}
#[tokio::test]
async fn test_install_git_branch() {
let dir = testdir!();
fs::write(dir.join("soldeer.toml"), "[dependencies]\n").unwrap();
let cmd: Command = Install::builder()
.dependency("mylib~dev")
.git_url("https://github.com/beeb/test-repo.git")
.branch("dev")
.build()
.into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_install(&dir, "mylib", "dev");
let lock = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
assert_eq!(
lock.entries.first().unwrap().as_git().unwrap().rev,
"8d903e557e8f1b6e62bde768aa456d4ddfca72c4"
);
assert!(!&dir.join("dependencies").join("mylib-1.0.0").join("test.txt").exists());
}
#[tokio::test]
async fn test_install_foundry_config() {
let dir = testdir!();
fs::write(dir.join("foundry.toml"), "[dependencies]\n").unwrap();
let cmd: Command = Install::builder().dependency("@openzeppelin-contracts~5").build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_install(&dir, "@openzeppelin-contracts", "5");
}
#[tokio::test]
async fn test_install_foundry_remappings() {
let dir = testdir!();
let contents = r#"[profile.default]
[soldeer]
remappings_location = "config"
[dependencies]
"@openzeppelin-contracts" = "5.1.0"
"#;
fs::write(dir.join("foundry.toml"), contents).unwrap();
let cmd: Command = Install::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let config = fs::read_to_string(dir.join("foundry.toml")).unwrap();
assert!(config.contains(
"remappings = [\"@openzeppelin-contracts-5.1.0/=dependencies/@openzeppelin-contracts-5.1.0/\"]"
));
}
#[tokio::test]
async fn test_install_missing_no_lock() {
let dir = testdir!();
let contents = r#"[dependencies]
"@openzeppelin-contracts" = "5.0.2"
"#;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let cmd: Command = Install::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_install(&dir, "@openzeppelin-contracts", "5.0.2");
}
#[tokio::test]
async fn test_install_missing_with_lock() {
let dir = testdir!();
let contents = r#"[dependencies]
mylib = "1.1"
"#;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let lock = r#"[[dependencies]]
name = "mylib"
version = "1.1.0"
url = "https://github.com/mario-eth/soldeer/archive/8585a7ec85a29889cec8d08f4770e15ec4795943.zip"
checksum = "94a73dbe106f48179ea39b00d42e5d4dd96fdc6252caa3a89ce7efdaec0b9468"
integrity = "f3c628f3e9eae4db14fe14f9ab29e49a0107c47b8ee956e4cee57b616b493fc2"
"#;
fs::write(dir.join(SOLDEER_LOCK), lock).unwrap();
let cmd: Command = Install::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
check_install(&dir, "mylib", "1.1");
}
#[tokio::test]
async fn test_install_second_time() {
let dir = testdir!();
let contents = r#"[dependencies]
mylib = "1.1"
"#;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
// get zip file locally for mock
let zip_file = download_file(
"https://github.com/mario-eth/soldeer/archive/8585a7ec85a29889cec8d08f4770e15ec4795943.zip",
&dir,
"tmp",
)
.await
.unwrap();
// serve the file with mock server
let mut server = mockito::Server::new_async().await;
let mock = server.mock("GET", "/file.zip").with_body_from_file(zip_file).create_async().await;
let mock = mock.expect(1); // download link should be called exactly once
let lock = format!(
r#"[[dependencies]]
name = "mylib"
version = "1.1.0"
url = "{}/file.zip"
checksum = "94a73dbe106f48179ea39b00d42e5d4dd96fdc6252caa3a89ce7efdaec0b9468"
integrity = "f3c628f3e9eae4db14fe14f9ab29e49a0107c47b8ee956e4cee57b616b493fc2"
"#,
server.url()
);
fs::write(dir.join(SOLDEER_LOCK), lock).unwrap();
let cmd: Command = Install::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd.clone(), Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
mock.assert(); // download link was called
// second install
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
mock.assert(); // download link was not called a second time
}
#[tokio::test]
async fn test_install_private_second_time() {
let dir = testdir!();
let contents = r#"[dependencies]
test-private = "0.1.0"
"#;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
// get zip file locally for mock
let zip_file = download_file(
"https://github.com/mario-eth/soldeer/archive/8585a7ec85a29889cec8d08f4770e15ec4795943.zip",
&dir,
"tmp",
)
.await
.unwrap();
// serve the file with mock server
let mut server = mockito::Server::new_async().await;
let data = format!(
r#"{{"data":[{{"created_at":"2025-09-28T12:36:09.526660Z","deleted":false,"id":"0440c261-8cdf-4738-9139-c4dc7b0c7f3e","internal_name":"test-private/0_1_0_28-09-2025_12:36:08_test-private.zip","private":true,"project_id":"14f419e7-2d64-49e4-86b9-b44b36627786","url":"{}/file.zip","version":"0.1.0"}}],"status":"success"}}"#,
server.url()
);
server.mock("GET", "/file.zip").with_body_from_file(zip_file).create_async().await;
server
.mock("GET", "/api/v1/revision-cli")
.match_query(Matcher::Any)
.with_header("content-type", "application/json")
.with_body(data)
.create_async()
.await;
let lock = r#"[[dependencies]]
name = "test-private"
version = "0.1.0"
checksum = "94a73dbe106f48179ea39b00d42e5d4dd96fdc6252caa3a89ce7efdaec0b9468"
integrity = "f3c628f3e9eae4db14fe14f9ab29e49a0107c47b8ee956e4cee57b616b493fc2"
"#;
fs::write(dir.join(SOLDEER_LOCK), lock).unwrap();
let cmd: Command = Install::builder().build().into();
let res = async_with_vars(
[
("SOLDEER_API_URL", Some(server.url().as_str())),
("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref())),
],
run(cmd.clone(), Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
// second install
let res = async_with_vars(
[
("SOLDEER_API_URL", Some(server.url().as_str())),
("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref())),
],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
}
#[tokio::test]
async fn test_install_add_existing_reinstall() {
let dir = testdir!();
let contents = r#"[dependencies]
"@openzeppelin-contracts" = "5.0.2"
"#;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let cmd: Command = Install::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok());
// remove dependencies folder and lockfile
fs::remove_dir_all(dir.join("dependencies")).unwrap();
fs::remove_file(dir.join(SOLDEER_LOCK)).unwrap();
// re-add the same dep, should re-install it
let cmd: Command =
Install::builder().dependency("@openzeppelin-contracts~5.0.2").build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok());
let dep_path = dir.join("dependencies").join("@openzeppelin-contracts-5.0.2");
assert!(dep_path.exists());
}
#[tokio::test]
async fn test_install_clean() {
let dir = testdir!();
let contents = r#"[dependencies]
"@openzeppelin-contracts" = "5.0.2"
"#;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let test_path = dir.join("dependencies").join("foo");
fs::create_dir_all(&test_path).unwrap();
fs::write(test_path.join("foo.txt"), "test").unwrap();
let cmd: Command = Install::builder().clean(true).build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
assert!(!test_path.exists());
}
#[tokio::test]
async fn test_install_recursive_deps() {
let dir = testdir!();
let contents = r#"[dependencies]
foo = { version = "0.1.0", git = "https://github.com/foundry-rs/forge-template.git" }
"#;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let cmd: Command = Install::builder().recursive_deps(true).build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let dep_path = dir.join("dependencies").join("foo-0.1.0");
assert!(dep_path.exists());
let sub_dirs_path = dep_path.join("lib");
assert!(sub_dirs_path.exists());
assert!(sub_dirs_path.join("forge-std").join("src").exists());
}
#[tokio::test]
async fn test_install_recursive_deps_soldeer() {
let dir = testdir!();
// this template uses soldeer to install forge-std
let contents = r#"[dependencies]
foo = { version = "0.1.0", git = "https://github.com/beeb/forge-template.git" }
"#;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let cmd: Command = Install::builder().recursive_deps(true).build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let dep_path = dir.join("dependencies").join("foo-0.1.0");
assert!(dep_path.exists());
let sub_dirs_path = dep_path.join("dependencies");
assert!(sub_dirs_path.exists());
assert!(sub_dirs_path.join("forge-std-1.9.7").join("src").exists());
}
#[tokio::test]
async fn test_install_recursive_deps_nested() {
let dir = testdir!();
let contents = r#"[dependencies]
"@uniswap-permit2" = { version = "1.0.0", url = "https://github.com/Uniswap/permit2/archive/cc56ad0f3439c502c246fc5cfcc3db92bb8b7219.zip" }
"#;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let cmd: Command = Install::builder().recursive_deps(true).build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let paths = [
"@uniswap-permit2-1.0.0/lib/forge-std/src",
"@uniswap-permit2-1.0.0/lib/forge-gas-snapshot/dependencies/forge-std-1.9.2/src",
"@uniswap-permit2-1.0.0/lib/openzeppelin-contracts/lib/erc4626-tests/ERC4626.test.sol",
"@uniswap-permit2-1.0.0/lib/openzeppelin-contracts/lib/forge-std/src",
"@uniswap-permit2-1.0.0/lib/openzeppelin-contracts/lib/halmos-cheatcodes/src",
"@uniswap-permit2-1.0.0/lib/solmate/lib/ds-test/src",
];
for path in paths {
let dep_path = dir.join("dependencies").join(path);
assert!(dep_path.exists());
}
}
#[tokio::test]
async fn test_install_recursive_project_root() {
let dir = testdir!();
let zip_path = create_zip_monorepo(&dir);
let checksum = hash_file(&zip_path).unwrap();
let contents = r#"[dependencies]
mylib = { version = "1.0.0", project_root = "contracts" }
[soldeer]
recursive_deps = true
"#;
// serve the dependency which uses foundry in a `contracts` subfolder
let mut server = mockito::Server::new_async().await;
server.mock("GET", "/file.zip").with_body_from_file(zip_path).create_async().await;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let lock = format!(
r#"[[dependencies]]
name = "mylib"
version = "1.0.0"
url = "{}/file.zip"
checksum = "{checksum}"
integrity = "e629088e5b74df78f116a24c328a64fd002b4e42449607b6ca78f9afb799374d"
"#,
server.url()
);
fs::write(dir.join(SOLDEER_LOCK), lock).unwrap();
let cmd: Command = Install::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd.clone(), Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
// check that we recursively installed all deps
assert!(dir.join("dependencies/mylib-1.0.0/contracts/dependencies/forge-std-1.11.0").is_dir());
}
#[tokio::test]
async fn test_install_recursive_project_root_invalid_path() {
let dir = testdir!();
let zip_path = create_zip_monorepo(&dir);
let checksum = hash_file(&zip_path).unwrap();
// directory traversal is forbidden
let contents = r#"[dependencies]
mylib = { version = "1.0.0", project_root = "../../../contracts" }
[soldeer]
recursive_deps = true
"#;
// serve the dependency which uses foundry in a `contracts` subfolder
let mut server = mockito::Server::new_async().await;
server.mock("GET", "/file.zip").with_body_from_file(zip_path).create_async().await;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let lock = format!(
r#"[[dependencies]]
name = "mylib"
version = "1.0.0"
url = "{}/file.zip"
checksum = "{checksum}"
integrity = "e629088e5b74df78f116a24c328a64fd002b4e42449607b6ca78f9afb799374d"
"#,
server.url()
);
fs::write(dir.join(SOLDEER_LOCK), lock).unwrap();
let cmd: Command = Install::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd.clone(), Verbosity::default()),
)
.await;
assert!(matches!(
res.unwrap_err(),
SoldeerError::InstallError(InstallError::ConfigError(
soldeer_core::errors::ConfigError::InvalidProjectRoot { .. }
))
));
}
#[tokio::test]
async fn test_install_regenerate_remappings() {
let dir = testdir!();
fs::write(dir.join("soldeer.toml"), "[dependencies]\n").unwrap();
fs::write(dir.join("remappings.txt"), "foo=bar").unwrap();
let cmd: Command = Install::builder()
.dependency("@openzeppelin-contracts~5")
.regenerate_remappings(true)
.build()
.into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let remappings = fs::read_to_string(dir.join("remappings.txt")).unwrap();
assert!(!remappings.contains("foo=bar"));
assert!(remappings.contains("@openzeppelin-contracts"));
}
#[tokio::test]
async fn test_add_remappings() {
let dir = testdir!();
let contents = r#"[profile.default]
src = "src"
out = "out"
libs = ["dependencies"]
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
[soldeer]
remappings_generate = true
remappings_prefix = "@custom-f@"
remappings_location = "config"
remappings_regenerate = true
[dependencies]
"#;
fs::write(dir.join("foundry.toml"), contents).unwrap();
let cmd: Command = Install::builder().dependency("forge-std~1.8.1").build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let updated_contents = r#"[profile.default]
src = "src"
out = "out"
libs = ["dependencies"]
remappings = ["@custom-f@forge-std-1.8.1/=dependencies/forge-std-1.8.1/"]
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
[soldeer]
remappings_generate = true
remappings_prefix = "@custom-f@"
remappings_location = "config"
remappings_regenerate = true
[dependencies]
forge-std = "1.8.1"
"#;
assert_eq!(updated_contents, fs::read_to_string(dir.join("foundry.toml")).unwrap());
}
#[tokio::test]
async fn test_modifying_remappings_prefix_config() {
let dir = testdir!();
let contents = r#"[profile.default]
libs = ["dependencies"]
remappings = ["@custom-f@forge-std-1.8.1/=dependencies/forge-std-1.8.1/"]
[soldeer]
remappings_prefix = "!custom-f!"
remappings_regenerate = true
remappings_location = "config"
[dependencies]
"#;
fs::write(dir.join("foundry.toml"), contents).unwrap();
let cmd: Command = Install::builder().dependency("forge-std~1.8.1").build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd.clone(), Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let expected = r#"[profile.default]
libs = ["dependencies"]
remappings = ["!custom-f!forge-std-1.8.1/=dependencies/forge-std-1.8.1/"]
[soldeer]
remappings_prefix = "!custom-f!"
remappings_regenerate = true
remappings_location = "config"
[dependencies]
forge-std = "1.8.1"
"#;
assert_eq!(expected, fs::read_to_string(dir.join("foundry.toml")).unwrap());
}
#[tokio::test]
async fn test_modifying_remappings_prefix_txt() {
let dir = testdir!();
let contents = r#"[profile.default]
[soldeer]
remappings_prefix = "!custom-f!"
remappings_regenerate = true
[dependencies]
"#;
fs::write(
dir.join("remappings.txt"),
"@custom-f@forge-std-1.8.1/=dependencies/forge-std-1.8.1/",
)
.unwrap();
fs::write(dir.join("foundry.toml"), contents).unwrap();
let cmd: Command = Install::builder().dependency("forge-std~1.8.1").build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd.clone(), Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let updated_contents = r#"!custom-f!forge-std-1.8.1/=dependencies/forge-std-1.8.1/
"#;
assert_eq!(updated_contents, fs::read_to_string(dir.join("remappings.txt")).unwrap());
}
#[tokio::test]
async fn test_install_new_foundry_no_dependency_tag() {
let dir = testdir!();
let contents = r#"[profile.default]
libs = ["lib"]
"#;
fs::write(dir.join("foundry.toml"), contents).unwrap();
let cmd: Command = Install::builder()
.dependency("@openzeppelin-contracts~5")
.config_location(ConfigLocation::Foundry)
.build()
.into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let config = fs::read_to_string(dir.join("foundry.toml")).unwrap();
let content = r#"[profile.default]
libs = ["lib", "dependencies"]
[dependencies]
"@openzeppelin-contracts" = "5"
"#;
assert_eq!(config, content);
}
#[tokio::test]
async fn test_install_new_soldeer_no_soldeer_toml() {
let dir = testdir!();
let cmd: Command = Install::builder()
.dependency("@openzeppelin-contracts~5")
.config_location(ConfigLocation::Soldeer)
.build()
.into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let config = fs::read_to_string(dir.join("soldeer.toml")).unwrap();
let content = r#"[dependencies]
"@openzeppelin-contracts" = "5"
"#;
assert_eq!(config, content);
}
#[tokio::test]
async fn test_install_new_soldeer_no_dependency_tag() {
let dir = testdir!();
let contents = r#"[soldeer]
"#;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let cmd: Command = Install::builder()
.dependency("@openzeppelin-contracts~5")
.config_location(ConfigLocation::Soldeer)
.build()
.into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let config = fs::read_to_string(dir.join("soldeer.toml")).unwrap();
let content = r#"[soldeer]
[dependencies]
"@openzeppelin-contracts" = "5"
"#;
assert_eq!(config, content);
}
#[tokio::test]
async fn test_install_recursive_deps_with_foundry_lock() {
let dir = testdir!();
let zip_path = create_zip_with_foundry_lock(&dir, None);
let checksum = hash_file(&zip_path).unwrap();
let contents = r#"[dependencies]
mylib = "1.0.0"
[soldeer]
recursive_deps = true
"#;
// Serve the dependency via mock server
let mut server = mockito::Server::new_async().await;
server.mock("GET", "/file.zip").with_body_from_file(&zip_path).create_async().await;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let lock = format!(
r#"[[dependencies]]
name = "mylib"
version = "1.0.0"
url = "{}/file.zip"
checksum = "{checksum}"
integrity = "placeholder"
"#,
server.url()
);
fs::write(dir.join(SOLDEER_LOCK), lock).unwrap();
let cmd: Command = Install::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd.clone(), Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
// Verify the submodule exists
let forge_std_path = dir.join("dependencies/mylib-1.0.0/lib/forge-std");
assert!(forge_std_path.exists());
// Verify it's checked out at the specific revision from foundry.lock
let output = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&forge_std_path)
.output()
.expect("failed to run git rev-parse");
let current_rev = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(current_rev, "c29afdd40a82db50a3d3709d324416be50050e5e");
}
#[tokio::test]
async fn test_install_recursive_deps_with_foundry_lock_branch() {
let dir = testdir!();
let zip_path = create_zip_with_foundry_lock(&dir, Some("master"));
let checksum = hash_file(&zip_path).unwrap();
let contents = r#"[dependencies]
mylib = "1.0.0"
[soldeer]
recursive_deps = true
"#;
// Serve the dependency via mock server
let mut server = mockito::Server::new_async().await;
server.mock("GET", "/file.zip").with_body_from_file(&zip_path).create_async().await;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let lock = format!(
r#"[[dependencies]]
name = "mylib"
version = "1.0.0"
url = "{}/file.zip"
checksum = "{checksum}"
integrity = "placeholder"
"#,
server.url()
);
fs::write(dir.join(SOLDEER_LOCK), lock).unwrap();
let cmd: Command = Install::builder().build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd.clone(), Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
// Verify the submodule exists
let forge_std_path = dir.join("dependencies/mylib-1.0.0/lib/forge-std");
assert!(forge_std_path.exists());
// Verify it's checked out at the specific revision from foundry.lock
let output = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(&forge_std_path)
.output()
.expect("failed to run git rev-parse");
let current_rev = String::from_utf8_lossy(&output.stdout).trim().to_string();
assert_eq!(current_rev, "c29afdd40a82db50a3d3709d324416be50050e5e",);
}
================================================
FILE: crates/commands/tests/tests-login.rs
================================================
use std::{fs, path::PathBuf};
use mockito::{Matcher, Mock, ServerGuard};
use soldeer_commands::{Command, Verbosity, commands::login::Login, run};
use temp_env::async_with_vars;
use testdir::testdir;
async fn mock_api_server() -> (ServerGuard, Mock) {
let mut server = mockito::Server::new_async().await;
let body = r#"{"status":"success","token": "example_token_jwt"}"#;
let mock = server
.mock("POST", "/api/v1/auth/login")
.match_query(Matcher::Any)
.with_header("content-type", "application/json")
.with_body(body)
.create_async()
.await;
(server, mock)
}
async fn mock_api_server_token() -> (ServerGuard, Mock) {
let mut server = mockito::Server::new_async().await;
let body = r#"{"status":"success","data":{"created_at": "2024-08-04T14:21:31.622589Z","email": "test@test.net","id": "b6d56bf0-00a5-474f-b732-f416bef53e92","organization": "test","role": "owner","updated_at": "2024-08-04T14:21:31.622589Z","username": "test","verified": true}}"#;
let mock = server
.mock("GET", "/api/v1/auth/validate-cli-token")
.match_query(Matcher::Any)
.with_header("content-type", "application/json")
.with_body(body)
.create_async()
.await;
(server, mock)
}
#[tokio::test]
async fn test_login_without_prompt_err_400() {
let cmd: Command = Login::builder().email("test@test.com").password("111111").build().into();
let res = run(cmd, Verbosity::default()).await;
assert_eq!(
res.unwrap_err().to_string(),
"error during login: http error during login: HTTP status client error (400 Bad Request) for url (https://api.soldeer.xyz/api/v1/auth/login)"
);
}
#[tokio::test]
async fn test_login_without_prompt_success() {
let (server, mock) = mock_api_server().await;
let dir = testdir!();
let login_file: PathBuf = dir.join("test_save_jwt");
let cmd: Command = Login::builder().email("test@test.com").password("111111").build().into();
let res = async_with_vars(
[
("SOLDEER_API_URL", Some(server.url())),
("SOLDEER_LOGIN_FILE", Some(login_file.to_string_lossy().to_string())),
],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok());
assert!(login_file.exists());
assert_eq!(fs::read_to_string(login_file).unwrap(), "example_token_jwt");
mock.expect(1);
}
#[tokio::test]
async fn test_login_token_success() {
let (server, mock) = mock_api_server_token().await;
let dir = testdir!();
let login_file: PathBuf = dir.join("test_save_jwt");
let cmd: Command = Login::builder().token("example_token_jwt").build().into();
let res = async_with_vars(
[
("SOLDEER_API_URL", Some(server.url())),
("SOLDEER_LOGIN_FILE", Some(login_file.to_string_lossy().to_string())),
],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok());
assert!(login_file.exists());
assert_eq!(fs::read_to_string(login_file).unwrap(), "example_token_jwt");
mock.expect(1);
}
#[tokio::test]
async fn test_login_token_failure() {
let cmd: Command = Login::builder().token("asdf").build().into();
let res = run(cmd, Verbosity::default()).await;
assert_eq!(res.unwrap_err().to_string(), "error during login: login error: invalid token");
}
================================================
FILE: crates/commands/tests/tests-push.rs
================================================
use mockito::{Matcher, Mock, ServerGuard};
use reqwest::StatusCode;
use soldeer_commands::{Verbosity, commands::push::Push, run};
use soldeer_core::{SoldeerError, errors::PublishError};
use std::{env, fs, path::PathBuf};
use temp_env::async_with_vars;
use testdir::testdir;
#[allow(clippy::unwrap_used)]
fn setup_project(dotfile: bool) -> (PathBuf, PathBuf) {
let dir = testdir!();
let login_file: PathBuf = dir.join("test_save_jwt");
fs::write(&login_file, "jwt_token_example").unwrap();
let project_path = dir.join("mypkg");
fs::create_dir(&project_path).unwrap();
fs::write(project_path.join("foundry.toml"), "[dependencies]\n").unwrap();
if dotfile {
fs::write(project_path.join(".env"), "super-secret-stuff").unwrap();
}
(login_file, project_path)
}
async fn mock_api_server(status_code: Option<StatusCode>) -> (ServerGuard, Mock) {
let mut server = mockito::Server::new_async().await;
let body = r#"{"data":[{"created_at":"2024-02-27T19:19:23.938837Z","created_by":"96228bb5-f777-4c19-ba72-363d14b8beed","deleted":false,"deprecated":false,"description":"","downloads":648041,"github_url":"","id":"37adefe5-9bc6-4777-aaf2-e56277d1f30b","image":"","latest_version":"1.10.0","long_description":"","name":"mock","organization_id":"ff9c0d8e-9275-4f6f-a1b7-2e822450a7ba","organization_name":"","organization_verified":true,"updated_at":"2024-02-27T19:19:23.938837Z"}],"status":"success"}"#;
server
.mock("GET", "/api/v2/project")
.match_query(Matcher::Any)
.with_header("content-type", "application/json")
.with_body(body)
.create_async()
.await;
let mock = match status_code {
Some(status_code) => {
server
.mock("POST", "/api/v1/revision/upload")
.with_header("content-type", "application/json")
.with_status(status_code.as_u16() as usize)
.with_body(r#"{"status":"fail","message": "failure"}"#)
.create_async()
.await
}
None => {
server
.mock("POST", "/api/v1/revision/upload")
.with_header("content-type", "application/json")
.with_body(r#"{"status":"success","data":{"data":{"project_id":"mock"}}}"#)
.create_async()
.await
}
};
(server, mock)
}
#[tokio::test]
async fn test_push_success() {
let (login_file, project_path) = setup_project(false);
let (server, mock) = mock_api_server(None).await;
env::set_current_dir(&project_path).unwrap();
let res = async_with_vars(
[
("SOLDEER_PROJECT_ROOT", Some(project_path.to_string_lossy().to_string())),
("SOLDEER_API_URL", Some(server.url())),
("SOLDEER_LOGIN_FILE", Some(login_file.to_string_lossy().to_string())),
],
run(Push::builder().dependency("mypkg~0.1.0").build().into(), Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
mock.expect(1);
}
#[tokio::test]
async fn test_push_other_dir_success() {
let dir = testdir!();
fs::write(dir.join("foundry.toml"), "[dependencies]\n").unwrap();
let login_file = dir.join("test_save_jwt");
fs::write(&login_file, "jwt_token_example").unwrap();
let project_path = dir.join("mypkg");
fs::create_dir(&project_path).unwrap();
fs::write(project_path.join("test.sol"), "contract Foo {}\n").unwrap();
let (server, mock) = mock_api_server(None).await;
let res = async_with_vars(
[
("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().to_string())),
("SOLDEER_API_URL", Some(server.url())),
("SOLDEER_LOGIN_FILE", Some(login_file.to_string_lossy().to_string())),
],
run(
Push::builder().dependency("mypkg~0.1.0").path(project_path).build().into(),
Verbosity::default(),
),
)
.await;
assert!(res.is_ok(), "{res:?}");
mock.expect(1);
}
#[tokio::test]
async fn test_push_not_found() {
let (login_file, project_path) = setup_project(false);
let (server, mock) = mock_api_server(Some(StatusCode::NO_CONTENT)).await;
let res = async_with_vars(
[
("SOLDEER_PROJECT_ROOT", Some(project_path.to_string_lossy().to_string())),
("SOLDEER_API_URL", Some(server.url())),
("SOLDEER_LOGIN_FILE", Some(login_file.to_string_lossy().to_string())),
],
run(
Push::builder().dependency("mypkg~0.1.0").path(project_path).build().into(),
Verbosity::default(),
),
)
.await;
assert!(matches!(res, Err(SoldeerError::PublishError(PublishError::ProjectNotFound))));
mock.expect(1);
}
#[tokio::test]
async fn test_push_already_exists() {
let (login_file, project_path) = setup_project(false);
let (server, mock) = mock_api_server(Some(StatusCode::ALREADY_REPORTED)).await;
let res = async_with_vars(
[
("SOLDEER_PROJECT_ROOT", Some(project_path.to_string_lossy().to_string())),
("SOLDEER_API_URL", Some(server.url())),
("SOLDEER_LOGIN_FILE", Some(login_file.to_string_lossy().to_string())),
],
run(
Push::builder().dependency("mypkg~0.1.0").path(project_path).build().into(),
Verbosity::default(),
),
)
.await;
assert!(matches!(res, Err(SoldeerError::PublishError(PublishError::AlreadyExists))));
mock.expect(1);
}
#[tokio::test]
async fn test_push_unauthorized() {
let (login_file, project_path) = setup_project(false);
let (server, mock) = mock_api_server(Some(StatusCode::UNAUTHORIZED)).await;
let res = async_with_vars(
[
("SOLDEER_PROJECT_ROOT", Some(project_path.to_string_lossy().to_string())),
("SOLDEER_API_URL", Some(server.url())),
("SOLDEER_LOGIN_FILE", Some(login_file.to_string_lossy().to_string())),
],
run(
Push::builder().dependency("mypkg~0.1.0").path(project_path).build().into(),
Verbosity::default(),
),
)
.await;
assert!(matches!(res, Err(SoldeerError::PublishError(PublishError::AuthError(_)))));
mock.expect(1);
}
#[tokio::test]
async fn test_push_payload_too_large() {
let (login_file, project_path) = setup_project(false);
let (server, mock) = mock_api_server(Some(StatusCode::PAYLOAD_TOO_LARGE)).await;
let res = async_with_vars(
[
("SOLDEER_PROJECT_ROOT", Some(project_path.to_string_lossy().to_string())),
("SOLDEER_API_URL", Some(server.url())),
("SOLDEER_LOGIN_FILE", Some(login_file.to_string_lossy().to_string())),
],
run(
Push::builder().dependency("mypkg~0.1.0").path(project_path).build().into(),
Verbosity::default(),
),
)
.await;
assert!(matches!(res, Err(SoldeerError::PublishError(PublishError::PayloadTooLarge))));
mock.expect(1);
}
#[tokio::test]
async fn test_push_other_error() {
let (login_file, project_path) = setup_project(false);
let (server, mock) = mock_api_server(Some(StatusCode::INTERNAL_SERVER_ERROR)).await;
let res = async_with_vars(
[
("SOLDEER_PROJECT_ROOT", Some(project_path.to_string_lossy().to_string())),
("SOLDEER_API_URL", Some(server.url())),
("SOLDEER_LOGIN_FILE", Some(login_file.to_string_lossy().to_string())),
],
run(
Push::builder().dependency("mypkg~0.1.0").path(project_path).build().into(),
Verbosity::default(),
),
)
.await;
assert!(matches!(res, Err(SoldeerError::PublishError(PublishError::HttpError(_)))));
mock.expect(1);
}
#[tokio::test]
async fn test_push_dry_run() {
let (login_file, project_path) = setup_project(true); // insert a .env file
let (server, mock) = mock_api_server(None).await;
let res = async_with_vars(
[
("SOLDEER_PROJECT_ROOT", Some(project_path.to_string_lossy().to_string())),
("SOLDEER_API_URL", Some(server.url())),
("SOLDEER_LOGIN_FILE", Some(login_file.to_string_lossy().to_string())),
],
run(
Push::builder()
.dependency("mypkg~0.1.0")
.path(&project_path)
.dry_run(true)
.build()
.into(),
Verbosity::default(),
),
)
.await;
assert!(res.is_ok(), "{res:?}");
mock.expect(0);
assert!(project_path.join("mypkg.zip").exists());
}
#[tokio::test]
async fn test_push_skip_warnings() {
let (login_file, project_path) = setup_project(true); // insert a .env file
let (server, mock) = mock_api_server(None).await;
let res = async_with_vars(
[
("SOLDEER_PROJECT_ROOT", Some(project_path.to_string_lossy().to_string())),
("SOLDEER_API_URL", Some(server.url())),
("SOLDEER_LOGIN_FILE", Some(login_file.to_string_lossy().to_string())),
],
run(
Push::builder()
.dependency("mypkg~0.1.0")
.path(&project_path)
.skip_warnings(true)
.build()
.into(),
Verbosity::default(),
),
)
.await;
assert!(res.is_ok(), "{res:?}");
mock.expect(1);
}
================================================
FILE: crates/commands/tests/tests-uninstall.rs
================================================
use soldeer_commands::{
Command, Verbosity,
commands::{install::Install, uninstall::Uninstall},
run,
};
use soldeer_core::{
config::read_config_deps,
lock::{SOLDEER_LOCK, read_lockfile},
};
use std::{fs, path::PathBuf};
use temp_env::async_with_vars;
use testdir::testdir;
#[allow(clippy::unwrap_used)]
async fn setup(config_filename: &str) -> PathBuf {
let dir = testdir!();
let mut contents = r#"[dependencies]
"@openzeppelin-contracts" = "5.0.2"
solady = "0.0.238"
"#
.to_string();
if config_filename == "foundry.toml" {
contents = format!(
r#"[profile.default]
[soldeer]
remappings_location = "config"
{contents}"#
);
}
fs::write(dir.join(config_filename), contents).unwrap();
let cmd: Command = Install::default().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
dir
}
#[tokio::test]
async fn test_uninstall_one() {
let dir = setup("soldeer.toml").await;
let cmd: Command = Uninstall::builder().dependency("solady").build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let (deps, _) = read_config_deps(dir.join("soldeer.toml")).unwrap();
assert!(!deps.iter().any(|d| d.name() == "solady"));
let remappings = fs::read_to_string(dir.join("remappings.txt")).unwrap();
assert!(!remappings.contains("solady"));
let lock = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
assert!(!lock.entries.iter().any(|d| d.name() == "solady"));
assert!(!dir.join("dependencies").join("solady-0.0.238").exists());
}
#[tokio::test]
async fn test_uninstall_all() {
let dir = setup("soldeer.toml").await;
let cmd: Command = Uninstall::builder().dependency("solady").build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let cmd: Command = Uninstall::builder().dependency("@openzeppelin-contracts").build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let (deps, _) = read_config_deps(dir.join("soldeer.toml")).unwrap();
assert!(deps.is_empty());
let remappings = fs::read_to_string(dir.join("remappings.txt")).unwrap();
assert_eq!(remappings, "");
assert!(!dir.join(SOLDEER_LOCK).exists());
}
#[tokio::test]
async fn test_uninstall_foundry_config() {
let dir = setup("foundry.toml").await;
let cmd: Command = Uninstall::builder().dependency("solady").build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let (deps, _) = read_config_deps(dir.join("foundry.toml")).unwrap();
assert!(!deps.iter().any(|d| d.name() == "solady"));
let config = fs::read_to_string(dir.join("foundry.toml")).unwrap();
assert!(!config.contains("solady"));
}
================================================
FILE: crates/commands/tests/tests-update.rs
================================================
use soldeer_commands::{
Command, Verbosity,
commands::{install::Install, update::Update},
run,
};
use soldeer_core::{
config::ConfigLocation,
lock::{SOLDEER_LOCK, read_lockfile},
};
use std::{fs, path::PathBuf};
use temp_env::async_with_vars;
use testdir::testdir;
#[allow(clippy::unwrap_used)]
async fn setup(config_filename: &str) -> PathBuf {
// install v1.9.0 of forge-std (faking an old install)
let dir = testdir!();
let mut contents = r#"[dependencies]
forge-std = "1.9.0"
"#
.to_string();
if config_filename == "foundry.toml" {
contents = format!(
r#"[profile.default]
[soldeer]
remappings_location = "config"
{contents}"#
);
}
fs::write(dir.join(config_filename), &contents).unwrap();
let cmd: Command = Install::default().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
// change install requirement to forge-std ^1.0.0 (making the current install outdated)
contents = contents.replace("1.9.0", "1");
fs::write(dir.join(config_filename), &contents).unwrap();
// update remappings accordingly
fs::write(dir.join("remappings.txt"), "forge-std-1/=dependencies/forge-std-1.9.0/\n").unwrap();
dir
}
#[tokio::test]
async fn test_update_existing() {
let dir = setup("soldeer.toml").await;
let cmd: Command = Update::default().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let lockfile = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
let version = lockfile.entries.first().unwrap().version();
assert_ne!(version, "1.9.0");
let remappings = fs::read_to_string(dir.join("remappings.txt")).unwrap();
assert_eq!(remappings, format!("forge-std-1/=dependencies/forge-std-{version}/\n"));
assert!(dir.join("dependencies").join(format!("forge-std-{version}")).exists());
}
#[tokio::test]
async fn test_update_foundry_config() {
let dir = setup("foundry.toml").await;
let cmd: Command = Update::default().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let lockfile = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
let version = lockfile.entries.first().unwrap().version();
assert_ne!(version, "1.9.0");
assert!(dir.join("dependencies").join(format!("forge-std-{version}")).exists());
}
#[tokio::test]
async fn test_update_missing() {
let dir = testdir!();
let contents = r#"[dependencies]
forge-std = "1"
"#;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let cmd: Command = Update::default().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let lockfile = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
let version = lockfile.entries.first().unwrap().version();
assert!(dir.join("dependencies").join(format!("forge-std-{version}")).exists());
}
#[tokio::test]
async fn test_update_custom_remappings() {
let dir = setup("soldeer.toml").await;
// customize remappings before update
fs::write(dir.join("remappings.txt"), "forge-std/=dependencies/forge-std-1.9.0/src/\n")
.unwrap();
let cmd: Command = Update::default().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let lockfile = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
let version = lockfile.entries.first().unwrap().version();
let remappings = fs::read_to_string(dir.join("remappings.txt")).unwrap();
assert_eq!(remappings, format!("forge-std/=dependencies/forge-std-{version}/src/\n"));
}
#[tokio::test]
async fn test_update_git_main() {
let dir = testdir!();
// install older commit in "main" branch
let contents = r#"[dependencies]
my-lib = { version = "branch-main", git = "https://github.com/beeb/test-repo.git" }
"#;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let lockfile = r#"[[dependencies]]
name = "my-lib"
version = "branch-main"
git = "https://github.com/beeb/test-repo.git"
rev = "78c2f6a1a54db26bab6c3f501854a1564eb3707f"
"#;
fs::write(dir.join(SOLDEER_LOCK), lockfile).unwrap();
let cmd: Command = Install::default().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
// update to latest commit in "main" branch
let cmd: Command = Update::default().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let lockfile = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
assert_eq!(
lockfile.entries.first().unwrap().as_git().unwrap().rev,
"d5d72fa135d28b2e8307650b3ea79115183f2406"
);
}
#[tokio::test]
async fn test_update_git_branch() {
let dir = testdir!();
// install older commit in "dev" branch
let contents = r#"[dependencies]
my-lib = { version = "branch-dev", git = "https://github.com/beeb/test-repo.git", branch = "dev" }
"#;
fs::write(dir.join("soldeer.toml"), contents).unwrap();
let lockfile = r#"[[dependencies]]
name = "my-lib"
version = "branch-dev"
git = "https://github.com/beeb/test-repo.git"
rev = "78c2f6a1a54db26bab6c3f501854a1564eb3707f"
"#;
fs::write(dir.join(SOLDEER_LOCK), lockfile).unwrap();
let cmd: Command = Install::default().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
// update to latest commit in "dev" branch
let cmd: Command = Update::default().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let lockfile = read_lockfile(dir.join(SOLDEER_LOCK)).unwrap();
assert_eq!(
lockfile.entries.first().unwrap().as_git().unwrap().rev,
"8d903e557e8f1b6e62bde768aa456d4ddfca72c4"
);
}
#[tokio::test]
async fn test_update_foundry_config_multi_dep() {
let dir = testdir!();
let contents = r#"[profile.default]
[dependencies]
"@tt" = {version = "1.6.1", url = "https://soldeer-revisions.s3.amazonaws.com/@openzeppelin-contracts/3_3_0-rc_2_22-01-2024_13:12:57_contracts.zip"}
forge-std = { version = "1.8.1" }
solmate = "6.7.0"
mario = { version = "1.0", git = "https://gitlab.com/mario4582928/Mario.git", rev = "22868f426bd4dd0e682b5ec5f9bd55507664240c" }
mario-custom-tag = { version = "1.0", git = "https://gitlab.com/mario4582928/Mario.git", tag = "custom-tag" }
mario-custom-branch = { version = "1.0", git = "https://gitlab.com/mario4582928/Mario.git", tag = "custom-branch" }
[soldeer]
remappings_location = "config"
"#;
fs::write(dir.join("foundry.toml"), contents).unwrap();
let cmd: Command = Update::default().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let deps = dir.join("dependencies");
assert!(deps.join("@tt-1.6.1").exists());
assert!(deps.join("forge-std-1.8.1").exists());
assert!(deps.join("solmate-6.7.0").exists());
assert!(deps.join("mario-1.0").exists());
assert!(deps.join("mario-custom-tag-1.0").exists());
assert!(deps.join("mario-custom-branch-1.0").exists());
}
#[tokio::test]
async fn test_install_new_foundry_no_foundry_toml() {
let dir = testdir!();
let cmd: Command = Update::builder().config_location(ConfigLocation::Foundry).build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let config = fs::read_to_string(dir.join("foundry.toml")).unwrap();
let expected = r#"[profile.default]
src = "src"
out = "out"
libs = ["dependencies"]
[dependencies]
# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options
"#;
assert_eq!(config, expected);
}
#[tokio::test]
async fn test_install_new_soldeer_no_soldeer_toml() {
let dir = testdir!();
let cmd: Command = Update::builder().config_location(ConfigLocation::Soldeer).build().into();
let res = async_with_vars(
[("SOLDEER_PROJECT_ROOT", Some(dir.to_string_lossy().as_ref()))],
run(cmd, Verbosity::default()),
)
.await;
assert!(res.is_ok(), "{res:?}");
let config = fs::read_to_string(dir.join("soldeer.toml")).unwrap();
let content = "[dependencies]\n";
assert_eq!(config, content);
}
================================================
FILE: crates/core/Cargo.toml
================================================
[package]
name = "soldeer-core"
description = "Core functionality for Soldeer"
authors.workspace = true
categories.workspace = true
edition.workspace = true
exclude.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[lints]
workspace = true
[dependencies]
bon.workspace = true
chrono = { version = "0.4.38", default-features = false, features = [
"serde",
"std",
] }
const-hex = "1.12.0"
derive_more.workspace = true
dunce = "1.0.5"
home = "0.5.9"
ignore = { version = "0.4.24", features = ["simd-accel"] }
log = { workspace = true, features = ["kv_std"] }
path-slash.workspace = true
rayon.workspace = true
regex = "1.10.5"
reqwest = { workspace = true, features = ["json", "multipart", "stream"] }
sanitize-filename = "0.6.0"
semver = "1.0.23"
serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.120"
sha2 = "0.10.8"
thiserror.workspace = true
tokio.workspace = true
toml_edit = { version = "0.25.11", features = ["serde"] }
uuid = { version = "1.10.0", features = ["serde", "v4"] }
zip = { version = "4.0.0", default-features = false, features = ["deflate"] }
zip-extract = { version = "0.4.0", default-features = false, features = [
"deflate",
] }
[dev-dependencies]
mockito.workspace = true
temp-env.workspace = true
testdir.workspace = true
[features]
serde = []
================================================
FILE: crates/core/src/auth.rs
================================================
//! Registry authentication
use crate::{errors::AuthError, registry::api_url, utils::login_file_path};
use log::{debug, info, warn};
use reqwest::{
Client, StatusCode,
header::{AUTHORIZATION, HeaderMap, HeaderValue},
};
use serde::{Deserialize, Serialize};
use std::{env, fs, path::PathBuf};
pub type Result<T> = std::result::Result<T, AuthError>;
/// Credentials to be used for login
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct Credentials {
pub email: String,
pub password: String,
}
/// Response from the login endpoint
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
pub struct LoginResponse {
pub status: String,
/// JWT token
pub token: String,
}
/// Get the JWT token from the environment or from the login file
///
/// Precedence is given to the `SOLDEER_API_TOKEN` environment variable.
pub fn get_token() -> Result<String> {
if let Ok(token) = env::var("SOLDEER_API_TOKEN") &&
!token.is_empty()
{
return Ok(token)
}
let token_path = login_file_path()?;
let jwt =
fs::read_to_string(&token_path).map_err(|_| AuthError::MissingToken)?.trim().to_string();
if jwt.is_empty() {
debug!(token_path:?; "token file exists but is empty");
return Err(AuthError::MissingToken);
}
debug!(token_path:?; "token retrieved from file");
Ok(jwt)
}
/// Get a header map with the bearer token set up if it exists
pub fn get_auth_headers() -> Result<HeaderMap> {
let mut headers: HeaderMap = HeaderMap::new();
let Ok(token) = get_token() else {
return Ok(headers);
};
let header_value =
HeaderValue::from_str(&format!("Bearer {token}")).map_err(|_| AuthError::InvalidToken)?;
headers.insert(AUTHORIZATION, header_value);
Ok(headers)
}
/// Save an access token in the login file
pub fn save_token(token: &str) -> Result<PathBuf> {
let token_path = login_file_path()?;
fs::write(&token_path, token)?;
Ok(token_path)
}
/// Retrieve user profile for the token to check its validity, returning the username
pub async fn check_token(token: &str) -> Result<String> {
let client = Client::new();
let url = api_url("v1", "auth/validate-cli-token", &[]);
let mut headers: HeaderMap = HeaderMap::new();
let header_value =
HeaderValue::from_str(&format!("Bearer {token}")).map_err(|_| AuthError::InvalidToken)?;
headers.insert(AUTHORIZATION, header_value);
let response = client.get(url).headers(headers).send().await?;
match response.status() {
s if s.is_success() => {
#[derive(Deserialize)]
struct User {
id: String,
username: String,
}
#[derive(Deserialize)]
struct UserResponse {
data: User,
}
let res: UserResponse = response.json().await?;
debug!("token is valid for user {} with ID {}", res.data.username, res.data.id);
Ok(res.data.username)
}
StatusCode::UNAUTHORIZED => Err(AuthError::InvalidToken),
_ => Err(AuthError::HttpError(
response.error_for_status().expect_err("result should be an error"),
)),
}
}
/// Execute the login request and store the JWT token in the login file
pub async fn execute_login(login: &Credentials) -> Result<PathBuf> {
warn!(
"the option to login via email and password will be removed in a future version of Soldeer. Please update your usage by either using `soldeer login --token [YOUR CLI TOKEN]` or passing the `SOLDEER_API_TOKEN` environment variable to the `push` command."
);
let token_path = login_file_path()?;
let url = api_url("v1", "auth/login", &[]);
let client = Client::new();
let res = client.post(url).json(login).send().await?;
match res.status() {
s if s.is_success() => {
debug!("login request completed");
let response: LoginResponse = res.json().await?;
fs::write(&token_path, response.token)?;
info!(token_path:?; "login successful");
Ok(token_path)
}
StatusCode::UNAUTHORIZED => Err(AuthError::InvalidCredentials),
_ => Err(AuthError::HttpError(
res.error_for_status().expect_err("result should be an error"),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use temp_env::{async_with_vars, with_var};
use testdir::testdir;
#[tokio::test]
async fn test_login_success() {
let mut server = mockito::Server::new_async().await;
server
.mock("POST", "/api/v1/auth/login")
.with_status(201)
.with_header("content-type", "application/json")
.with_body(r#"{"status":"200","token":"jwt_token_example"}"#)
.create_async()
.await;
let test_file = testdir!().join("test_save_jwt");
let res = async_with_vars(
[
("SOLDEER_API_URL", Some(server.url())),
("SOLDEER_LOGIN_FILE", Some(test_file.to_string_lossy().to_string())),
],
execute_login(&Credentials {
email: "test@test.com".to_string(),
password: "1234".to_string(),
}),
)
.await;
assert!(res.is_ok(), "{res:?}");
assert_eq!(fs::canonicalize(res.unwrap()).unwrap(), fs::canonicalize(&test_file).unwrap());
assert_eq!(fs::read_to_string(test_file).unwrap(), "jwt_token_example");
}
#[tokio::test]
async fn test_login_401() {
let mut server = mockito::Server::new_async().await;
server
.mock("POST", "/api/v1/auth/login")
.with_status(401)
.with_header("content-type", "application/json")
.with_body(r#"{"status":"401"}"#)
.create_async()
.await;
let test_file = testdir!().join("test_save_jwt");
let res = async_with_vars(
[
("SOLDEER_API_URL", Some(server.url())),
("SOLDEER_LOGIN_FILE", Some(test_file.to_string_lossy().to_string())),
],
execute_login(&Credentials {
email: "test@test.com".to_string(),
password: "1234".to_string(),
}),
)
.await;
assert!(matches!(res, Err(AuthError::InvalidCredentials)), "{res:?}");
}
#[tokio::test]
async fn test_login_500() {
let mut server = mockito::Server::new_async().await;
server
.mock("POST", "/api/v1/auth/login")
.with_status(500)
.with_header("content-type", "application/json")
.with_body(r#"{"status":"500"}"#)
.create_async()
.await;
let test_file = testdir!().join("test_save_jwt");
let res = async_with_vars(
[
("SOLDEER_API_URL", Some(server.url())),
("SOLDEER_LOGIN_FILE", Some(test_file.to_string_lossy().to_string())),
],
execute_login(&Credentials {
email: "test@test.com".to_string(),
password: "1234".to_string(),
}),
)
.await;
assert!(matches!(res, Err(AuthError::HttpError(_))), "{res:?}");
}
#[tokio::test]
async fn test_check_token_success() {
let mut server = mockito::Server::new_async().await;
server
.mock("GET", "/api/v1/auth/validate-cli-token")
.with_status(200)
.with_header("content-type", "application/json")
.with_body(
r#"{"status":"success","data":{"created_at": "2024-08-04T14:21:31.622589Z","email": "test@test.net","id": "b6d56bf0-00a5-474f-b732-f416bef53e92","organization": "test","role": "owner","updated_at": "2024-08-04T14:21:31.622589Z","username": "test","verified": true}}"#,
)
.create_async()
.await;
let res =
async_with_vars([("SOLDEER_API_URL", Some(server.url()))], check_token("eyJ0..."))
.await;
assert!(res.is_ok(), "{res:?}");
assert_eq!(res.unwrap(), "test");
}
#[tokio::test]
async fn test_check_token_failure() {
let mut server = mockito::Server::new_async().await;
server
.mock("GET", "/api/v1/auth/validate-cli-token")
.with_status(401)
.with_header("content-type", "application/json")
.with_body(r#"{"status":"fail","message":"Invalid token"}"#)
.create_async()
.await;
let res =
async_with_vars([("SOLDEER_API_URL", Some(server.url()))], check_token("foobar")).await;
assert!(res.is_err(), "{res:?}");
}
#[test]
fn test_get_token_env() {
let res = with_var("SOLDEER_API_TOKEN", Some("test"), get_token);
assert!(res.is_ok(), "{res:?}");
assert_eq!(res.unwrap(), "test");
}
}
================================================
FILE: crates/core/src/config.rs
================================================
//! Manage the Soldeer configuration and dependencies list.
use crate::{
download::{find_install_path, find_install_path_sync},
errors::ConfigError,
lock::SOLDEER_LOCK,
remappings::RemappingsLocation,
};
use derive_more::derive::{Display, From, FromStr};
use log::{debug, warn};
use serde::Deserialize;
use std::{
env, fmt, fs,
path::{Path, PathBuf},
};
use toml_edit::{Array, DocumentMut, InlineTable, Item, Table, value};
pub type Result<T> = std::result::Result<T, ConfigError>;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum UrlType {
Git(String),
Http(String),
}
impl UrlType {
pub fn git(url: impl Into<String>) -> Self {
Self::Git(url.into())
}
pub fn http(url: impl Into<String>) -> Self {
Self::Http(url.into())
}
}
/// The paths used by Soldeer.
///
/// The paths are canonicalized on creation of the object.
///
/// To create this object, the [`Paths::new`] and [`Paths::from_root`] methods can be used.
///
/// # Examples
///
/// ```
/// # use soldeer_core::config::Paths;
/// # let dir = testdir::testdir!();
/// # std::env::set_current_dir(&dir).unwrap();
/// # std::fs::write("foundry.toml", "[dependencies]\n").unwrap();
/// let paths = Paths::new().unwrap(); // foundry.toml exists in the current path
/// assert_eq!(paths.root, std::env::current_dir().unwrap());
/// assert_eq!(paths.config, std::env::current_dir().unwrap().join("foundry.toml"));
///
/// let paths = Paths::from_root(&dir).unwrap(); // root is the given path
/// assert_eq!(paths.root, dir);
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, Deserialize))]
// making sure the struct is not constructible from the outside without using the new/from methods
#[non_exhaustive]
pub struct Paths {
/// The root directory of the project.
///
/// At the moment, the current directory or the path given by the `SOLDEER_PROJECT_ROOT`
/// environment variable.
pub root: PathBuf,
/// The path to the config file.
///
/// `foundry.toml` if it contains a `[dependencies]` table, otherwise `soldeer.toml` if it
/// exists. Otherwise, the `foundry.toml` file is used by default. When the config file does
/// not exist, a new one is created with default contents.
pub config: PathBuf,
/// The path to the dependencies folder (does not need to exist).
///
/// This is `/dependencies` inside the root directory.
pub dependencies: PathBuf,
/// The path to the lockfile (does not need to exist).
///
/// This is `/soldeer.lock` inside the root directory.
pub lock: PathBuf,
/// The path to the remappings file (does not need to exist).
///
/// This path gets ignored if the remappings should be generated in the `foundry.toml` file.
/// This is `/remappings.txt` inside the root directory.
pub remappings: PathBuf,
}
impl Paths {
/// Instantiate all the paths needed for Soldeer.
///
/// The root path defaults to the current directory but can be overridden with the
/// `SOLDEER_PROJECT_ROOT` environment variable.
///
/// The paths are canonicalized.
pub fn new() -> Result<Self> {
Self::with_config(None)
}
/// Instantiate all the paths needed for Soldeer.
///
/// The root path is automatically detected (by traversing the path) but can be overridden with
/// the `SOLDEER_PROJECT_ROOT` environment variable.
/// Alternatively, the [`Paths::with_root_and_config`] constructor can be used.
///
/// If a config location is provided, it bypasses auto-detection and uses that. If `None`, then
/// the location is auto-detected or if impossible, the `foundry.toml` file is used. If the
/// config file does not exist yet, it gets created with default content.
///
/// The paths are canonicalized.
pub fn with_config(config_location: Option<ConfigLocation>) -> Result<Self> {
let root = dunce::canonicalize(Self::get_root_path())?;
Self::with_root_and_config(root, config_location)
}
/// Instantiate all the paths needed for Soldeer.
///
/// If a config location is provided, it bypasses auto-detection and uses that. If `None`, then
/// the location is auto-detected or if impossible, the `foundry.toml` file is used. If the
/// config file does not exist yet, it gets created with default content.
///
/// The paths are canonicalized.
pub fn with_root_and_config(
root: impl AsRef<Path>,
config_location: Option<ConfigLocation>,
) -> Result<Self> {
let root = root.as_ref();
let config = Self::get_config_path(root, config_location)?;
let dependencies = root.join("dependencies");
let lock = root.join(SOLDEER_LOCK);
let remappings = root.join("remappings.txt");
Ok(Self { root: root.to_path_buf(), config, dependencies, lock, remappings })
}
/// Generate the paths object from a known root directory.
///
/// The `SOLDEER_PROJECT_ROOT` environment variable is ignored.
///
/// The paths are canonicalized.
pub fn from_root(root: impl AsRef<Path>) -> Result<Self> {
let root = dunce::canonicalize(root.as_ref())?;
let config = Self::get_config_path(&root, None)?;
let dependencies = root.join("dependencies");
let lock = root.join(SOLDEER_LOCK);
let remappings = root.join("remappings.txt");
Ok(Self { root, config, dependencies, lock, remappings })
}
/// Get the root directory path.
///
/// If `SOLDEER_PROJECT` root is present in the environment, this is the returned value. Else,
/// we search for the root of the project with `find_project_root`.
pub fn get_root_path() -> PathBuf {
let res = env::var("SOLDEER_PROJECT_ROOT").map_or_else(
|_| {
debug!("SOLDEER_PROJECT_ROOT not defined, searching for project root");
find_project_root(None::<PathBuf>).expect("could not find project root")
},
|p| {
if p.is_empty() {
debug!("SOLDEER_PROJECT_ROOT exists but is empty, searching for project root");
find_project_root(None::<PathBuf>).expect("could not find project root")
} else {
debug!(path = p; "root set by SOLDEER_PROJECT_ROOT");
PathBuf::from(p)
}
},
);
debug!(path:? = res; "found project root");
res
}
/// Get the path to the config file.
///
/// If a parameter is given for `config_location`, it will be used. Otherwise, the function will
/// try to auto-detect the location based on the existence of the `dependencies` entry in
/// the foundry config file, or the existence of a `soldeer.toml` file. If no config can be
/// found, `foundry.toml` is used by default.
fn get_config_path(
root: impl AsRef<Path>,
config_location: Option<ConfigLocation>,
) -> Result<PathBuf> {
let foundry_path = root.as_ref().join("foundry.toml");
let soldeer_path = root.as_ref().join("soldeer.toml");
// use the user preference if available
let location = config_location.or_else(|| {
debug!("no preferred config location, trying to detect automatically");
detect_config_location(root)
}).unwrap_or_else(|| {
warn!("config file location could not be determined automatically, using foundry by default");
ConfigLocation::Foundry
});
debug!("using config location {location:?}");
create_or_modify_config(location, &foundry_path, &soldeer_path)
}
/// Default Foundry config file path
pub fn foundry_default() -> PathBuf {
let root: PathBuf =
dunce::canonicalize(Self::get_root_path()).expect("could not get the root");
root.join("foundry.toml")
}
/// Default Soldeer config file path
pub fn soldeer_default() -> PathBuf {
let root: PathBuf =
dunce::canonicalize(Self::get_root_path()).expect("could not get the root");
root.join("soldeer.toml")
}
}
/// For clap
fn default_true() -> bool {
true
}
/// The Soldeer config options.
#[derive(Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct SoldeerConfig {
/// Whether to generate remappings or completely leave them untouched.
///
/// Defaults to `true`.
#[serde(default = "default_true")]
pub remappings_generate: bool,
/// Whether to regenerate the remappings every time and ignore existing content.
///
/// Defaults to `false`.
#[serde(default)]
pub remappings_regenerate: bool,
/// Whether to include the version requirement string in the left part of the remappings.
///
/// Defaults to `true`.
#[serde(default = "default_true")]
pub remappings_version: bool,
/// A prefix to add to each dependency name in the left part of the remappings.
///
/// None by default.
#[serde(default)]
pub remappings_prefix: String,
/// The location where the remappings file should be generated.
///
/// Either inside the `foundry.toml` config file or as a separate `remappings.txt` file.
/// This gets ignored if the config file is `soldeer.toml`, in which case the remappings
/// are always generated in a separate file.
///
/// Defaults to [`RemappingsLocation::Txt`].
#[serde(default)]
pub remappings_location: RemappingsLocation,
/// Whether to include dependencies from dependencies.
///
/// For dependencies which use soldeer, the `soldeer install` command will be invoked.
/// Git dependencies which have submodules will see their submodules cloned as well.
///
/// Defaults to `false`.
#[serde(default)]
pub recursive_deps: bool,
}
impl Default for SoldeerConfig {
fn default() -> Self {
Self {
remappings_generate: true,
remappings_regenerate: false,
remappings_version: true,
remappings_prefix: String::new(),
remappings_location: RemappingsLocation::default(),
recursive_deps: false,
}
}
}
/// A git identifier used to specify a revision, branch or tag.
///
/// # Examples
///
/// ```
/// # use soldeer_core::config::GitIdentifier;
/// let rev = GitIdentifier::from_rev("082692fcb6b5b1ab8f856914897f7f2b46b84fd2");
/// let branch = GitIdentifier::from_branch("feature/foo");
/// let tag = GitIdentifier::from_tag("v1.0.0");
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, Deserialize))]
pub enum GitIdentifier {
/// A commit hash
Rev(String),
/// A branch name
Branch(String),
/// A tag name
Tag(String),
}
impl GitIdentifier {
/// Create a new git identifier from a revision hash.
pub fn from_rev(rev: impl Into<String>) -> Self {
let rev: String = rev.into();
Self::Rev(rev)
}
/// Create a new git identifier from a branch name.
pub fn from_branch(branch: impl Into<String>) -> Self {
let branch: String = branch.into();
Self::Branch(branch)
}
/// Create a new git identifier from a tag name.
pub fn from_tag(tag: impl Into<String>) -> Self {
let tag: String = tag.into();
Self::Tag(tag)
}
}
/// A git dependency config item.
///
/// This struct is used to represent a git dependency from the config file.
#[derive(Debug, Clone, PartialEq, Eq, Hash, bon::Builder)]
#[allow(clippy::duplicated_attributes)]
#[builder(on(String, into), on(PathBuf, into))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, Deserialize))]
pub struct GitDependency {
/// The name of the dependency (user-defined).
pub name: String,
/// The version requirement string (semver).
///
/// Example: `>=1.9.3 || ^2.0.0`
///
/// When no operator is used before the version number, it defaults to `=` which pins the
/// version.
#[cfg_attr(feature = "serde", serde(rename = "version"))]
pub version_req: String,
/// The git URL, must end with `.git`.
pub git: String,
/// The git identifier (revision, branch or tag).
///
/// If omitted, the default branch is used.
pub identifier: Option<GitIdentifier>,
/// An optional relative path to the project's root within the repository.
///
/// The project root is where the soldeer.toml or foundry.toml resides. If no path is provided,
/// then the repo's root must contain a Soldeer config.
pub project_root: Option<PathBuf>,
}
impl fmt::Display for GitDependency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}~{}", self.name, self.version_req)
}
}
/// An HTTP dependency config item.
///
/// This struct is used to represent an HTTP dependency from the config file.
#[derive(Debug, Clone, PartialEq, Eq, Hash, bon::Builder)]
#[allow(clippy::duplicated_attributes)]
#[builder(on(String, into), on(PathBuf, into))]
#[cfg_attr(feature = "serde", derive(serde::Serialize, Deserialize))]
pub struct HttpDependency {
/// The name of the dependency (user-defined).
pub name: String,
/// The version requirement string (semver).
///
/// Example: `>=1.9.3 || ^2.0.0`
///
/// When no operator is used before the version number, it defaults to `=` which pins the
/// version.
#[cfg_attr(feature = "serde", serde(rename = "version"))]
pub version_req: String,
/// The URL to the dependency.
///
/// If omitted, the registry will be contacted to get the download URL for that dependency (by
/// name).
pub url: Option<String>,
/// An optional relative path to the project's root within the zip file.
///
/// The project root is where the soldeer.toml or foundry.toml resides. If no path is provided,
/// then the zip's root must contain a Soldeer config.
pub project_root: Option<PathBuf>,
}
impl fmt::Display for HttpDependency {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}~{}", self.name, self.version_req)
}
}
/// A git or HTTP dependency config item.
///
/// A builder can be used to create the underlying [`HttpDependency`] or [`GitDependency`] and then
/// converted into this type with `.into()`.
///
/// # Examples
///
/// ```
/// # use soldeer_core::config::{Dependency, HttpDependency};
/// let dep: Dependency = HttpDependency::builder()
/// .name("my-dep")
/// .version_req("^1.0.0")
/// .url("https://...")
/// .build()
/// .into();
/// ```
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, From)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, Deserialize))]
pub enum Dependency {
#[from(HttpDependency)]
Http(HttpDependency),
#[from(GitDependency)]
Git(GitDependency),
}
impl Dependency {
/// Create a new dependency from a name and version requirement string.
///
/// The string should be in the format `name~version_req`.
///
/// The version requirement string can use the semver format.
///
/// Example: `dependency~^1.0.0`
///
/// If a custom URL is provided, then the version requirement string
/// cannot contain the `=` character, as it would break the remappings.
///
/// # Examples
///
/// ```
/// # use soldeer_core::config::{Dependency, HttpDependency, GitDependency, GitIdentifier, UrlType};
/// assert_eq!(
/// Dependency::from_name_version("my-lib~^1.0.0", Some(UrlType::http("https://foo.bar/zip.zip")), None)
/// .unwrap(),
/// HttpDependency::builder()
/// .name("my-lib")
/// .version_req("^1.0.
gitextract_fq4q0vwy/ ├── .config/ │ └── nextest.toml ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ ├── feature_request.yml │ │ └── registry_request.yml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ └── workflows/ │ ├── release.yml │ └── rust.yml ├── .gitignore ├── .vscode/ │ └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Cargo.toml ├── LICENSE ├── README.md ├── USAGE.md ├── clippy.toml ├── crates/ │ ├── cli/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── main.rs │ ├── commands/ │ │ ├── Cargo.toml │ │ ├── src/ │ │ │ ├── commands/ │ │ │ │ ├── clean.rs │ │ │ │ ├── init.rs │ │ │ │ ├── install.rs │ │ │ │ ├── login.rs │ │ │ │ ├── mod.rs │ │ │ │ ├── push.rs │ │ │ │ ├── uninstall.rs │ │ │ │ └── update.rs │ │ │ ├── lib.rs │ │ │ └── utils.rs │ │ └── tests/ │ │ ├── tests-clean.rs │ │ ├── tests-init.rs │ │ ├── tests-install.rs │ │ ├── tests-login.rs │ │ ├── tests-push.rs │ │ ├── tests-uninstall.rs │ │ └── tests-update.rs │ └── core/ │ ├── Cargo.toml │ └── src/ │ ├── auth.rs │ ├── config.rs │ ├── download.rs │ ├── errors.rs │ ├── install.rs │ ├── lib.rs │ ├── lock/ │ │ └── forge.rs │ ├── lock.rs │ ├── push.rs │ ├── registry.rs │ ├── remappings.rs │ ├── update.rs │ └── utils.rs ├── flake.nix ├── release-plz.toml └── rustfmt.toml
SYMBOL INDEX (461 symbols across 31 files)
FILE: crates/cli/src/main.rs
constant HAVE_COLOR (line 8) | const HAVE_COLOR: Condition = Condition(|| {
function main (line 15) | async fn main() {
function banner (line 38) | fn banner() {
FILE: crates/commands/src/commands/clean.rs
type Clean (line 11) | pub struct Clean {
function clean_command (line 15) | pub(crate) fn clean_command(paths: &Paths, _cmd: &Clean) -> Result<()> {
FILE: crates/commands/src/commands/init.rs
type Init (line 23) | pub struct Init {
function init_command (line 37) | pub(crate) async fn init_command(paths: &Paths, cmd: Init) -> Result<()> {
FILE: crates/commands/src/commands/install.rs
type Install (line 40) | pub struct Install {
function install_command (line 95) | pub(crate) async fn install_command(paths: &Paths, cmd: Install) -> Resu...
FILE: crates/commands/src/commands/login.rs
type Login (line 20) | pub struct Login {
function login_command (line 34) | pub(crate) async fn login_command(cmd: Login) -> Result<()> {
FILE: crates/commands/src/commands/mod.rs
type CustomLevel (line 14) | pub struct CustomLevel;
method default_filter (line 17) | fn default_filter() -> VerbosityFilter {
method verbose_help (line 21) | fn verbose_help() -> Option<&'static str> {
method verbose_long_help (line 25) | fn verbose_long_help() -> Option<&'static str> {
method quiet_help (line 40) | fn quiet_help() -> Option<&'static str> {
type Args (line 49) | pub struct Args {
type Command (line 61) | pub enum Command {
type Version (line 75) | pub struct Version {}
function validate_dependency (line 77) | fn validate_dependency(dep: &str) -> std::result::Result<String, String> {
FILE: crates/commands/src/commands/push.rs
type Push (line 31) | pub struct Push {
function push_command (line 55) | pub(crate) async fn push_command(cmd: Push) -> Result<()> {
function prompt_user_for_confirmation (line 95) | fn prompt_user_for_confirmation() -> Result<bool> {
FILE: crates/commands/src/commands/uninstall.rs
type Uninstall (line 16) | pub struct Uninstall {
function uninstall_command (line 21) | pub(crate) fn uninstall_command(paths: &Paths, cmd: &Uninstall) -> Resul...
FILE: crates/commands/src/commands/update.rs
type Update (line 23) | pub struct Update {
function update_command (line 46) | pub(crate) async fn update_command(paths: &Paths, cmd: Update) -> Result...
FILE: crates/commands/src/lib.rs
type ConfigLocation (line 26) | pub struct ConfigLocation(soldeer_core::config::ConfigLocation);
method value_variants (line 29) | fn value_variants<'a>() -> &'a [Self] {
method to_possible_value (line 36) | fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
method from (line 51) | fn from(value: soldeer_core::config::ConfigLocation) -> Self {
function from (line 45) | fn from(value: ConfigLocation) -> Self {
function run (line 56) | pub async fn run(command: Command, verbosity: Verbosity<CustomLevel>) ->...
FILE: crates/commands/src/utils.rs
constant PROGRESS_TEMPLATE (line 10) | pub const PROGRESS_TEMPLATE: &str = "[{elapsed_precise}] {bar:30.magenta...
type Progress (line 14) | pub struct Progress {
method new (line 27) | pub fn new(title: impl fmt::Display, total: usize, mut monitor: Instal...
method start_all (line 110) | pub fn start_all(&self) {
method stop_all (line 119) | pub fn stop_all(&self) {
method set_error (line 128) | pub fn set_error(&self, error: impl fmt::Display) {
function get_config_location (line 134) | pub fn get_config_location(
function prompt_config_location (line 148) | pub fn prompt_config_location() -> Result<ConfigLocation> {
FILE: crates/commands/tests/tests-clean.rs
function check_clean_success (line 20) | fn check_clean_success(dir: &Path, config_filename: &str) {
function check_artifacts_exist (line 33) | fn check_artifacts_exist(dir: &Path) {
function setup_project_with_dependencies (line 45) | async fn setup_project_with_dependencies(config_filename: &str) -> PathB...
function test_clean_basic (line 73) | async fn test_clean_basic() {
function test_clean_foundry_config (line 89) | async fn test_clean_foundry_config() {
function test_clean_no_artifacts (line 104) | async fn test_clean_no_artifacts() {
function test_clean_restores_with_install (line 121) | async fn test_clean_restores_with_install() {
function test_clean_with_complex_file_structure (line 150) | async fn test_clean_with_complex_file_structure() {
function test_clean_permission_error (line 180) | async fn test_clean_permission_error() {
function test_clean_with_soldeer_config_variations (line 219) | async fn test_clean_with_soldeer_config_variations() {
function test_clean_multiple_times (line 260) | async fn test_clean_multiple_times() {
FILE: crates/commands/tests/tests-init.rs
function test_init_clean (line 13) | async fn test_init_clean() {
function test_init_no_clean (line 46) | async fn test_init_no_clean() {
function test_init_no_remappings (line 78) | async fn test_init_no_remappings() {
function test_init_no_gitignore (line 104) | async fn test_init_no_gitignore() {
function test_init_select_foundry_location (line 126) | async fn test_init_select_foundry_location() {
function test_init_select_soldeer_location (line 159) | async fn test_init_select_soldeer_location() {
FILE: crates/commands/tests/tests-install.rs
function check_install (line 20) | fn check_install(dir: &Path, name: &str, version_req: &str) {
function create_zip_monorepo (line 37) | fn create_zip_monorepo(testdir: &Path) -> PathBuf {
function create_zip_with_foundry_lock (line 68) | fn create_zip_with_foundry_lock(testdir: &Path, branch: Option<&str>) ->...
function test_install_registry_any_version (line 126) | async fn test_install_registry_any_version() {
function test_install_registry_wildcard (line 140) | async fn test_install_registry_wildcard() {
function test_install_registry_specific_version (line 154) | async fn test_install_registry_specific_version() {
function test_install_custom_http (line 169) | async fn test_install_custom_http() {
function test_install_git_main (line 192) | async fn test_install_git_main() {
function test_install_git_commit (line 216) | async fn test_install_git_commit() {
function test_install_git_tag (line 241) | async fn test_install_git_tag() {
function test_install_git_branch (line 266) | async fn test_install_git_branch() {
function test_install_foundry_config (line 291) | async fn test_install_foundry_config() {
function test_install_foundry_remappings (line 305) | async fn test_install_foundry_remappings() {
function test_install_missing_no_lock (line 330) | async fn test_install_missing_no_lock() {
function test_install_missing_with_lock (line 347) | async fn test_install_missing_with_lock() {
function test_install_second_time (line 372) | async fn test_install_second_time() {
function test_install_private_second_time (line 424) | async fn test_install_private_second_time() {
function test_install_add_existing_reinstall (line 486) | async fn test_install_add_existing_reinstall() {
function test_install_clean (line 518) | async fn test_install_clean() {
function test_install_recursive_deps (line 538) | async fn test_install_recursive_deps() {
function test_install_recursive_deps_soldeer (line 559) | async fn test_install_recursive_deps_soldeer() {
function test_install_recursive_deps_nested (line 581) | async fn test_install_recursive_deps_nested() {
function test_install_recursive_project_root (line 609) | async fn test_install_recursive_project_root() {
function test_install_recursive_project_root_invalid_path (line 650) | async fn test_install_recursive_project_root_invalid_path() {
function test_install_regenerate_remappings (line 694) | async fn test_install_regenerate_remappings() {
function test_add_remappings (line 715) | async fn test_add_remappings() {
function test_modifying_remappings_prefix_config (line 764) | async fn test_modifying_remappings_prefix_config() {
function test_modifying_remappings_prefix_txt (line 805) | async fn test_modifying_remappings_prefix_txt() {
function test_install_new_foundry_no_dependency_tag (line 837) | async fn test_install_new_foundry_no_dependency_tag() {
function test_install_new_soldeer_no_soldeer_toml (line 865) | async fn test_install_new_soldeer_no_soldeer_toml() {
function test_install_new_soldeer_no_dependency_tag (line 887) | async fn test_install_new_soldeer_no_dependency_tag() {
function test_install_recursive_deps_with_foundry_lock (line 913) | async fn test_install_recursive_deps_with_foundry_lock() {
function test_install_recursive_deps_with_foundry_lock_branch (line 966) | async fn test_install_recursive_deps_with_foundry_lock_branch() {
FILE: crates/commands/tests/tests-login.rs
function mock_api_server (line 8) | async fn mock_api_server() -> (ServerGuard, Mock) {
function mock_api_server_token (line 21) | async fn mock_api_server_token() -> (ServerGuard, Mock) {
function test_login_without_prompt_err_400 (line 35) | async fn test_login_without_prompt_err_400() {
function test_login_without_prompt_success (line 45) | async fn test_login_without_prompt_success() {
function test_login_token_success (line 66) | async fn test_login_token_success() {
function test_login_token_failure (line 86) | async fn test_login_token_failure() {
FILE: crates/commands/tests/tests-push.rs
function setup_project (line 10) | fn setup_project(dotfile: bool) -> (PathBuf, PathBuf) {
function mock_api_server (line 23) | async fn mock_api_server(status_code: Option<StatusCode>) -> (ServerGuar...
function test_push_success (line 57) | async fn test_push_success() {
function test_push_other_dir_success (line 77) | async fn test_push_other_dir_success() {
function test_push_not_found (line 105) | async fn test_push_not_found() {
function test_push_already_exists (line 127) | async fn test_push_already_exists() {
function test_push_unauthorized (line 149) | async fn test_push_unauthorized() {
function test_push_payload_too_large (line 171) | async fn test_push_payload_too_large() {
function test_push_other_error (line 193) | async fn test_push_other_error() {
function test_push_dry_run (line 215) | async fn test_push_dry_run() {
function test_push_skip_warnings (line 243) | async fn test_push_skip_warnings() {
FILE: crates/commands/tests/tests-uninstall.rs
function setup (line 15) | async fn setup(config_filename: &str) -> PathBuf {
function test_uninstall_one (line 44) | async fn test_uninstall_one() {
function test_uninstall_all (line 63) | async fn test_uninstall_all() {
function test_uninstall_foundry_config (line 88) | async fn test_uninstall_foundry_config() {
FILE: crates/commands/tests/tests-update.rs
function setup (line 15) | async fn setup(config_filename: &str) -> PathBuf {
function test_update_existing (line 49) | async fn test_update_existing() {
function test_update_foundry_config (line 67) | async fn test_update_foundry_config() {
function test_update_missing (line 83) | async fn test_update_missing() {
function test_update_custom_remappings (line 102) | async fn test_update_custom_remappings() {
function test_update_git_main (line 121) | async fn test_update_git_main() {
function test_update_git_branch (line 159) | async fn test_update_git_branch() {
function test_update_foundry_config_multi_dep (line 197) | async fn test_update_foundry_config_multi_dep() {
function test_install_new_foundry_no_foundry_toml (line 233) | async fn test_install_new_foundry_no_foundry_toml() {
function test_install_new_soldeer_no_soldeer_toml (line 257) | async fn test_install_new_soldeer_no_soldeer_toml() {
FILE: crates/core/src/auth.rs
type Result (line 11) | pub type Result<T> = std::result::Result<T, AuthError>;
type Credentials (line 15) | pub struct Credentials {
type LoginResponse (line 22) | pub struct LoginResponse {
function get_token (line 31) | pub fn get_token() -> Result<String> {
function get_auth_headers (line 49) | pub fn get_auth_headers() -> Result<HeaderMap> {
function save_token (line 61) | pub fn save_token(token: &str) -> Result<PathBuf> {
function check_token (line 68) | pub async fn check_token(token: &str) -> Result<String> {
function execute_login (line 99) | pub async fn execute_login(login: &Credentials) -> Result<PathBuf> {
function test_login_success (line 130) | async fn test_login_success() {
function test_login_401 (line 158) | async fn test_login_401() {
function test_login_500 (line 184) | async fn test_login_500() {
function test_check_token_success (line 210) | async fn test_check_token_success() {
function test_check_token_failure (line 230) | async fn test_check_token_failure() {
function test_get_token_env (line 246) | fn test_get_token_env() {
FILE: crates/core/src/config.rs
type Result (line 17) | pub type Result<T> = std::result::Result<T, ConfigError>;
type UrlType (line 21) | pub enum UrlType {
method git (line 27) | pub fn git(url: impl Into<String>) -> Self {
method http (line 31) | pub fn http(url: impl Into<String>) -> Self {
type Paths (line 60) | pub struct Paths {
method new (line 98) | pub fn new() -> Result<Self> {
method with_config (line 113) | pub fn with_config(config_location: Option<ConfigLocation>) -> Result<...
method with_root_and_config (line 125) | pub fn with_root_and_config(
method from_root (line 143) | pub fn from_root(root: impl AsRef<Path>) -> Result<Self> {
method get_root_path (line 157) | pub fn get_root_path() -> PathBuf {
method get_config_path (line 183) | fn get_config_path(
method foundry_default (line 202) | pub fn foundry_default() -> PathBuf {
method soldeer_default (line 209) | pub fn soldeer_default() -> PathBuf {
function default_true (line 217) | fn default_true() -> bool {
type SoldeerConfig (line 224) | pub struct SoldeerConfig {
method default (line 270) | fn default() -> Self {
type GitIdentifier (line 294) | pub enum GitIdentifier {
method from_rev (line 307) | pub fn from_rev(rev: impl Into<String>) -> Self {
method from_branch (line 313) | pub fn from_branch(branch: impl Into<String>) -> Self {
method from_tag (line 319) | pub fn from_tag(tag: impl Into<String>) -> Self {
type GitDependency (line 332) | pub struct GitDependency {
method fmt (line 361) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result {
type HttpDependency (line 373) | pub struct HttpDependency {
method fmt (line 400) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::fmt::Result {
type Dependency (line 423) | pub enum Dependency {
method from_name_version (line 473) | pub fn from_name_version(
method name (line 523) | pub fn name(&self) -> &str {
method version_req (line 531) | pub fn version_req(&self) -> &str {
method url (line 539) | pub fn url(&self) -> Option<&String> {
method install_path_sync (line 547) | pub fn install_path_sync(&self, deps: impl AsRef<Path>) -> Option<Path...
method install_path (line 553) | pub async fn install_path(&self, deps: impl AsRef<Path>) -> Option<Pat...
method project_root (line 559) | pub fn project_root(&self) -> Option<PathBuf> {
method to_toml_value (line 567) | pub fn to_toml_value(&self) -> (String, Item) {
method is_http (line 646) | pub fn is_http(&self) -> bool {
method as_http (line 651) | pub fn as_http(&self) -> Option<&HttpDependency> {
method as_http_mut (line 656) | pub fn as_http_mut(&mut self) -> Option<&mut HttpDependency> {
method is_git (line 661) | pub fn is_git(&self) -> bool {
method as_git (line 666) | pub fn as_git(&self) -> Option<&GitDependency> {
method as_git_mut (line 671) | pub fn as_git_mut(&mut self) -> Option<&mut GitDependency> {
method from (line 677) | fn from(dep: &HttpDependency) -> Self {
method from (line 683) | fn from(dep: &GitDependency) -> Self {
type ConfigLocation (line 691) | pub enum ConfigLocation {
method from (line 700) | fn from(value: ConfigLocation) -> Self {
type ParsingWarning (line 711) | pub struct ParsingWarning {
method fmt (line 717) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type ParsingResult (line 725) | pub struct ParsingResult {
method has_warnings (line 732) | pub fn has_warnings(&self) -> bool {
method from (line 738) | fn from(value: HttpDependency) -> Self {
method from (line 744) | fn from(value: GitDependency) -> Self {
method from (line 750) | fn from(value: Dependency) -> Self {
function detect_config_location (line 760) | pub fn detect_config_location(root: impl AsRef<Path>) -> Option<ConfigLo...
function read_config_deps (line 796) | pub fn read_config_deps(path: impl AsRef<Path>) -> Result<(Vec<Dependenc...
function read_soldeer_config (line 816) | pub fn read_soldeer_config(path: impl AsRef<Path>) -> Result<SoldeerConf...
function add_to_config (line 832) | pub fn add_to_config(dependency: &Dependency, config_path: impl AsRef<Pa...
function delete_from_config (line 854) | pub fn delete_from_config(dependency_name: &str, path: impl AsRef<Path>)...
function update_config_libs (line 876) | pub fn update_config_libs(foundry_config: impl AsRef<Path>) -> Result<()> {
function find_git_root (line 921) | fn find_git_root(relative_to: impl AsRef<Path>) -> Result<Option<PathBuf...
function find_project_root (line 939) | fn find_project_root(cwd: Option<impl AsRef<Path>>) -> Result<PathBuf> {
function parse_dependency (line 967) | fn parse_dependency(name: impl Into<String>, value: &Item) -> Result<Par...
function create_or_modify_config (line 1160) | fn create_or_modify_config(
function write_to_config (line 1207) | fn write_to_config(content: &str, filename: &str) -> PathBuf {
function test_paths_config_soldeer (line 1214) | fn test_paths_config_soldeer() {
function test_paths_config_foundry (line 1228) | fn test_paths_config_foundry() {
function test_paths_from_root (line 1247) | fn test_paths_from_root() {
function test_from_name_version_no_url (line 1256) | fn test_from_name_version_no_url() {
function test_from_name_version_with_http_url (line 1266) | fn test_from_name_version_with_http_url() {
function test_from_name_version_with_git_url (line 1285) | fn test_from_name_version_with_git_url() {
function test_from_name_version_with_git_url_rev (line 1320) | fn test_from_name_version_with_git_url_rev() {
function test_from_name_version_with_git_url_branch (line 1340) | fn test_from_name_version_with_git_url_branch() {
function test_from_name_version_with_git_url_tag (line 1360) | fn test_from_name_version_with_git_url_tag() {
function test_from_name_version_with_git_ssh (line 1380) | fn test_from_name_version_with_git_ssh() {
function test_from_name_version_with_git_ssh_rev (line 1399) | fn test_from_name_version_with_git_ssh_rev() {
function test_from_name_version_empty_version (line 1419) | fn test_from_name_version_empty_version() {
function test_from_name_version_invalid_version (line 1425) | fn test_from_name_version_invalid_version() {
function test_read_soldeer_config_default (line 1446) | fn test_read_soldeer_config_default() {
function test_read_soldeer_config (line 1457) | fn test_read_soldeer_config() {
function test_read_foundry_config_deps (line 1487) | fn test_read_foundry_config_deps() {
function test_read_soldeer_config_deps (line 1586) | fn test_read_soldeer_config_deps() {
function test_read_soldeer_config_deps_bad_version (line 1682) | fn test_read_soldeer_config_deps_bad_version() {
function test_read_soldeer_config_deps_bad_git (line 1719) | fn test_read_soldeer_config_deps_bad_git() {
function test_add_to_config (line 1734) | fn test_add_to_config() {
function test_add_to_config_no_section (line 1799) | fn test_add_to_config_no_section() {
function test_delete_from_config (line 1809) | fn test_delete_from_config() {
function test_delete_from_config_missing (line 1869) | fn test_delete_from_config_missing() {
function test_update_config_libs (line 1879) | fn test_update_config_libs() {
function test_update_config_profile_empty (line 1900) | fn test_update_config_profile_empty() {
function test_update_config_libs_empty (line 1918) | fn test_update_config_libs_empty() {
function test_parse_dependency (line 1940) | fn test_parse_dependency() {
function test_parse_dependency_extra_field (line 1961) | fn test_parse_dependency_extra_field() {
function test_parse_dependency_git_extra_url (line 1974) | fn test_parse_dependency_git_extra_url() {
function test_parse_dependency_git_field_conflict (line 1993) | fn test_parse_dependency_git_field_conflict() {
function test_parse_dependency_missing_url (line 2008) | fn test_parse_dependency_missing_url() {
function test_find_git_root (line 2023) | fn test_find_git_root() {
function test_find_git_root_nested (line 2054) | fn test_find_git_root_nested() {
function test_find_project_root_with_foundry_toml (line 2075) | fn test_find_project_root_with_foundry_toml() {
function test_find_project_root_with_soldeer_toml (line 2086) | fn test_find_project_root_with_soldeer_toml() {
function test_find_project_root_in_subdirectory (line 2097) | fn test_find_project_root_in_subdirectory() {
function test_find_project_root_git_boundary (line 2111) | fn test_find_project_root_git_boundary() {
FILE: crates/core/src/download.rs
type Result (line 17) | pub type Result<T> = std::result::Result<T, DownloadError>;
function download_file (line 23) | pub async fn download_file(
function unzip_file (line 48) | pub async fn unzip_file(path: impl AsRef<Path>, into: impl AsRef<Path>) ...
function clone_repo (line 77) | pub async fn clone_repo(
function delete_dependency_files_sync (line 103) | pub fn delete_dependency_files_sync(dependency: &Dependency, deps: impl ...
function find_install_path_sync (line 117) | pub fn find_install_path_sync(dependency: &Dependency, deps: impl AsRef<...
function find_install_path (line 138) | pub async fn find_install_path(dependency: &Dependency, deps: impl AsRef...
function delete_dependency_files (line 162) | pub async fn delete_dependency_files(
function install_path_matches (line 182) | fn install_path_matches(dependency: &Dependency, path: impl AsRef<Path>)...
function test_download_file (line 199) | async fn test_download_file() {
function test_unzip_file (line 214) | async fn test_unzip_file() {
function test_clone_repo (line 231) | async fn test_clone_repo() {
function test_clone_repo_rev (line 239) | async fn test_clone_repo_rev() {
function test_clone_repo_branch (line 252) | async fn test_clone_repo_branch() {
function test_clone_repo_tag (line 265) | async fn test_clone_repo_tag() {
function test_install_path_matches (line 278) | fn test_install_path_matches() {
function test_install_path_matches_nosemver (line 296) | fn test_install_path_matches_nosemver() {
function test_find_install_path_sync (line 310) | fn test_find_install_path_sync() {
function test_find_install_path (line 322) | async fn test_find_install_path() {
FILE: crates/core/src/errors.rs
type SoldeerError (line 9) | pub enum SoldeerError {
type AuthError (line 43) | pub enum AuthError {
type ConfigError (line 65) | pub enum ConfigError {
type DownloadError (line 116) | pub enum DownloadError {
type InstallError (line 147) | pub enum InstallError {
type LockError (line 178) | pub enum LockError {
type PublishError (line 206) | pub enum PublishError {
type RegistryError (line 256) | pub enum RegistryError {
type RemappingsError (line 280) | pub enum RemappingsError {
type UpdateError (line 293) | pub enum UpdateError {
FILE: crates/core/src/install.rs
type Result (line 32) | pub type Result<T> = std::result::Result<T, InstallError>;
type DependencyName (line 35) | pub struct DependencyName(String);
method from (line 46) | fn from(value: &T) -> Self {
type Target (line 38) | type Target = String;
method deref (line 40) | fn deref(&self) -> &Self::Target {
type InstallMonitoring (line 53) | pub struct InstallMonitoring {
type InstallProgress (line 75) | pub struct InstallProgress {
method new (line 98) | pub fn new() -> (Self, InstallMonitoring) {
method log (line 126) | pub fn log(&self, msg: impl fmt::Display) {
method update_all (line 133) | pub fn update_all(&self, dependency_name: DependencyName) {
type DependencyStatus (line 156) | pub enum DependencyStatus {
type HttpInstallInfo (line 170) | struct HttpInstallInfo {
method fmt (line 192) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type GitInstallInfo (line 201) | struct GitInstallInfo {
method fmt (line 223) | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
type InstallInfo (line 233) | enum InstallInfo {
method from (line 245) | fn from(value: HttpInstallInfo) -> Self {
method from (line 251) | fn from(value: GitInstallInfo) -> Self {
method from_lock (line 257) | async fn from_lock(lock: LockEntry, project_root: Option<PathBuf>) -> ...
type Submodule (line 300) | struct Submodule {
function install_dependencies (line 312) | pub async fn install_dependencies(
function install_dependencies_sequential (line 357) | pub async fn install_dependencies_sequential(
function install_dependency (line 386) | pub async fn install_dependency(
function check_dependency_integrity (line 522) | pub async fn check_dependency_integrity(
function ensure_dependencies_dir (line 536) | pub fn ensure_dependencies_dir(path: impl AsRef<Path>) -> Result<()> {
function install_dependency_inner (line 547) | async fn install_dependency_inner(
function install_subdependencies (line 607) | fn install_subdependencies(
function install_subdependencies_inner (line 661) | async fn install_subdependencies_inner(paths: Paths) -> Result<()> {
function install_http_dependency (line 679) | async fn install_http_dependency(
function get_submodules (line 734) | async fn get_submodules(path: &PathBuf) -> Result<HashMap<String, Submod...
function reinit_submodules (line 760) | async fn reinit_submodules(path: &PathBuf) -> Result<Vec<PathBuf>> {
function check_http_dependency (line 802) | async fn check_http_dependency(
function check_git_dependency (line 834) | async fn check_git_dependency(
function reset_git_dependency (line 885) | async fn reset_git_dependency(lock: &GitLockEntry, deps: impl AsRef<Path...
function get_subdependency_root (line 896) | async fn get_subdependency_root(
function mock_api_server (line 932) | async fn mock_api_server() -> ServerGuard {
function mock_api_private (line 953) | async fn mock_api_private() -> ServerGuard {
function test_check_http_dependency (line 975) | async fn test_check_http_dependency() {
function test_check_git_dependency (line 1016) | async fn test_check_git_dependency() {
function test_reset_git_dependency (line 1060) | async fn test_reset_git_dependency() {
function test_install_dependency_inner_http (line 1087) | async fn test_install_dependency_inner_http() {
function test_install_dependency_inner_git (line 1110) | async fn test_install_dependency_inner_git() {
function test_install_dependency_inner_git_rev (line 1131) | async fn test_install_dependency_inner_git_rev() {
function test_install_dependency_inner_git_branch (line 1153) | async fn test_install_dependency_inner_git_branch() {
function test_install_dependency_inner_git_tag (line 1175) | async fn test_install_dependency_inner_git_tag() {
function test_install_dependency_registry (line 1197) | async fn test_install_dependency_registry() {
function test_install_dependency_registry_compatible (line 1225) | async fn test_install_dependency_registry_compatible() {
function test_install_dependency_http (line 1249) | async fn test_install_dependency_http() {
function test_install_dependency_git (line 1269) | async fn test_install_dependency_git() {
function test_install_dependency_private (line 1289) | async fn test_install_dependency_private() {
FILE: crates/core/src/lib.rs
type Result (line 5) | pub type Result<T> = std::result::Result<T, SoldeerError>;
FILE: crates/core/src/lock.rs
constant SOLDEER_LOCK (line 19) | pub const SOLDEER_LOCK: &str = "soldeer.lock";
type Result (line 21) | pub type Result<T> = std::result::Result<T, LockError>;
type Integrity (line 24) | pub trait Integrity {
method install_path (line 26) | fn install_path(&self, deps: impl AsRef<Path>) -> PathBuf;
method integrity (line 29) | fn integrity(&self) -> Option<&String>;
method install_path (line 56) | fn install_path(&self, deps: impl AsRef<Path>) -> PathBuf {
method integrity (line 61) | fn integrity(&self) -> Option<&String> {
method install_path (line 97) | fn install_path(&self, deps: impl AsRef<Path>) -> PathBuf {
method integrity (line 102) | fn integrity(&self) -> Option<&String> {
method install_path (line 138) | fn install_path(&self, deps: impl AsRef<Path>) -> PathBuf {
method integrity (line 143) | fn integrity(&self) -> Option<&String> {
type GitLockEntry (line 37) | pub struct GitLockEntry {
type HttpLockEntry (line 71) | pub struct HttpLockEntry {
type PrivateLockEntry (line 115) | pub struct PrivateLockEntry {
type LockEntry (line 170) | pub enum LockEntry {
type Error (line 234) | type Error = LockError;
method try_from (line 237) | fn try_from(value: TomlLockEntry) -> std::result::Result<Self, Self::E...
method name (line 285) | pub fn name(&self) -> &str {
method version (line 294) | pub fn version(&self) -> &str {
method install_path (line 303) | pub fn install_path(&self, deps: impl AsRef<Path>) -> PathBuf {
method as_http (line 312) | pub fn as_http(&self) -> Option<&HttpLockEntry> {
method as_git (line 317) | pub fn as_git(&self) -> Option<&GitLockEntry> {
method as_private (line 322) | pub fn as_private(&self) -> Option<&PrivateLockEntry> {
method from (line 329) | fn from(value: HttpLockEntry) -> Self {
method from (line 336) | fn from(value: GitLockEntry) -> Self {
method from (line 343) | fn from(value: PrivateLockEntry) -> Self {
type TomlLockEntry (line 188) | pub struct TomlLockEntry {
method from (line 200) | fn from(value: LockEntry) -> Self {
type LockFileParsed (line 352) | struct LockFileParsed {
type LockFile (line 362) | pub struct LockFile {
function read_lockfile (line 371) | pub fn read_lockfile(path: impl AsRef<Path>) -> Result<LockFile> {
function generate_lockfile_contents (line 392) | pub fn generate_lockfile_contents(mut entries: Vec<LockEntry>) -> String {
function add_to_lockfile (line 402) | pub fn add_to_lockfile(entry: LockEntry, path: impl AsRef<Path>) -> Resu...
function remove_lock (line 420) | pub fn remove_lock(dependency: &Dependency, path: impl AsRef<Path>) -> R...
function format_install_path (line 448) | pub fn format_install_path(name: &str, version: &str, deps: impl AsRef<P...
function test_toml_to_lock_entry_conversion_http (line 458) | fn test_toml_to_lock_entry_conversion_http() {
function test_toml_to_lock_entry_conversion_git (line 480) | fn test_toml_to_lock_entry_conversion_git() {
function test_toml_lock_entry_bad_http (line 501) | fn test_toml_lock_entry_bad_http() {
function test_toml_lock_entry_bad_private (line 534) | fn test_toml_lock_entry_bad_private() {
function test_toml_lock_entry_bad_git (line 552) | fn test_toml_lock_entry_bad_git() {
function test_read_lockfile (line 582) | fn test_read_lockfile() {
function test_generate_lockfile_content (line 623) | fn test_generate_lockfile_content() {
function test_add_to_lockfile (line 646) | fn test_add_to_lockfile() {
function test_replace_in_lockfile (line 672) | fn test_replace_in_lockfile() {
function test_remove_lock (line 698) | fn test_remove_lock() {
function test_remove_lock_empty (line 724) | fn test_remove_lock_empty() {
FILE: crates/core/src/lock/forge.rs
constant FOUNDRY_LOCK (line 17) | pub const FOUNDRY_LOCK: &str = "foundry.lock";
type DepMap (line 20) | pub type DepMap = HashMap<PathBuf, DepIdentifier>;
type Lockfile (line 24) | pub struct Lockfile {
method new (line 39) | pub fn new(project_root: &Path) -> Self {
method read (line 46) | pub fn read(&mut self) -> Result<()> {
method get (line 61) | pub fn get(&self, path: &Path) -> Option<&DepIdentifier> {
method len (line 66) | pub fn len(&self) -> usize {
method is_empty (line 71) | pub fn is_empty(&self) -> bool {
method iter (line 76) | pub fn iter(&self) -> impl Iterator<Item = (&PathBuf, &DepIdentifier)> {
method exists (line 80) | pub fn exists(&self) -> bool {
type DepIdentifier (line 93) | pub enum DepIdentifier {
method rev (line 109) | pub fn rev(&self) -> &str {
method name (line 120) | pub fn name(&self) -> &str {
method checkout_id (line 129) | pub fn checkout_id(&self) -> &str {
method is_branch (line 138) | pub fn is_branch(&self) -> bool {
method fmt (line 144) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
FILE: crates/core/src/push.rs
type Result (line 24) | pub type Result<T> = std::result::Result<T, PublishError>;
function push_version (line 35) | pub async fn push_version(
function validate_name (line 74) | pub fn validate_name(name: &str) -> Result<()> {
function validate_version (line 87) | pub fn validate_version(version: &str) -> Result<()> {
function zip_file (line 98) | pub fn zip_file(
function filter_ignored_files (line 164) | pub fn filter_ignored_files(root_directory_path: impl AsRef<Path>) -> Ve...
function push_to_repo (line 206) | async fn push_to_repo(
function test_validate_name (line 272) | fn test_validate_name() {
function test_empty_version (line 289) | fn test_empty_version() {
function test_filter_files_to_copy (line 294) | fn test_filter_files_to_copy() {
function test_zip_file (line 363) | async fn test_zip_file() {
FILE: crates/core/src/registry.rs
type Result (line 17) | pub type Result<T> = std::result::Result<T, RegistryError>;
type Revision (line 22) | pub struct Revision {
type Project (line 51) | pub struct Project {
type RevisionResponse (line 89) | pub struct RevisionResponse {
type ProjectResponse (line 100) | pub struct ProjectResponse {
type DownloadUrl (line 111) | pub struct DownloadUrl {
function api_url (line 139) | pub fn api_url(version: &str, path: &str, params: &[(&str, &str)]) -> Url {
function get_dependency_url_remote (line 151) | pub async fn get_dependency_url_remote(
function get_project_id (line 173) | pub async fn get_project_id(dependency_name: &str) -> Result<String> {
function get_latest_version (line 187) | pub async fn get_latest_version(dependency_name: &str) -> Result<Depende...
type Versions (line 216) | pub enum Versions {
function get_all_versions_descending (line 229) | pub async fn get_all_versions_descending(dependency_name: &str) -> Resul...
function get_latest_supported_version (line 267) | pub async fn get_latest_supported_version(dependency: &Dependency) -> Re...
function parse_version_req (line 313) | pub fn parse_version_req(version_req: &str) -> Option<VersionReq> {
function test_get_dependency_url (line 345) | async fn test_get_dependency_url() {
function test_get_dependency_url_nomatch (line 371) | async fn test_get_dependency_url_nomatch() {
function test_get_project_id (line 393) | async fn test_get_project_id() {
function test_get_project_id_nomatch (line 411) | async fn test_get_project_id_nomatch() {
function test_get_latest_forge_std (line 429) | async fn test_get_latest_forge_std() {
function test_get_all_versions_descending (line 452) | async fn test_get_all_versions_descending() {
function test_get_latest_supported_version_semver (line 481) | async fn test_get_latest_supported_version_semver() {
function test_get_latest_supported_version_no_semver (line 504) | async fn test_get_latest_supported_version_no_semver() {
function test_parse_version_req (line 536) | fn test_parse_version_req() {
FILE: crates/core/src/remappings.rs
type Result (line 19) | pub type Result<T> = std::result::Result<T, RemappingsError>;
type RemappingsAction (line 24) | pub enum RemappingsAction {
type RemappingsLocation (line 39) | pub enum RemappingsLocation {
function remappings_txt (line 59) | pub fn remappings_txt(
function remappings_foundry (line 98) | pub fn remappings_foundry(
function edit_remappings (line 157) | pub fn edit_remappings(
function format_remap_name (line 189) | pub fn format_remap_name(soldeer_config: &SoldeerConfig, dependency: &De...
function generate_remappings (line 208) | fn generate_remappings(
type RemappingInfo (line 334) | struct RemappingInfo {
function remappings_from_deps (line 344) | fn remappings_from_deps(
function get_install_dir_relative (line 363) | fn get_install_dir_relative(dependency: &Dependency, paths: &Paths) -> R...
function format_array (line 398) | fn format_array(array: &mut Array) {
function test_get_install_dir_relative (line 419) | fn test_get_install_dir_relative() {
function test_format_remap_name (line 450) | fn test_format_remap_name() {
function test_remappings_from_deps (line 504) | fn test_remappings_from_deps() {
function test_generate_remappings_add (line 531) | fn test_generate_remappings_add() {
function test_generate_remappings_remove (line 564) | fn test_generate_remappings_remove() {
function test_generate_remappings_update (line 593) | fn test_generate_remappings_update() {
function test_remappings_foundry_default_profile_empty (line 644) | fn test_remappings_foundry_default_profile_empty() {
function test_remappings_foundry_second_profile_empty (line 671) | fn test_remappings_foundry_second_profile_empty() {
function test_remappings_foundry_two_profiles (line 702) | fn test_remappings_foundry_two_profiles() {
function test_remappings_foundry_keep_existing (line 742) | fn test_remappings_foundry_keep_existing() {
function test_remappings_txt_keep (line 770) | fn test_remappings_txt_keep() {
function test_remappings_txt_regenerate (line 788) | fn test_remappings_txt_regenerate() {
function test_remappings_txt_missing (line 806) | fn test_remappings_txt_missing() {
function test_edit_remappings_soldeer_config (line 829) | fn test_edit_remappings_soldeer_config() {
function test_generate_remappings_update_semver_custom (line 847) | fn test_generate_remappings_update_semver_custom() {
function test_generate_remappings_duplicates (line 873) | fn test_generate_remappings_duplicates() {
FILE: crates/core/src/update.rs
type Result (line 14) | pub type Result<T> = std::result::Result<T, UpdateError>;
function update_dependencies (line 31) | pub async fn update_dependencies(
function update_dependency (line 72) | pub async fn update_dependency(
FILE: crates/core/src/utils.rs
type IntegrityChecksum (line 29) | pub struct IntegrityChecksum(pub String);
function login_file_path (line 38) | pub fn login_file_path() -> Result<PathBuf, std::io::Error> {
function check_dotfiles (line 59) | pub fn check_dotfiles(files: &[PathBuf]) -> bool {
function sanitize_filename (line 66) | pub fn sanitize_filename(dependency_name: &str) -> String {
function hash_content (line 76) | pub fn hash_content<R: Read>(content: &mut R) -> [u8; 32] {
function hash_folder (line 93) | pub fn hash_folder(folder_path: impl AsRef<Path>) -> Result<IntegrityChe...
function hash_file (line 165) | pub fn hash_file(path: impl AsRef<Path>) -> Result<IntegrityChecksum, st...
function run_git_command (line 178) | pub async fn run_git_command<I, S>(
function run_forge_command (line 211) | pub async fn run_forge_command<I, S>(
function remove_forge_lib (line 239) | pub async fn remove_forge_lib(root: impl AsRef<Path>) -> Result<(), Inst...
function canonicalize (line 269) | pub async fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf, std...
function canonicalize_sync (line 278) | pub fn canonicalize_sync(path: impl AsRef<Path>) -> Result<PathBuf, std:...
function path_matches (line 288) | pub fn path_matches(dependency: &Dependency, path: impl AsRef<Path>) -> ...
function create_test_folder (line 316) | fn create_test_folder(name: Option<&str>) -> PathBuf {
function test_hash_content (line 339) | fn test_hash_content() {
function test_hash_content_content_sensitive (line 349) | fn test_hash_content_content_sensitive() {
function test_hash_file (line 358) | fn test_hash_file() {
function test_hash_folder_abs_path_insensitive (line 366) | fn test_hash_folder_abs_path_insensitive() {
function test_hash_folder_rel_path_sensitive (line 384) | fn test_hash_folder_rel_path_sensitive() {
function test_hash_folder_content_sensitive (line 393) | fn test_hash_folder_content_sensitive() {
Condensed preview — 56 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (489K chars).
[
{
"path": ".config/nextest.toml",
"chars": 168,
"preview": "[profile.default]\nretries = { backoff = \"exponential\", count = 2, delay = \"2s\", jitter = true }\nslow-timeout = { period "
},
{
"path": ".github/CODE_OF_CONDUCT.md",
"chars": 349,
"preview": "The Soldeer project adheres to the\n[Rust Code of Conduct](https://www.rust-lang.org/policies/code-of-conduct).\nThis code"
},
{
"path": ".github/ISSUE_TEMPLATE/bug_report.yml",
"chars": 2183,
"preview": "name: 🐛 Bug Report\ndescription: Report an issue found in Soldeer.\nlabels: ['bug']\nbody:\n - type: markdown\n attribute"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 171,
"preview": "blank_issues_enabled: true\ncontact_links:\n - name: Soldeer Contributors Telegram\n url: https://t.me/+tn6gOCJseD83OTZ"
},
{
"path": ".github/ISSUE_TEMPLATE/feature_request.yml",
"chars": 1248,
"preview": "name: 💡 Feature Request\ndescription: Suggest a feature for Soldeer\nlabels: ['enhancement']\nbody:\n - type: markdown\n "
},
{
"path": ".github/ISSUE_TEMPLATE/registry_request.yml",
"chars": 1301,
"preview": "name: 📦 Registry Addition\ndescription: Suggest a missing package for the Soldeer registry.\nlabels: ['add-dependency']\nas"
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 777,
"preview": "<!--\nBefore submitting a PR, please read https://github.com/mario-eth/soldeer/blob/main/CONTRIBUTING.md\n\n1. Give the PR "
},
{
"path": ".github/dependabot.yml",
"chars": 155,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"github-actions\"\n directory: \"/\"\n # Check for updates every Monday\n "
},
{
"path": ".github/workflows/release.yml",
"chars": 2054,
"preview": "name: Release\n\npermissions:\n pull-requests: write\n contents: write\n\non:\n push:\n branches:\n - main\n\njobs:\n # "
},
{
"path": ".github/workflows/rust.yml",
"chars": 2167,
"preview": "name: Rust\n\non:\n push:\n branches: ['main']\n pull_request:\n\nenv:\n CARGO_TERM_COLOR: always\n\njobs:\n build-test:\n "
},
{
"path": ".gitignore",
"chars": 307,
"preview": "/target\ndependencies/\n.dependency_reading.toml\nremappings.txt\ncrawler/target/\n*.DS_Store*\npackage-lock.json\npackage.json"
},
{
"path": ".vscode/settings.json",
"chars": 236,
"preview": "{\n \"git.ignoreLimitWarning\": true,\n \"editor.formatOnSave\": true,\n \"rust-analyzer.rustfmt.extraArgs\": [\"+nightly\"],\n "
},
{
"path": "CHANGELOG.md",
"chars": 11479,
"preview": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Change"
},
{
"path": "CONTRIBUTING.md",
"chars": 11501,
"preview": "## Contributing to Soldeer\n\nThanks for your interest in improving Soldeer!\n\nThere are multiple opportunities to contribu"
},
{
"path": "Cargo.toml",
"chars": 1336,
"preview": "[workspace]\nmembers = [\"crates/cli\", \"crates/core\", \"crates/commands\"]\nresolver = \"2\"\n\n[workspace.package]\nauthors = [\"m"
},
{
"path": "LICENSE",
"chars": 1066,
"preview": "MIT License\n\nCopyright (c) 2023 mario-eth\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
},
{
"path": "README.md",
"chars": 1945,
"preview": "# Soldeer ![Rust][rust-badge] [![License: MIT][license-badge]][license]\n\n[rust-badge]: https://img.shields.io/badge/Buil"
},
{
"path": "USAGE.md",
"chars": 13046,
"preview": "# Usage Guide\n\n`Soldeer` is straightforward to use. It can either be invoked from the `forge` tool provided by Foundry, "
},
{
"path": "clippy.toml",
"chars": 29,
"preview": "allow-unwrap-in-tests = true\n"
},
{
"path": "crates/cli/Cargo.toml",
"chars": 675,
"preview": "[package]\nname = \"soldeer\"\ndescription.workspace = true\nauthors.workspace = true\ncategories.workspace = true\nedition.wor"
},
{
"path": "crates/cli/src/main.rs",
"chars": 1543,
"preview": "//! Soldeer is a package manager for Solidity projects\nuse std::env;\n\nuse log::Level;\nuse soldeer_commands::{Args, comma"
},
{
"path": "crates/commands/Cargo.toml",
"chars": 874,
"preview": "[package]\nname = \"soldeer-commands\"\ndescription = \"High-level commands for the Soldeer CLI\"\nauthors.workspace = true\ncat"
},
{
"path": "crates/commands/src/commands/clean.rs",
"chars": 668,
"preview": "use crate::utils::success;\nuse clap::Parser;\nuse soldeer_core::{Result, config::Paths};\nuse std::fs;\n\n/// Clean download"
},
{
"path": "crates/commands/src/commands/init.rs",
"chars": 2776,
"preview": "use crate::{\n ConfigLocation,\n utils::{Progress, remark, success},\n};\nuse clap::Parser;\nuse soldeer_core::{\n Re"
},
{
"path": "crates/commands/src/commands/install.rs",
"chars": 8374,
"preview": "use super::validate_dependency;\nuse crate::{\n ConfigLocation,\n utils::{Progress, remark, success, warning},\n};\nuse"
},
{
"path": "crates/commands/src/commands/login.rs",
"chars": 3477,
"preview": "use crate::utils::{info, remark, step, success, warning};\nuse clap::Parser;\nuse email_address_parser::{EmailAddress, Par"
},
{
"path": "crates/commands/src/commands/mod.rs",
"chars": 2116,
"preview": "pub use clap::{Parser, Subcommand};\nuse clap_verbosity_flag::{LogLevel, VerbosityFilter};\nuse derive_more::derive::From;"
},
{
"path": "crates/commands/src/commands/push.rs",
"chars": 3897,
"preview": "use super::validate_dependency;\nuse crate::utils::{info, remark, success, warning};\nuse clap::Parser;\nuse soldeer_core::"
},
{
"path": "crates/commands/src/commands/uninstall.rs",
"chars": 1425,
"preview": "use crate::utils::success;\nuse clap::Parser;\nuse soldeer_core::{\n Result, SoldeerError,\n config::{Paths, delete_fr"
},
{
"path": "crates/commands/src/commands/update.rs",
"chars": 2802,
"preview": "use crate::{\n ConfigLocation,\n utils::{Progress, success, warning},\n};\nuse clap::Parser;\nuse soldeer_core::{\n R"
},
{
"path": "crates/commands/src/lib.rs",
"chars": 5544,
"preview": "//! High-level commands for the Soldeer CLI\n#![cfg_attr(docsrs, feature(doc_cfg))]\npub use crate::commands::{Args, Comma"
},
{
"path": "crates/commands/src/utils.rs",
"chars": 7795,
"preview": "#![allow(unused_macros)]\n//! Utils for the commands crate\nuse std::{fmt, path::Path};\n\nuse crate::ConfigLocation;\nuse cl"
},
{
"path": "crates/commands/tests/tests-clean.rs",
"chars": 9328,
"preview": "use soldeer_commands::{\n Command, Verbosity,\n commands::{clean::Clean, install::Install},\n run,\n};\nuse soldeer_"
},
{
"path": "crates/commands/tests/tests-init.rs",
"chars": 6271,
"preview": "use soldeer_commands::{Command, Verbosity, commands::init::Init, run};\nuse soldeer_core::{\n config::{ConfigLocation, "
},
{
"path": "crates/commands/tests/tests-install.rs",
"chars": 33092,
"preview": "#![allow(clippy::unwrap_used)]\nuse mockito::Matcher;\nuse soldeer_commands::{Command, Verbosity, commands::install::Insta"
},
{
"path": "crates/commands/tests/tests-login.rs",
"chars": 3366,
"preview": "use std::{fs, path::PathBuf};\n\nuse mockito::{Matcher, Mock, ServerGuard};\nuse soldeer_commands::{Command, Verbosity, com"
},
{
"path": "crates/commands/tests/tests-push.rs",
"chars": 9420,
"preview": "use mockito::{Matcher, Mock, ServerGuard};\nuse reqwest::StatusCode;\nuse soldeer_commands::{Verbosity, commands::push::Pu"
},
{
"path": "crates/commands/tests/tests-uninstall.rs",
"chars": 3404,
"preview": "use soldeer_commands::{\n Command, Verbosity,\n commands::{install::Install, uninstall::Uninstall},\n run,\n};\nuse "
},
{
"path": "crates/commands/tests/tests-update.rs",
"chars": 9483,
"preview": "use soldeer_commands::{\n Command, Verbosity,\n commands::{install::Install, update::Update},\n run,\n};\nuse soldee"
},
{
"path": "crates/core/Cargo.toml",
"chars": 1457,
"preview": "[package]\nname = \"soldeer-core\"\ndescription = \"Core functionality for Soldeer\"\nauthors.workspace = true\ncategories.works"
},
{
"path": "crates/core/src/auth.rs",
"chars": 9021,
"preview": "//! Registry authentication\nuse crate::{errors::AuthError, registry::api_url, utils::login_file_path};\nuse log::{debug, "
},
{
"path": "crates/core/src/config.rs",
"chars": 78535,
"preview": "//! Manage the Soldeer configuration and dependencies list.\nuse crate::{\n download::{find_install_path, find_install_"
},
{
"path": "crates/core/src/download.rs",
"chars": 12188,
"preview": "//! Download and/or extract dependencies\nuse crate::{\n config::{Dependency, GitIdentifier},\n errors::DownloadError"
},
{
"path": "crates/core/src/errors.rs",
"chars": 8873,
"preview": "use std::{\n io,\n path::{PathBuf, StripPrefixError},\n};\nuse thiserror::Error;\n\n#[derive(Error, Debug)]\n#[non_exhaus"
},
{
"path": "crates/core/src/install.rs",
"chars": 54346,
"preview": "//! Install dependencies.\n//!\n//! This module contains functions to install dependencies from the config object or from "
},
{
"path": "crates/core/src/lib.rs",
"chars": 380,
"preview": "//! Low-level library for interacting with Soldeer registries and files\n#![cfg_attr(docsrs, feature(doc_cfg))]\npub use e"
},
{
"path": "crates/core/src/lock/forge.rs",
"chars": 4502,
"preview": "//! Vendored version of the `lockfile` module of `forge`.\n//!\n//! Slightly adapted to reduce dependencies.\n\nuse log::deb"
},
{
"path": "crates/core/src/lock.rs",
"chars": 24707,
"preview": "//! Lockfile handling.\n//!\n//! The lockfile contains the resolved dependencies of a project. It is a TOML file with an a"
},
{
"path": "crates/core/src/push.rs",
"chars": 14871,
"preview": "//! Handle publishing of a dependency to the registry.\nuse crate::{\n auth::get_token,\n errors::{AuthError, Publish"
},
{
"path": "crates/core/src/registry.rs",
"chars": 25387,
"preview": "//! Soldeer registry client.\n//!\n//! The registry client is responsible for fetching information about packages from the"
},
{
"path": "crates/core/src/remappings.rs",
"chars": 37513,
"preview": "//! Remappings management.\nuse crate::{\n config::{Dependency, Paths, SoldeerConfig, read_config_deps},\n errors::Re"
},
{
"path": "crates/core/src/update.rs",
"chars": 8372,
"preview": "//! Update dependencies to the latest version.\nuse crate::{\n config::{Dependency, GitIdentifier},\n errors::UpdateE"
},
{
"path": "crates/core/src/utils.rs",
"chars": 15211,
"preview": "//! Utility functions used throughout the codebase.\nuse crate::{\n config::Dependency,\n errors::{DownloadError, Ins"
},
{
"path": "flake.nix",
"chars": 1233,
"preview": "{\n inputs = {\n nixpkgs.url = \"github:NixOS/nixpkgs/nixpkgs-unstable\";\n fenix = {\n url = \"github:nix-communit"
},
{
"path": "release-plz.toml",
"chars": 1236,
"preview": "[workspace]\ndependencies_update = true\ngit_release_enable = false # we only need to create a git tag for one of t"
},
{
"path": "rustfmt.toml",
"chars": 305,
"preview": "reorder_imports = true\nimports_granularity = \"Crate\"\nuse_small_heuristics = \"Max\"\ncomment_width = 100\nwrap_comments = tr"
}
]
About this extraction
This page contains the full source code of the mario-eth/soldeer GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 56 files (455.1 KB), approximately 116.9k tokens, and a symbol index with 461 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.