Compare commits

...

6 Commits

  1. 2
      Cargo.lock
  2. 2
      Cargo.toml
  3. 305
      README.md
  4. 207
      doc/README.tera.md
  5. 4
      doc/cli-menu.txt
  6. 32
      doc/just-commands.txt
  7. 19
      doc/publish-cli-menu.txt
  8. 2
      justfile
  9. 23
      publish-config.toml.sample
  10. 4
      src/generate-readme.rs
  11. 6
      src/main.rs
  12. 226
      src/publish.rs

2
Cargo.lock generated

@ -1607,7 +1607,7 @@ checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]] [[package]]
name = "registry-backup" name = "registry-backup"
version = "0.4.1" version = "0.5.0-beta.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",

2
Cargo.toml

@ -1,7 +1,7 @@
[package] [package]
name = "registry-backup" name = "registry-backup"
authors = ["Jonathan Strong <jstrong@shipyard.rs>"] authors = ["Jonathan Strong <jstrong@shipyard.rs>"]
version = "0.4.1" version = "0.5.0-beta.1"
edition = "2021" edition = "2021"
#publish = ["shipyard-rs-public"] #publish = ["shipyard-rs-public"]
readme = "README.md" readme = "README.md"

305
README.md

@ -1,13 +1,25 @@
# registry-backup # registry-backup
A command line utility for downloading all .crate files hosted by a Cargo registry server. Command line utilities for backup, export, and migration of a Rust private crate registry.
Use cases: Use cases:
- **Backup:** retrieve a registry server's files for backup storage - **Backup:** retrieve a registry server's files for backup storage
- **Export:** pull the files so you can host them at another registry server - **Export:** pull the files so you can host them at another registry server
- **Migration:** publish downloaded .crate files to a new private registry, including modifying the `Cargo.toml` manifests of each published crate version to make it compatible with the destination registry
## Example Usage: ## Tools
There are two binaries in the repo:
- `registry-backup`: for downloading all .crate files hosted by a Cargo registry server
- `publish`: for publishing the .crate files downloaded by `registry-backup` to a different registry
## `registry-backup`
`registry-backup` is a tool to download all of the .crate files hosted by a Cargo registry server.
### Example Usage
Specify the registry index either as a local path (`--index-path`)... Specify the registry index either as a local path (`--index-path`)...
@ -28,18 +40,18 @@ $ RUST_LOG=info registry-backup \
--auth-token ${AUTH_TOKEN} # for private registry, need auth --auth-token ${AUTH_TOKEN} # for private registry, need auth
``` ```
## Install ### Install
```console ```console
$ cargo install registry-backup --git https://git.shipyard.rs/jstrong/registry-backup.git $ cargo install registry-backup --git https://git.shipyard.rs/jstrong/registry-backup.git
``` ```
## Runtime Options ### Runtime Options
```console ```console
$ ./target/release/registry-backup --help $ ./target/release/registry-backup --help
registry-backup 0.4.1 registry-backup 0.5.0-beta.1
Jonathan Strong <jstrong@shipyard.rs> Jonathan Strong <jstrong@shipyard.rs>
Download all .crate files from a registry server Download all .crate files from a registry server
@ -82,7 +94,7 @@ OPTIONS:
-U, --user-agent <USER_AGENT> -U, --user-agent <USER_AGENT>
Value of user-agent HTTP header Value of user-agent HTTP header
[default: registry-backup/v0.4.1] [default: registry-backup/v0.5.0-beta.1]
-R, --requests-per-second <INT> -R, --requests-per-second <INT>
Requests to registry server will not exceed this rate Requests to registry server will not exceed this rate
@ -116,7 +128,7 @@ OPTIONS:
``` ```
## Configuration File ### Configuration File
A toml configuration file may be used instead of command line flags. A sample file (`config.toml.sample`) is included. From the example file: A toml configuration file may be used instead of command line flags. A sample file (`config.toml.sample`) is included. From the example file:
@ -152,6 +164,251 @@ $ just release-build # alternatively, cargo build --bin registry-backup --releas
# cp target/release/registry-backup ~/.cargo/bin/ # cp target/release/registry-backup ~/.cargo/bin/
``` ```
## `publish`
`publish` is a tool to publish all of the crate versions from a *source registry* to second *destination registry*.
### Usage Overview
`publish` is different from `registry-backup` in that in requires several steps, including the use of a Python script.
In general, migrating all of the crate versions to another registry is relatively complex, compared to just downloading the .crate files. Migrating to a new registry involves the following (big picture) steps:
1) extracting the order that crate versions were published to the source registry from the git history of the crate index repository
2) extracting the source files, including `Cargo.toml` manifests, from the downloaded `.crate` files
3) modifying the `Cargo.toml` manifests for each crate version so the crate will be compatible with the destination registry
4) publishing the crate versions, in the right order and using the modified `Cargo.toml` manifests, to the destination registry
### Background Context: `cargo publish`, `.crate` Files, and `Cargo.toml.orig`
When you run the `cargo publish` command to publish a crate version to a registry server, it generates an alternate `Cargo.toml` manifest based on the contents of the original `Cargo.toml` in combination with the configured settings with which the command was invoked.
For example, if you had configured a private registry in `~/.cargo/config.toml`:
```toml
# ~/.cargo/config.toml
[registries.my-private-registry]
index = "ssh://git@ssh.shipyard.rs/my-private-registry/crate-index.git"
```
And then added a dependency from that registry in a `Cargo.toml` for a crate:
```toml
# Cargo.toml
[package]
name = "foo"
publish = ["my-private-registry"]
[dependencies]
bar = { version = "1.0", registry = "my-private-registry" }
```
...`cargo publish` would convert the dependency into one with a hard-coded `registry-index` field that points to the specific index URL that was configured at the time it was invoked:
```
# cargo publish-generated Cargo.toml
[package]
name = "foo"
publish = ["my-private-registry"]
[dependencies]
bar = { version = "1.0", registry-index = "ssh://git@ssh.shipyard.rs/my-private-registry/crate-index.git" }
```
`cargo publish` includes the original `Cargo.toml` file at the path `Cargo.toml.orig` in the `.crate` file (actually a `.tar.gz` archive).
Since the `registry-index` entries generated by `cargo publish` point to the specific URL of the source registry, just publishing the `.crate` file as is to the destination registry will not suffice. To resolve this problem, `publish` uses the `Cargo.toml.orig` file contained in the `.crate` file, modifies the dependency entries according to the settings of the destination registry, and publishes them to the destination registry using `cargo publish` (i.e. discard the `cargo publish`-generated `Cargo.toml`, relying instead on the modified `Cargo.toml.orig` in combination with runtime settings provided as env vars to `cargo`).
### The Global Dependency Graph of a Registry and `publish-log.csv`
Once we have solved how to take a `.crate` file from the source registry and publish it to the destination registry, there is still the issue of which order the crate versions should be published. If crate `a` version 1.2.3 depends on crate `b` version 2.3.4, then crate `b` version 2.3.4 needs to have already been published to the registry at the time crate `a` version 1.2.3 is published, otherwise it will depend on a crate that does not (yet) exist (in the destination registry, at least). If you try to publish crates without respecting this global dependency graph using `cargo publish`, it will exit with an error, and it's not a good idea otherwise, either.
Building a dependency graph for the entire registry is certainly possible, theoretically. However, in practice it is tedious to do, mainly because it requires mirroring `cargo`'s dependency resolution process, just to be able to identify the full set of dependencies that would end up in the `Cargo.lock` file. That, in turn, requires using `cargo` (i.e. via the `cargo metadata` command), which is slow for large registries (only a single `cargo metadata` command can run at a time due to the use of lock files), and quite involved in terms of parsing the programmatically-generated outputs (wow it is amazing how many different forms crate metadata is represented in various `cargo`/registry contexts!).
To shortcut these complexities, `publish` relies on the use of a Python script to extract the order in which crate versions were published to a registry using the git history of the crate index repository.
The tool (`script/get-publish-history.py`) was based on an open source script that utilizes the `GitPython` library to traverse the commit history of a repo. In a few minutes work, we were able to modify the script to extract the publish order of all the crate versions appearing in the crate index repository. And, as much as we love Rust (and do not share the same passion for Python), porting the code to Rust using the `git2` crate appeared like quite a tedious project itself.
To generate a `.csv` file with the order in which crates were published, first clone the crate index repository, e.g.:
```
$ git clone ssh://git@ssh.shipyard.rs/my-private-registry/crate-index.git
```
Then run the script (it has two dependencies `GitPython` and `pandas`, both of which can be `pip install`ed or otherwise acquired using whatever terrible Python package manager you want):
```
$ python script/get-publish-history.py path/to/crate-index > publish-log.csv
```
You will need a `publish-log.csv` generated from the source registry to use `publish`.
(You might be wondering why we are relying on git history to reconstruct the publishing order. The primary reason is the crate index metadata (or any other metadata universally available from a crate registry) does not include any information about when each crate version was published.)
### Detailed Usage Example
##### 1) Clone the source registry crate index repository:
```
$ mkdir source-registry
$ git clone <source registry crate index repo url> source-registry/crate-index
```
##### 2) Use `registry-backup` to download all the `.crate` files from the source registry:
```
$ cargo install registry-backup --git https://git.shipyard.rs/jstrong/registry-backup.git # or build from source
$ RUST_LOG=info registry-backup \
--index-path source-registry/crate-index \
--output-path source-registry/crate-files
```
##### 3) Use the `get-publish-history.py` script to extract the crate version publish history:
```
$ . ../virtualenvs/my-env/activate # or whatever you use
$ pip install GitPython
$ pip install pandas
$ python3 script/get-publish-history.py source-registry/crate-index > source-registry/publish-log.csv
```
##### 4) Create a configuration file:
```toml
# publish-config.toml
# source registry config
[src]
index-dir = "source-registry/crate-index" # <- see step 1
crate-files-dir = "source-registry/crate-files" # <- see step 2
publish-history-csv = "source-registry/publish-log.csv" # <- see step 3
registry-name = "my-old-registry" # <- whatever label the source registry was given in Cargo.toml files
index-url = "https://github.com/my-org/crate-index.git" # <- index url, i.e. same as one provided in ~/.cargo/config.toml
# destination registry config
[dst]
index-url = "ssh://git@ssh.shipyard.rs/my-new-registry/crate-index.git"
registry-name = "my-new-registry" # can be same as old name or a different name
auth-token = "xxx" # auth token for publishing to the destination registry
```
##### 5) Build `publish`:
```
$ cargo bulid --bin publish --features publish --release
```
##### 6) Validate your config file (optional):
```
$ ./target/release/publish --config publish-config.toml --validate
```
##### 7) Publish to the destination registry using `publish`:
```
$ RUST_LOG=info ./target/release/publish --config publish-config.toml
```
### Expected Runtime
As an example, using `publish`, it took us about 50 minutes to migrate a registry with 77 crates and 937 versions. Results may vary based on the machine used to run `publish` as well as the performance of the destination registry server.
### Building `publish` (Full Example)
```
$ git clone https://git.shipyard.rs/jstrong/registry-backup.git
$ cd registry-backup
$ just release-build-publish # alternately, cargo build --bin publish --features publish --release
```
Note: `--release` really is quite a bit faster, at least for larger registries.
### Configuration File
Annotated example configuration file:
```toml
# optional field for providing a regex-based filter
# to limit which crates are published to the destination
# registry. only crates with names matching the regex will
# be published.
#
filter-crates = "^."
# do everything except actually publish to the destination registry
dry-run = false
# source registry config
[src]
index-dir = "path/to/crate-index/repo" # git clone of crate index repository
crate-files-dir = "path/to/crate/files" # i.e. files downloaded by registry-backup tool
publish-history-csv = "path/to/publish-log.csv" # see docs above
registry-name = "my-old-registry" # whatever label the source registry was given in Cargo.toml files
index-url = "https://github.com/my-org/crate-index.git" # index url, i.e. same as one provided in ~/.cargo/config.toml
# destination registry config
[dst]
index-url = "ssh://git@ssh.shipyard.rs/my-new-registry/crate-index.git" # index url of new registry
registry-name = "my-new-registry" # can be same as old name or a different name
auth-token = "xxx" # auth token for publishing to the destination registry
```
### Runtime Options
```console
$ ./target/release/publish --help
registry-backup 0.5.0-beta.1
Jonathan Strong <jstrong@shipyard.rs>
USAGE:
publish [OPTIONS] --config-file <PATH>
OPTIONS:
-c, --config-file <PATH> Config file with source directories and destination registry info
--dry-run Perform all the work of generating `cargo publish` payloads, but
don't send them to the destination registry server
--validate Load config file, validate the settings, and display the final
loaded content to stdout, then exit
--filter-crates <REGEX> Use to limit which crates from the source registry are published
to the destination registry. Expects a regular expression which
will be matched against the names of crates. Only crates with
names that match the regex will be published. This field may also
be specified at the top level of the config file
-h, --help Print help information
-V, --version Print version information
```
### Configuration File
A toml configuration file may be used instead of command line flags. A sample file (`config.toml.sample`) is included. From the example file:
```toml
dry-run = false
filter-crates = "^."
[registry]
index-url = "ssh://git@ssh.shipyard.rs/shipyard-rs-public/crate-index.git"
# alternatively, specify a local dir
# index-path = "/path/to/cloned/index"
auth-token = "xxx"
[http]
user-agent = "registry-backup/v0.1.0"
requests-per-second = 100
max-concurrent-requests = 50
[output]
path = "output"
overwrite-existing = false
format = "{crate}/{version}/download"
```
## Running Tests ## Running Tests
```console ```console
@ -168,22 +425,24 @@ Included commands:
$ just --list $ just --list
Available recipes: Available recipes:
cargo +args='' # cargo wrapper; executes a cargo command using the settings in justfile (RUSTFLAGS, etc.) cargo +args='' # cargo wrapper; executes a cargo command using the settings in justfile (RUSTFLAGS, etc.)
check +args='' # cargo check wrapper check +args='' # cargo check wrapper
debug-build +args='' # cargo build wrapper - builds registry-backup in debug mode debug-build +args='' # cargo build wrapper - builds registry-backup in debug mode
generate-readme # generate updated README.md debug-build-publish +args='' # cargo build wrapper - builds publish tool in debug mode
generate-readme # generate updated README.md
get-crate-version get-crate-version
install # cargo install registry-backup via git dep install # cargo install registry-backup via git dep
pre-release # check, run tests, check non-error output for clippy, run rustfmt pre-release # check, run tests, check non-error output for clippy, run rustfmt
release # release version (regenerate docs, git tag v0.0.0) release # release version (regenerate docs, git tag v0.0.0)
release-build +args='' # cargo build --release wrapper - builds registry-backup in release mode release-build +args='' # cargo build --release wrapper - builds registry-backup in release mode
release-prep # get everything all ready for release release-build-publish +args='' # cargo build --release wrapper - builds publish tool in release mode
show-build-env # diagnostic command for viewing value of build variables at runtime release-prep # get everything all ready for release
test +args='' # cargo test wrapper show-build-env # diagnostic command for viewing value of build variables at runtime
update-readme # re-generate README.md and overwrite existing file with output test +args='' # cargo test wrapper
update-readme-and-commit # re-generate, overwrite, stage, and commit update-readme # re-generate README.md and overwrite existing file with output
update-readme-and-stage # re-generate, overwrite, and stage changes update-readme-and-commit # re-generate, overwrite, stage, and commit
verify-clean-git # verify no uncommitted changes update-readme-and-stage # re-generate, overwrite, and stage changes
verify-clean-git # verify no uncommitted changes
``` ```
@ -193,7 +452,7 @@ The commands that mirror cargo commands (e.g. `just test`) are included for the
This file is generated using a template (`doc/README.tera.md`) rendered using updated outputs of the CLI menu, config sample, and other values. This file is generated using a template (`doc/README.tera.md`) rendered using updated outputs of the CLI menu, config sample, and other values.
This version of `README.md` was generated at `Thu, 08 Dec 2022 02:23:17 +0000` based on git commit `3241e207`. This version of `README.md` was generated at `Fri, 10 Nov 2023 01:30:48 +0000` based on git commit `4c2a9e5f`.
To (re-)generate the `README.md` file, use the justfile command: To (re-)generate the `README.md` file, use the justfile command:

207
doc/README.tera.md

@ -1,13 +1,25 @@
# registry-backup # registry-backup
A command line utility for downloading all .crate files hosted by a Cargo registry server. Command line utilities for backup, export, and migration of a Rust private crate registry.
Use cases: Use cases:
- **Backup:** retrieve a registry server's files for backup storage - **Backup:** retrieve a registry server's files for backup storage
- **Export:** pull the files so you can host them at another registry server - **Export:** pull the files so you can host them at another registry server
- **Migration:** publish downloaded .crate files to a new private registry, including modifying the `Cargo.toml` manifests of each published crate version to make it compatible with the destination registry
## Example Usage: ## Tools
There are two binaries in the repo:
- `registry-backup`: for downloading all .crate files hosted by a Cargo registry server
- `publish`: for publishing the .crate files downloaded by `registry-backup` to a different registry
## `registry-backup`
`registry-backup` is a tool to download all of the .crate files hosted by a Cargo registry server.
### Example Usage
Specify the registry index either as a local path (`--index-path`)... Specify the registry index either as a local path (`--index-path`)...
@ -28,13 +40,13 @@ $ RUST_LOG=info registry-backup \
--auth-token ${AUTH_TOKEN} # for private registry, need auth --auth-token ${AUTH_TOKEN} # for private registry, need auth
``` ```
## Install ### Install
```console ```console
$ cargo install registry-backup --git https://git.shipyard.rs/jstrong/registry-backup.git $ cargo install registry-backup --git https://git.shipyard.rs/jstrong/registry-backup.git
``` ```
## Runtime Options ### Runtime Options
```console ```console
$ ./target/release/registry-backup --help $ ./target/release/registry-backup --help
@ -42,7 +54,7 @@ $ ./target/release/registry-backup --help
{{ cli_menu }} {{ cli_menu }}
``` ```
## Configuration File ### Configuration File
A toml configuration file may be used instead of command line flags. A sample file (`config.toml.sample`) is included. From the example file: A toml configuration file may be used instead of command line flags. A sample file (`config.toml.sample`) is included. From the example file:
@ -60,6 +72,191 @@ $ just release-build # alternatively, cargo build --bin registry-backup --releas
# cp target/release/registry-backup ~/.cargo/bin/ # cp target/release/registry-backup ~/.cargo/bin/
``` ```
## `publish`
`publish` is a tool to publish all of the crate versions from a *source registry* to second *destination registry*.
### Usage Overview
`publish` is different from `registry-backup` in that in requires several steps, including the use of a Python script.
In general, migrating all of the crate versions to another registry is relatively complex, compared to just downloading the .crate files. Migrating to a new registry involves the following (big picture) steps:
1) extracting the order that crate versions were published to the source registry from the git history of the crate index repository
2) extracting the source files, including `Cargo.toml` manifests, from the downloaded `.crate` files
3) modifying the `Cargo.toml` manifests for each crate version so the crate will be compatible with the destination registry
4) publishing the crate versions, in the right order and using the modified `Cargo.toml` manifests, to the destination registry
### Background Context: `cargo publish`, `.crate` Files, and `Cargo.toml.orig`
When you run the `cargo publish` command to publish a crate version to a registry server, it generates an alternate `Cargo.toml` manifest based on the contents of the original `Cargo.toml` in combination with the configured settings with which the command was invoked.
For example, if you had configured a private registry in `~/.cargo/config.toml`:
```toml
# ~/.cargo/config.toml
[registries.my-private-registry]
index = "ssh://git@ssh.shipyard.rs/my-private-registry/crate-index.git"
```
And then added a dependency from that registry in a `Cargo.toml` for a crate:
```toml
# Cargo.toml
[package]
name = "foo"
publish = ["my-private-registry"]
[dependencies]
bar = { version = "1.0", registry = "my-private-registry" }
```
...`cargo publish` would convert the dependency into one with a hard-coded `registry-index` field that points to the specific index URL that was configured at the time it was invoked:
```
# cargo publish-generated Cargo.toml
[package]
name = "foo"
publish = ["my-private-registry"]
[dependencies]
bar = { version = "1.0", registry-index = "ssh://git@ssh.shipyard.rs/my-private-registry/crate-index.git" }
```
`cargo publish` includes the original `Cargo.toml` file at the path `Cargo.toml.orig` in the `.crate` file (actually a `.tar.gz` archive).
Since the `registry-index` entries generated by `cargo publish` point to the specific URL of the source registry, just publishing the `.crate` file as is to the destination registry will not suffice. To resolve this problem, `publish` uses the `Cargo.toml.orig` file contained in the `.crate` file, modifies the dependency entries according to the settings of the destination registry, and publishes them to the destination registry using `cargo publish` (i.e. discard the `cargo publish`-generated `Cargo.toml`, relying instead on the modified `Cargo.toml.orig` in combination with runtime settings provided as env vars to `cargo`).
### The Global Dependency Graph of a Registry and `publish-log.csv`
Once we have solved how to take a `.crate` file from the source registry and publish it to the destination registry, there is still the issue of which order the crate versions should be published. If crate `a` version 1.2.3 depends on crate `b` version 2.3.4, then crate `b` version 2.3.4 needs to have already been published to the registry at the time crate `a` version 1.2.3 is published, otherwise it will depend on a crate that does not (yet) exist (in the destination registry, at least). If you try to publish crates without respecting this global dependency graph using `cargo publish`, it will exit with an error, and it's not a good idea otherwise, either.
Building a dependency graph for the entire registry is certainly possible, theoretically. However, in practice it is tedious to do, mainly because it requires mirroring `cargo`'s dependency resolution process, just to be able to identify the full set of dependencies that would end up in the `Cargo.lock` file. That, in turn, requires using `cargo` (i.e. via the `cargo metadata` command), which is slow for large registries (only a single `cargo metadata` command can run at a time due to the use of lock files), and quite involved in terms of parsing the programmatically-generated outputs (wow it is amazing how many different forms crate metadata is represented in various `cargo`/registry contexts!).
To shortcut these complexities, `publish` relies on the use of a Python script to extract the order in which crate versions were published to a registry using the git history of the crate index repository.
The tool (`script/get-publish-history.py`) was based on an open source script that utilizes the `GitPython` library to traverse the commit history of a repo. In a few minutes work, we were able to modify the script to extract the publish order of all the crate versions appearing in the crate index repository. And, as much as we love Rust (and do not share the same passion for Python), porting the code to Rust using the `git2` crate appeared like quite a tedious project itself.
To generate a `.csv` file with the order in which crates were published, first clone the crate index repository, e.g.:
```
$ git clone ssh://git@ssh.shipyard.rs/my-private-registry/crate-index.git
```
Then run the script (it has two dependencies `GitPython` and `pandas`, both of which can be `pip install`ed or otherwise acquired using whatever terrible Python package manager you want):
```
$ python script/get-publish-history.py path/to/crate-index > publish-log.csv
```
You will need a `publish-log.csv` generated from the source registry to use `publish`.
(You might be wondering why we are relying on git history to reconstruct the publishing order. The primary reason is the crate index metadata (or any other metadata universally available from a crate registry) does not include any information about when each crate version was published.)
### Detailed Usage Example
##### 1) Clone the source registry crate index repository:
```
$ mkdir source-registry
$ git clone <source registry crate index repo url> source-registry/crate-index
```
##### 2) Use `registry-backup` to download all the `.crate` files from the source registry:
```
$ cargo install registry-backup --git https://git.shipyard.rs/jstrong/registry-backup.git # or build from source
$ RUST_LOG=info registry-backup \
--index-path source-registry/crate-index \
--output-path source-registry/crate-files
```
##### 3) Use the `get-publish-history.py` script to extract the crate version publish history:
```
$ . ../virtualenvs/my-env/activate # or whatever you use
$ pip install GitPython
$ pip install pandas
$ python3 script/get-publish-history.py source-registry/crate-index > source-registry/publish-log.csv
```
##### 4) Create a configuration file:
```toml
# publish-config.toml
# source registry config
[src]
index-dir = "source-registry/crate-index" # <- see step 1
crate-files-dir = "source-registry/crate-files" # <- see step 2
publish-history-csv = "source-registry/publish-log.csv" # <- see step 3
registry-name = "my-old-registry" # <- whatever label the source registry was given in Cargo.toml files
index-url = "https://github.com/my-org/crate-index.git" # <- index url, i.e. same as one provided in ~/.cargo/config.toml
# destination registry config
[dst]
index-url = "ssh://git@ssh.shipyard.rs/my-new-registry/crate-index.git"
registry-name = "my-new-registry" # can be same as old name or a different name
auth-token = "xxx" # auth token for publishing to the destination registry
```
##### 5) Build `publish`:
```
$ cargo bulid --bin publish --features publish --release
```
##### 6) Validate your config file (optional):
```
$ ./target/release/publish --config publish-config.toml --validate
```
##### 7) Publish to the destination registry using `publish`:
```
$ RUST_LOG=info ./target/release/publish --config publish-config.toml
```
### Expected Runtime
As an example, using `publish`, it took us about 50 minutes to migrate a registry with 77 crates and 937 versions. Results may vary based on the machine used to run `publish` as well as the performance of the destination registry server.
### Building `publish` (Full Example)
```
$ git clone https://git.shipyard.rs/jstrong/registry-backup.git
$ cd registry-backup
$ just release-build-publish # alternately, cargo build --bin publish --features publish --release
```
Note: `--release` really is quite a bit faster, at least for larger registries.
### Configuration File
Annotated example configuration file:
```toml
{{ publish_config_sample }}
```
### Runtime Options
```console
$ ./target/release/publish --help
{{ publish_cli_menu }}
```
### Configuration File
A toml configuration file may be used instead of command line flags. A sample file (`config.toml.sample`) is included. From the example file:
```toml
{{ config_sample }}
```
## Running Tests ## Running Tests
```console ```console

4
doc/cli-menu.txt

@ -1,4 +1,4 @@
registry-backup 0.4.1 registry-backup 0.5.0-beta.1
Jonathan Strong <jstrong@shipyard.rs> Jonathan Strong <jstrong@shipyard.rs>
Download all .crate files from a registry server Download all .crate files from a registry server
@ -41,7 +41,7 @@ OPTIONS:
-U, --user-agent <USER_AGENT> -U, --user-agent <USER_AGENT>
Value of user-agent HTTP header Value of user-agent HTTP header
[default: registry-backup/v0.4.1] [default: registry-backup/v0.5.0-beta.1]
-R, --requests-per-second <INT> -R, --requests-per-second <INT>
Requests to registry server will not exceed this rate Requests to registry server will not exceed this rate

32
doc/just-commands.txt

@ -1,17 +1,19 @@
Available recipes: Available recipes:
cargo +args='' # cargo wrapper; executes a cargo command using the settings in justfile (RUSTFLAGS, etc.) cargo +args='' # cargo wrapper; executes a cargo command using the settings in justfile (RUSTFLAGS, etc.)
check +args='' # cargo check wrapper check +args='' # cargo check wrapper
debug-build +args='' # cargo build wrapper - builds registry-backup in debug mode debug-build +args='' # cargo build wrapper - builds registry-backup in debug mode
generate-readme # generate updated README.md debug-build-publish +args='' # cargo build wrapper - builds publish tool in debug mode
generate-readme # generate updated README.md
get-crate-version get-crate-version
install # cargo install registry-backup via git dep install # cargo install registry-backup via git dep
pre-release # check, run tests, check non-error output for clippy, run rustfmt pre-release # check, run tests, check non-error output for clippy, run rustfmt
release # release version (regenerate docs, git tag v0.0.0) release # release version (regenerate docs, git tag v0.0.0)
release-build +args='' # cargo build --release wrapper - builds registry-backup in release mode release-build +args='' # cargo build --release wrapper - builds registry-backup in release mode
release-prep # get everything all ready for release release-build-publish +args='' # cargo build --release wrapper - builds publish tool in release mode
show-build-env # diagnostic command for viewing value of build variables at runtime release-prep # get everything all ready for release
test +args='' # cargo test wrapper show-build-env # diagnostic command for viewing value of build variables at runtime
update-readme # re-generate README.md and overwrite existing file with output test +args='' # cargo test wrapper
update-readme-and-commit # re-generate, overwrite, stage, and commit update-readme # re-generate README.md and overwrite existing file with output
update-readme-and-stage # re-generate, overwrite, and stage changes update-readme-and-commit # re-generate, overwrite, stage, and commit
verify-clean-git # verify no uncommitted changes update-readme-and-stage # re-generate, overwrite, and stage changes
verify-clean-git # verify no uncommitted changes

19
doc/publish-cli-menu.txt

@ -0,0 +1,19 @@
registry-backup 0.5.0-beta.1
Jonathan Strong <jstrong@shipyard.rs>
USAGE:
publish [OPTIONS] --config-file <PATH>
OPTIONS:
-c, --config-file <PATH> Config file with source directories and destination registry info
--dry-run Perform all the work of generating `cargo publish` payloads, but
don't send them to the destination registry server
--validate Load config file, validate the settings, and display the final
loaded content to stdout, then exit
--filter-crates <REGEX> Use to limit which crates from the source registry are published
to the destination registry. Expects a regular expression which
will be matched against the names of crates. Only crates with
names that match the regex will be published. This field may also
be specified at the top level of the config file
-h, --help Print help information
-V, --version Print version information

2
justfile

@ -38,6 +38,8 @@ release-build-publish +args='':
generate-readme: generate-readme:
just debug-build just debug-build
./target/debug/registry-backup --help > doc/cli-menu.txt ./target/debug/registry-backup --help > doc/cli-menu.txt
just debug-build-publish
./target/debug/publish --help > doc/publish-cli-menu.txt
just --list > doc/just-commands.txt just --list > doc/just-commands.txt
just cargo run --bin generate-readme --features docs just cargo run --bin generate-readme --features docs

23
publish-config.toml.sample

@ -3,22 +3,21 @@
# registry. only crates with names matching the regex will # registry. only crates with names matching the regex will
# be published. # be published.
# #
# filter-crates = "" filter-crates = "^."
# do everything except actually publish to the destination registry # do everything except actually publish to the destination registry
dry-run = false dry-run = false
# source registry config
[src] [src]
# path of local dir where crate index repository has been cloned index-dir = "path/to/crate-index/repo" # git clone of crate index repository
index-dir = "path/to/cloned/rrate-index/repo" crate-files-dir = "path/to/crate/files" # i.e. files downloaded by registry-backup tool
# path of dir where .crate files were downloaded to. use the publish-history-csv = "path/to/publish-log.csv" # see docs above
# registry-backup tool to quickly and easily download all of registry-name = "my-old-registry" # whatever label the source registry was given in Cargo.toml files
# a registry's files index-url = "https://github.com/my-org/crate-index.git" # index url, i.e. same as one provided in ~/.cargo/config.toml
crate-files-dir = "path/to/crate/files"
# destination registry config
[dst] [dst]
# the value of the `api` field in the destination registry's index-url = "ssh://git@ssh.shipyard.rs/my-new-registry/crate-index.git" # index url of new registry
# config.json file (part of the crate index registry-name = "my-new-registry" # can be same as old name or a different name
api-url = "https://crates.shipyard.rs" auth-token = "xxx" # auth token for publishing to the destination registry
# auth token to use when publishing crate versions
auth-token = "xxx"

4
src/generate-readme.rs

@ -2,8 +2,10 @@ use chrono::prelude::*;
const README_TEMPLATE: &str = include_str!("../doc/README.tera.md"); const README_TEMPLATE: &str = include_str!("../doc/README.tera.md");
const CLI_MENU: &str = include_str!("../doc/cli-menu.txt"); const CLI_MENU: &str = include_str!("../doc/cli-menu.txt");
const PUBLISH_CLI_MENU: &str = include_str!("../doc/publish-cli-menu.txt");
const JUST_COMMANDS: &str = include_str!("../doc/just-commands.txt"); const JUST_COMMANDS: &str = include_str!("../doc/just-commands.txt");
const CONFIG_SAMPLE: &str = include_str!("../config.toml.sample"); const CONFIG_SAMPLE: &str = include_str!("../config.toml.sample");
const PUBLISH_CONFIG_SAMPLE: &str = include_str!("../publish-config.toml.sample");
fn get_commit_ref() -> Result<String, Box<dyn std::error::Error>> { fn get_commit_ref() -> Result<String, Box<dyn std::error::Error>> {
let output = std::process::Command::new("git") let output = std::process::Command::new("git")
@ -21,7 +23,9 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
tera.add_raw_template("README.md", README_TEMPLATE).unwrap(); tera.add_raw_template("README.md", README_TEMPLATE).unwrap();
let mut ctx = tera::Context::new(); let mut ctx = tera::Context::new();
ctx.insert("cli_menu", CLI_MENU); ctx.insert("cli_menu", CLI_MENU);
ctx.insert("publish_cli_menu", PUBLISH_CLI_MENU);
ctx.insert("config_sample", CONFIG_SAMPLE); ctx.insert("config_sample", CONFIG_SAMPLE);
ctx.insert("publish_config_sample", PUBLISH_CONFIG_SAMPLE);
ctx.insert("just_commands", JUST_COMMANDS); ctx.insert("just_commands", JUST_COMMANDS);
ctx.insert("git_commit", &get_commit_ref()?); ctx.insert("git_commit", &get_commit_ref()?);
ctx.insert("generation_time", &Utc::now().to_rfc2822()); ctx.insert("generation_time", &Utc::now().to_rfc2822());

6
src/main.rs

@ -87,7 +87,7 @@ impl RegistryConfig {
out out
} else { } else {
Path::new(&self.dl) Path::new(&self.dl)
.join(&format!( .join(format!(
"{name}/{version}/download", "{name}/{version}/download",
name = name, name = name,
version = version, version = version,
@ -797,10 +797,10 @@ fn main() -> Result<(), anyhow::Error> {
setup_logger(); setup_logger();
info!("initializing...");
let config = Config::parse(); let config = Config::parse();
info!("initializing...");
let rt = tokio::runtime::Builder::new_multi_thread() let rt = tokio::runtime::Builder::new_multi_thread()
.enable_all() .enable_all()
.build() .build()

226
src/publish.rs

@ -1,22 +1,22 @@
#![allow(unused_labels)] #![allow(unused_labels)]
use std::path::{Path, PathBuf};
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};
use std::io::{self, prelude::*}; use std::io::{self, prelude::*};
use std::path::{Path, PathBuf};
use std::time::*; use std::time::*;
use serde::{Serialize, Deserialize}; use anyhow::{anyhow, bail, Context, Error};
use chrono::prelude::*;
use clap::Parser; use clap::Parser;
use tracing::{debug, error, info, trace, warn}; use convert_case::{Case, Casing};
use tracing_subscriber::filter::EnvFilter;
use anyhow::{anyhow, bail, Error, Context};
use semver::Version;
use futures::stream::StreamExt; use futures::stream::StreamExt;
use tokio::io::AsyncBufReadExt;
use tempfile::TempDir;
use rayon::prelude::*; use rayon::prelude::*;
use chrono::prelude::*; use semver::Version;
use convert_case::{Case, Casing}; use serde::{Deserialize, Serialize};
use tempfile::TempDir;
use tokio::io::AsyncBufReadExt;
use tracing::{debug, error, info, trace, warn};
use tracing_subscriber::filter::EnvFilter;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
#[clap(author, version, global_setting(clap::AppSettings::DeriveDisplayOrder))] #[clap(author, version, global_setting(clap::AppSettings::DeriveDisplayOrder))]
@ -130,10 +130,10 @@ fn csv_setup(path: &Path) -> Result<CsvSetup, Error> {
let file = std::fs::File::open(path)?; let file = std::fs::File::open(path)?;
let buf = std::io::BufReader::new(file); let buf = std::io::BufReader::new(file);
let mut rdr = csv::Reader::from_reader(buf); let mut rdr = csv::Reader::from_reader(buf);
let headers = let headers = rdr
rdr.byte_headers() .byte_headers()
.map_err(|e| anyhow!("failed to parse csv headers: {}", e))? .map_err(|e| anyhow!("failed to parse csv headers: {}", e))?
.clone(); .clone();
let row = csv::ByteRecord::new(); let row = csv::ByteRecord::new();
Ok(CsvSetup { rdr, headers, row }) Ok(CsvSetup { rdr, headers, row })
} }
@ -258,17 +258,14 @@ fn extract_manifest_files_from_tar<R: Read>(rdr: R) -> Result<ManifestFiles, Err
cargo_lock = Some(data); cargo_lock = Some(data);
} }
if cargo_toml.is_some() if cargo_toml.is_some() && cargo_toml_orig.is_some() && cargo_lock.is_some() {
&& cargo_toml_orig.is_some() break;
&& cargo_lock.is_some()
{
break
} }
} }
if !(cargo_toml.is_some() && cargo_toml_orig.is_some()) if !(cargo_toml.is_some() && cargo_toml_orig.is_some()) {
{ anyhow::bail!(
anyhow::bail!("some required manifest files missing in .crate archive \ "some required manifest files missing in .crate archive \
(cargo_toml={:?} cargo_toml_orig={:?} cargo_lock={:?})", (cargo_toml={:?} cargo_toml_orig={:?} cargo_lock={:?})",
cargo_toml.is_some(), cargo_toml.is_some(),
cargo_toml_orig.is_some(), cargo_toml_orig.is_some(),
@ -296,9 +293,10 @@ fn load_config_file(opt: &Opt) -> Result<Config, Error> {
bail!("path does not exist: {:?}", opt.config_file); bail!("path does not exist: {:?}", opt.config_file);
} }
let toml = std::fs::read_to_string(&opt.config_file)?; let toml = std::fs::read_to_string(&opt.config_file)?;
let mut config: Config = toml::from_str(&toml) let mut config: Config = toml::from_str(&toml).context(
.context("read config file, but unable to parse toml - check \ "read config file, but unable to parse toml - check \
format against example config")?; format against example config",
)?;
// augment using command line opts // augment using command line opts
config.filter_crates = config.filter_crates.or_else(|| opt.filter_crates.clone()); config.filter_crates = config.filter_crates.or_else(|| opt.filter_crates.clone());
config.dry_run |= opt.dry_run; config.dry_run |= opt.dry_run;
@ -313,9 +311,7 @@ fn is_hidden(entry: &walkdir::DirEntry) -> bool {
.unwrap_or(false) .unwrap_or(false)
} }
async fn get_index_metas( async fn get_index_metas(config: &Config) -> Result<HashMap<String, Vec<IndexMeta>>, Error> {
config: &Config,
) -> Result<HashMap<String, Vec<IndexMeta>>, Error> {
let filter = config.compile_filter()?; let filter = config.compile_filter()?;
let mut n_excl = 0; let mut n_excl = 0;
@ -362,29 +358,26 @@ async fn get_index_metas(
} }
let crate_versions: Vec<Result<(String, Vec<IndexMeta>), Error>> = let crate_versions: Vec<Result<(String, Vec<IndexMeta>), Error>> =
futures::stream::iter(files.into_iter().map(|(crate_name, path)| { futures::stream::iter(files.into_iter().map(|(crate_name, path)| async move {
async move { let file = tokio::fs::File::open(&path).await.map_err(|e| {
let file = tokio::fs::File::open(&path).await.map_err(|e| { error!(err = ?e, ?path, "failed to open file");
error!(err = ?e, ?path, "failed to open file"); e
})?;
let buf = tokio::io::BufReader::new(file);
let mut out = Vec::new();
let mut lines = buf.lines();
'lines: while let Some(line) = lines.next_line().await? {
let index_meta: IndexMeta = serde_json::from_str(&line).map_err(|e| {
error!(err = ?e, ?path, "failed to parse line");
e e
})?; })?;
let buf = tokio::io::BufReader::new(file); out.push(index_meta);
let mut out = Vec::new();
let mut lines = buf.lines();
'lines: while let Some(line) = lines.next_line().await? {
let index_meta: IndexMeta = serde_json::from_str(&line)
.map_err(|e| {
error!(err = ?e, ?path, "failed to parse line");
e
})?;
out.push(index_meta);
}
debug!(crate_name = %out.first().map(|x| x.name.as_str()).unwrap_or("na"),
"parsed {} crate versions from metadata file", out.len()
);
Ok((crate_name, out))
} }
debug!(crate_name = %out.first().map(|x| x.name.as_str()).unwrap_or("na"),
"parsed {} crate versions from metadata file", out.len()
);
Ok((crate_name, out))
})) }))
.buffer_unordered(num_cpus::get()) .buffer_unordered(num_cpus::get())
.collect() .collect()
@ -452,7 +445,9 @@ struct VersionMeta {
impl VersionMeta { impl VersionMeta {
pub fn source_dir(&self) -> PathBuf { pub fn source_dir(&self) -> PathBuf {
self.tmp.path().join(&format!("{}-{}", self.index_meta.name, self.index_meta.vers)) self.tmp
.path()
.join(format!("{}-{}", self.index_meta.name, self.index_meta.vers))
} }
} }
@ -469,30 +464,32 @@ fn parse_one_manifest(
) -> Result<VersionMeta, Error> { ) -> Result<VersionMeta, Error> {
let version = index_meta.vers.clone(); let version = index_meta.vers.clone();
trace!(%crate_name, %version, "processing crate version"); trace!(%crate_name, %version, "processing crate version");
let dot_crate_path = config.src.crate_files_dir let dot_crate_path = config
.join(&format!("{}/{}/download", crate_name, index_meta.vers)); .src
.crate_files_dir
.join(format!("{}/{}/download", crate_name, index_meta.vers));
verify_file_exists(&dot_crate_path)?; verify_file_exists(&dot_crate_path)?;
trace!(path = ?dot_crate_path, "reading .crate file"); trace!(path = ?dot_crate_path, "reading .crate file");
let dot_crate_bytes = std::fs::read(&dot_crate_path) let dot_crate_bytes = std::fs::read(&dot_crate_path).with_context(|| {
.with_context(|| { format!(
format!("failed to read .crate file for \ "failed to read .crate file for \
{crate_name} v{0} with path {dot_crate_path:?}", {crate_name} v{0} with path {dot_crate_path:?}",
index_meta.vers, index_meta.vers,
) )
})?; })?;
trace!("extracting Cargo.toml from .crate targz archive"); trace!("extracting Cargo.toml from .crate targz archive");
let decoder = flate2::read::GzDecoder::new(&dot_crate_bytes[..]); let decoder = flate2::read::GzDecoder::new(&dot_crate_bytes[..]);
let manifest_files = extract_manifest_files_from_tar(decoder) let manifest_files = extract_manifest_files_from_tar(decoder).map_err(|err| {
.map_err(|err| { error!(%crate_name, vers = %index_meta.vers, ?err, "failed to extract manifest files");
error!(%crate_name, vers = %index_meta.vers, ?err, "failed to extract manifest files"); err
err })?;
})?;
let tmp = TempDir::new()?; let tmp = TempDir::new()?;
let decoder = flate2::read::GzDecoder::new(&dot_crate_bytes[..]); let decoder = flate2::read::GzDecoder::new(&dot_crate_bytes[..]);
tar::Archive::new(decoder).unpack(tmp.path()) tar::Archive::new(decoder)
.unpack(tmp.path())
.map_err(|err| { .map_err(|err| {
error!(%crate_name, vers = %index_meta.vers, ?err, "failed to unpack to temp dir"); error!(%crate_name, vers = %index_meta.vers, ?err, "failed to unpack to temp dir");
err err
@ -500,7 +497,7 @@ fn parse_one_manifest(
trace!(tmpdir = ?tmp.path(), "unpacked .crate archive to temp dir"); trace!(tmpdir = ?tmp.path(), "unpacked .crate archive to temp dir");
let target_dir = tmp.path().join("target"); let target_dir = tmp.path().join("target");
std::fs::create_dir(&target_dir)?; std::fs::create_dir(target_dir)?;
Ok(VersionMeta { Ok(VersionMeta {
index_meta, index_meta,
@ -559,10 +556,7 @@ fn parse_manifests(
/// [target.'cfg(not(target_env = "msvc"))'.dependencies] /// [target.'cfg(not(target_env = "msvc"))'.dependencies]
/// dep-one = { version = "0.1.0", registry = "old-registry" } /// dep-one = { version = "0.1.0", registry = "old-registry" }
/// ``` /// ```
fn edit_deps( fn edit_deps(manifest: &mut toml_edit::Document, config: &Config) {
manifest: &mut toml_edit::Document,
config: &Config,
) {
use toml_edit::{visit_mut::VisitMut, TableLike}; use toml_edit::{visit_mut::VisitMut, TableLike};
struct DepsVisitor<'a>(&'a Config); struct DepsVisitor<'a>(&'a Config);
@ -609,21 +603,30 @@ fn edit_publish_registry(
src_registry_name: &str, src_registry_name: &str,
dst_registry_name: &str, dst_registry_name: &str,
) -> Result<(), Error> { ) -> Result<(), Error> {
let Some(package) = manifest.get_mut("package").and_then(|item| item.as_table_like_mut()) else { let Some(package) = manifest
.get_mut("package")
.and_then(|item| item.as_table_like_mut())
else {
anyhow::bail!("package key not found in manifest toml"); anyhow::bail!("package key not found in manifest toml");
}; };
let Some(publish_item) = package.get_mut("publish") else { let Some(publish_item) = package.get_mut("publish") else {
trace!("no 'publish' key in Cargo.toml package section"); trace!("no 'publish' key in Cargo.toml package section");
return Ok(()) return Ok(());
}; };
let Some(publish_array) = publish_item.as_array_mut() else { let Some(publish_array) = publish_item.as_array_mut() else {
anyhow::bail!("failed to cast publish item as array"); anyhow::bail!("failed to cast publish item as array");
}; };
let Some(i) = publish_array.iter().position(|x| x.as_str().map(|s| s == src_registry_name).unwrap_or(false)) else { let Some(i) = publish_array
anyhow::bail!("publish key exists, but source registry name does not appear in it! (`{}`)", publish_array.to_string()); .iter()
.position(|x| x.as_str().map(|s| s == src_registry_name).unwrap_or(false))
else {
anyhow::bail!(
"publish key exists, but source registry name does not appear in it! (`{}`)",
publish_array.to_string()
);
}; };
let item_i = publish_array.get_mut(i).unwrap(); let item_i = publish_array.get_mut(i).unwrap();
@ -634,10 +637,17 @@ fn edit_publish_registry(
fn prepare_source_dir_for_publish(config: &Config, meta: &mut VersionMeta) -> Result<(), Error> { fn prepare_source_dir_for_publish(config: &Config, meta: &mut VersionMeta) -> Result<(), Error> {
let source_dir = meta.source_dir(); let source_dir = meta.source_dir();
let mut modified_manifest = meta.manifest_files.cargo_toml_orig.parse::<toml_edit::Document>()?; let mut modified_manifest = meta
.manifest_files
edit_deps(&mut modified_manifest, &config); .cargo_toml_orig
edit_publish_registry(&mut modified_manifest, &config.src.registry_name, &config.dst.registry_name)?; .parse::<toml_edit::Document>()?;
edit_deps(&mut modified_manifest, config);
edit_publish_registry(
&mut modified_manifest,
&config.src.registry_name,
&config.dst.registry_name,
)?;
// write modified manifest over Cargo.toml (leaves Cargo.toml.orig as is) // write modified manifest over Cargo.toml (leaves Cargo.toml.orig as is)
let modified_manifest_toml = modified_manifest.to_string(); let modified_manifest_toml = modified_manifest.to_string();
@ -675,12 +685,15 @@ fn prepare_source_dir_for_publish(config: &Config, meta: &mut VersionMeta) -> Re
Ok(()) Ok(())
} }
fn prepare_source_dirs_for_publish(config: &Config, manifests: &mut HashMap<String, Vec<VersionMeta>>) -> Result<(), Error> { fn prepare_source_dirs_for_publish(
config: &Config,
manifests: &mut HashMap<String, Vec<VersionMeta>>,
) -> Result<(), Error> {
let begin = Instant::now(); let begin = Instant::now();
manifests.par_iter_mut() manifests.par_iter_mut()
.map(|(name, versions)| -> Result<(), Error> { .map(|(name, versions)| -> Result<(), Error> {
for meta in versions.iter_mut() { for meta in versions.iter_mut() {
prepare_source_dir_for_publish(&config, meta) prepare_source_dir_for_publish(config, meta)
.map_err(|err| { .map_err(|err| {
error!(%name, vers = %meta.index_meta.vers, ?err, "prepare_source_dir_for_publish failed"); error!(%name, vers = %meta.index_meta.vers, ?err, "prepare_source_dir_for_publish failed");
err err
@ -695,8 +708,14 @@ fn prepare_source_dirs_for_publish(config: &Config, manifests: &mut HashMap<Stri
fn cargo_publish_modified_source_dir(config: &Config, meta: &VersionMeta) -> Result<(), Error> { fn cargo_publish_modified_source_dir(config: &Config, meta: &VersionMeta) -> Result<(), Error> {
let begin = Instant::now(); let begin = Instant::now();
info!(name = %meta.index_meta.name, vers = %meta.index_meta.vers, "publishing crate version"); info!(name = %meta.index_meta.name, vers = %meta.index_meta.vers, "publishing crate version");
let index_env_key = format!("CARGO_REGISTRIES_{}_INDEX", config.dst.registry_name.to_case(Case::ScreamingSnake)); let index_env_key = format!(
let token_env_key = format!("CARGO_REGISTRIES_{}_TOKEN", config.dst.registry_name.to_case(Case::ScreamingSnake)); "CARGO_REGISTRIES_{}_INDEX",
config.dst.registry_name.to_case(Case::ScreamingSnake)
);
let token_env_key = format!(
"CARGO_REGISTRIES_{}_TOKEN",
config.dst.registry_name.to_case(Case::ScreamingSnake)
);
let source_dir = meta.source_dir(); let source_dir = meta.source_dir();
let manifest_path = source_dir.join("Cargo.toml"); let manifest_path = source_dir.join("Cargo.toml");
@ -723,8 +742,14 @@ fn cargo_publish_modified_source_dir(config: &Config, meta: &VersionMeta) -> Res
let stderr = std::str::from_utf8(&output.stderr).unwrap_or("utf8err"); let stderr = std::str::from_utf8(&output.stderr).unwrap_or("utf8err");
error!(exit_status = ?output.status, "cargo publish error!\nstdout:\n{}\nstderr:\n:{}\n\n", stdout, stderr); error!(exit_status = ?output.status, "cargo publish error!\nstdout:\n{}\nstderr:\n:{}\n\n", stdout, stderr);
if !stderr.contains("already exists") { if !stderr.contains("already exists") {
debug!("cargo publish error - original Cargo.toml:\n***\n{}\n***", meta.manifest_files.cargo_toml_orig); debug!(
debug!("cargo publish error - modified Cargo.toml:\n***\n{}\n***", meta.modified_manifest_toml.as_ref().unwrap()); "cargo publish error - original Cargo.toml:\n***\n{}\n***",
meta.manifest_files.cargo_toml_orig
);
debug!(
"cargo publish error - modified Cargo.toml:\n***\n{}\n***",
meta.modified_manifest_toml.as_ref().unwrap()
);
} }
} }
@ -762,15 +787,18 @@ fn verify_file_exists<P: AsRef<std::path::Path>>(path: P) -> Result<(), Error> {
fn read_publish_log_csv(path: &Path) -> Result<Vec<PublishLogRow>, Error> { fn read_publish_log_csv(path: &Path) -> Result<Vec<PublishLogRow>, Error> {
let begin = Instant::now(); let begin = Instant::now();
let CsvSetup { mut rdr, headers, mut row } = csv_setup(path)?; let CsvSetup {
mut rdr,
headers,
mut row,
} = csv_setup(path)?;
let mut out = Vec::new(); let mut out = Vec::new();
while rdr.read_byte_record(&mut row)? { while rdr.read_byte_record(&mut row)? {
// only partially deserialized after this // only partially deserialized after this
let parsed: PublishLogRow = row.deserialize(Some(&headers)) let parsed: PublishLogRow = row.deserialize(Some(&headers)).map_err(|err| {
.map_err(|err| { error!(?row, ?headers, ?err, "deserializing row failed");
error!(?row, ?headers, ?err, "deserializing row failed"); err
err })?;
})?;
out.push(parsed); out.push(parsed);
} }
info!(?path, "parsed publish log csv in {:?}", begin.elapsed()); info!(?path, "parsed publish log csv in {:?}", begin.elapsed());
@ -796,7 +824,7 @@ fn main() -> Result<(), Error> {
if opt.validate { if opt.validate {
println!("{:#?}", config); println!("{:#?}", config);
return Ok(()) return Ok(());
} }
let mut publish_log = read_publish_log_csv(&config.src.publish_history_csv)?; let mut publish_log = read_publish_log_csv(&config.src.publish_history_csv)?;
@ -805,9 +833,7 @@ fn main() -> Result<(), Error> {
info!(n_rows = publish_log.len(), "parsed publish log csv"); info!(n_rows = publish_log.len(), "parsed publish log csv");
if let Some(filter) = config.compile_filter()? { if let Some(filter) = config.compile_filter()? {
publish_log.retain(|x| { publish_log.retain(|x| filter.is_match(&x.crate_name));
filter.is_match(&x.crate_name)
});
info!(n_filtered_rows = publish_log.len(), "filtered publish log"); info!(n_filtered_rows = publish_log.len(), "filtered publish log");
} }
@ -817,16 +843,18 @@ fn main() -> Result<(), Error> {
prepare_source_dirs_for_publish(&config, &mut manifests)?; prepare_source_dirs_for_publish(&config, &mut manifests)?;
let mut by_name_vers: HashMap<(&str, &Version), &VersionMeta> = manifests.iter() let mut by_name_vers: HashMap<(&str, &Version), &VersionMeta> = manifests
.flat_map(|(k, v)| { .iter()
v.iter().map(|m| ((k.as_str(), &m.index_meta.vers), m)) .flat_map(|(k, v)| v.iter().map(|m| ((k.as_str(), &m.index_meta.vers), m)))
}).collect(); .collect();
for row in publish_log.iter() { for row in publish_log.iter() {
let Some(meta) = by_name_vers.remove(&(row.crate_name.as_str(), &row.version)) else { let Some(meta) = by_name_vers.remove(&(row.crate_name.as_str(), &row.version)) else {
warn!(?row, "crate version in publish log not found in index versions"); warn!(
continue ?row,
"crate version in publish log not found in index versions"
);
continue;
}; };
if let Err(err) = cargo_publish_modified_source_dir(&config, meta) { if let Err(err) = cargo_publish_modified_source_dir(&config, meta) {

Loading…
Cancel
Save