| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627 |
- using Invercargill;
- using Invercargill.DataStructures;
- private int dcba_main(string[] args) {
- if(args.length < 3) {
- return dcba_usage();
- }
- if(args[2] == "repository") {
- return dcba_repository_main(args);
- }
- if(args[2] == "add-repo") {
- return Usm.Cli.DcbaAddRepo.run(args);
- }
- return dcba_usage();
- }
- private int dcba_usage() {
- printerr("USAGE:\n\tusm dcba repository <init|add|remove|list|verify|publish|build-list>\n\tusm dcba add-repo <url-or-path>\n");
- return 255;
- }
- private int dcba_repository_main(string[] args) {
- if(args.length < 4) {
- return dcba_repository_usage();
- }
- if(args[2] != "repository") {
- return dcba_repository_usage();
- }
- if(args[3] == "init") {
- if(args.length != 5) {
- return dcba_repository_usage();
- }
- return dcba_repository_init(args[4]);
- }
- if(args[3] == "add") {
- if(args.length != 5) {
- return dcba_repository_usage();
- }
- return dcba_repository_add(args[4]);
- }
- if(args[3] == "remove") {
- if(args.length != 5) {
- return dcba_repository_usage();
- }
- return dcba_repository_remove(args[4]);
- }
- if(args[3] == "list") {
- if(args.length > 5) {
- return dcba_repository_usage();
- }
- return dcba_repository_list(args.length == 5 ? args[4] : null);
- }
- if(args[3] == "verify") {
- if(args.length != 5) {
- return dcba_repository_usage();
- }
- return dcba_repository_verify(args[4]);
- }
- if(args[3] == "publish") {
- if(args.length != 5) {
- return dcba_repository_usage();
- }
- return dcba_repository_publish(args[4]);
- }
- if(args[3] == "build-list") {
- if(args.length != 4) {
- return dcba_repository_usage();
- }
- return dcba_repository_build_list();
- }
- return dcba_repository_usage();
- }
- private int dcba_repository_usage() {
- printerr("USAGE:\n\tusm dcba repository init <repository name>\n\tusm dcba repository add <artifact.dcba>\n\tusm dcba repository remove <name|name-version|artifact-filename>\n\tusm dcba repository list [repo.dcbar]\n\tusm dcba repository verify repo.dcbar\n\tusm dcba repository publish <path>\n\tusm dcba repository build-list\n");
- return 255;
- }
- private int dcba_repository_init(string name) {
- if(dcba_find_repo_file(Environment.get_current_dir()) != null
- || FileUtils.test(Path.build_filename("keys", "private-key"), FileTest.EXISTS)) {
- printerr("A DCBA repository has already been initialised in this directory\n");
- return 1;
- }
- uint8[] public_key;
- uint8[] private_key;
- dcba_generate_keypair(out public_key, out private_key);
- DirUtils.create("keys", 0700);
- DirUtils.create("public", 0755);
- try {
- FileUtils.set_data(Path.build_filename("keys", "public-key"), public_key);
- FileUtils.set_data(Path.build_filename("keys", "private-key"), private_key);
- FileUtils.chmod(Path.build_filename("keys", "private-key"), 0600);
- var repository = new Usm.Repository();
- repository.key = Wrap.byte_array(public_key);
- repository.name = name;
- repository.url = @"file://$(Environment.get_current_dir())/public";
- repository.summary = @"My $name DCBA repository";
- var properties = Usm.Repository.get_mapper().map_from(repository);
- var json = new InvercargillJson.JsonElement.from_properties(properties);
- json.write_to_file(@"$name.dcbar");
- var gitignore = "keys/private*\n";
- if(FileUtils.test(".gitignore", FileTest.EXISTS)) {
- string existing;
- FileUtils.get_contents(".gitignore", out existing);
- if(!existing.has_suffix("\n")) {
- existing += "\n";
- }
- if(existing.index_of("keys/private*") == -1) {
- FileUtils.set_contents(".gitignore", existing + gitignore);
- }
- }
- else {
- FileUtils.set_contents(".gitignore", gitignore);
- }
- }
- catch(Error e) {
- printerr(@"Error: $(e.message)\n");
- return e.code;
- }
- printerr("DCBA repository initialised, add artifacts with `usm dcba repository add <name-version.dcba>` then publish them with `usm dcba repository publish <path>`\n");
- return 0;
- }
- /**
- * Locates the nearest DCBA repository root: the first directory at or
- * above the current directory holding a `*.dcbar` file, a `keys/`
- * directory and a `public/` directory — the `.dcbar`-suffix counterpart
- * of `repository_find_root`, deliberately separate so a USM source
- * repository and a DCBA repository can never be mistaken for one
- * another even when nested in the same tree.
- */
- private string? dcba_find_root() {
- var directory = Environment.get_current_dir();
- while(true) {
- if(dcba_find_repo_file(directory) != null
- && FileUtils.test(Path.build_filename(directory, "keys"), FileTest.IS_DIR)
- && FileUtils.test(Path.build_filename(directory, "public"), FileTest.IS_DIR)) {
- return directory;
- }
- var parent = Path.get_dirname(directory);
- if(parent == directory) {
- return null;
- }
- directory = parent;
- }
- }
- /**
- * Finds the first `*.dcbar` file directly inside `root`, or null when
- * there is none.
- */
- private string? dcba_find_repo_file(string root) {
- try {
- foreach(var file in Iterate.directory(root)) {
- if(file.has_suffix(".dcbar")) {
- return Path.build_filename(root, file);
- }
- }
- }
- catch(Error e) {
- return null;
- }
- return null;
- }
- /**
- * Loads the signing keys from `keys/public-key` and `keys/private-key` in
- * the DCBA repository root, falling back to the legacy loose
- * `public-key` and `private-key` files there.
- */
- private bool dcba_load_keys(string root, out uint8[] public_key, out uint8[] private_key) {
- public_key = new uint8[0];
- private_key = new uint8[0];
- var key_dir = Path.build_filename(root, "keys");
- if(dcba_read_key(Path.build_filename(key_dir, "public-key"), out public_key)
- && dcba_read_key(Path.build_filename(key_dir, "private-key"), out private_key)) {
- return true;
- }
- if(dcba_read_key(Path.build_filename(root, "public-key"), out public_key)
- && dcba_read_key(Path.build_filename(root, "private-key"), out private_key)) {
- return true;
- }
- printerr("No signing keys found in \"keys/\" (or as legacy \"public-key\"/\"private-key\") within the DCBA repository root\n");
- return false;
- }
- private bool dcba_read_key(string path, out uint8[] key) {
- key = new uint8[0];
- if(!FileUtils.test(path, FileTest.EXISTS)) {
- return false;
- }
- try {
- FileUtils.get_data(path, out key);
- return true;
- }
- catch(Error e) {
- printerr(@"Error: $(e.message)\n");
- return false;
- }
- }
- private void dcba_generate_keypair(out uint8[] public_key, out uint8[] private_key) {
- public_key = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
- private_key = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
- Sodium.Asymmetric.Signing.generate_keypair(public_key, private_key);
- }
- /**
- * Validates that the file at `path` is a readable tar.xz archive by
- * listing it through `tar -tf` — the same archive family USM's build
- * artifacts (`build.tar.xz`) and `.dcba` files use.
- */
- private void dcba_validate_tar_xz(string path) throws Error {
- if(!FileUtils.test(path, FileTest.EXISTS)) {
- throw new IOError.NOT_FOUND(@"\"$path\" does not exist");
- }
- var process = new Subprocess.newv(
- new string[] { "tar", "-tf", path },
- SubprocessFlags.STDOUT_PIPE | SubprocessFlags.STDERR_PIPE);
- // Drain both pipes so a large archive listing cannot deadlock on a full buffer
- var buffer = new uint8[64 * 1024];
- while(process.get_stdout_pipe().read(buffer) > 0) {
- }
- while(process.get_stderr_pipe().read(buffer) > 0) {
- }
- try {
- process.wait_check();
- }
- catch(Error e) {
- throw new IOError.INVALID_ARGUMENT(@"\"$(Path.get_basename(path))\" is not a readable tar.xz archive\n");
- }
- }
- private int dcba_repository_add(string artifact_path) {
- var root = dcba_find_root();
- if(root == null) {
- printerr(@"No DCBA repository root found (a directory containing a *.dcbar file, \"keys/\" and \"public/\" at or above the current directory)\n");
- return 1;
- }
- uint8[] public_key;
- uint8[] private_key;
- if(!dcba_load_keys(root, out public_key, out private_key)) {
- return 1;
- }
- var filename = Path.get_basename(artifact_path);
- if(!filename.has_suffix(".dcba")) {
- printerr(@"\"$filename\" is not a .dcba artifact\n");
- return 1;
- }
- var identity = Usm.DcbaArtifact.parse_filename(filename);
- if(identity == null) {
- printerr(@"\"$filename\" does not carry a parseable version, rename it to <name>-<version>.dcba (e.g. app-1.0.0.dcba)\n");
- return 1;
- }
- Invercargill.BinaryData checksum;
- try {
- dcba_validate_tar_xz(artifact_path);
- checksum = calculate_file_checksum(artifact_path);
- }
- catch(Error e) {
- printerr(@"Invalid artifact \"$artifact_path\": $(e.message)\n");
- return 1;
- }
- var public_dir = Path.build_filename(root, "public");
- try {
- foreach(var file in Iterate.directory(public_dir)) {
- if(!file.has_suffix(".dcba")) {
- continue;
- }
- var existing_path = Path.build_filename(public_dir, file);
- var existing_identity = Usm.DcbaArtifact.parse_filename(file);
- var same_artifact = existing_identity != null && ((!)existing_identity).same_artifact((!)identity);
- if(!same_artifact && file != filename) {
- continue;
- }
- if(calculate_file_checksum(existing_path).equals(checksum)) {
- printerr(@"\"$(((!)identity).name)-$(((!)identity).version)\" is already present as \"$file\", nothing to add\n");
- return dcba_rebuild_listing(root, public_key, private_key);
- }
- printerr(@"\"$file\" already exists with different content, remove it first with \"usm dcba repository remove\"\n");
- return 1;
- }
- File.new_for_path(artifact_path).copy(File.new_for_path(Path.build_filename(public_dir, filename)), FileCopyFlags.OVERWRITE);
- }
- catch(Error e) {
- printerr(@"Error: $(e.message)\n");
- return e.code;
- }
- printerr(@"Added \"$(((!)identity).name)-$(((!)identity).version)\" as \"$filename\"\n");
- return dcba_rebuild_listing(root, public_key, private_key);
- }
- private int dcba_repository_remove(string selector) {
- var root = dcba_find_root();
- if(root == null) {
- printerr("No DCBA repository root found (a directory containing a *.dcbar file, \"keys/\" and \"public/\" at or above the current directory)\n");
- return 1;
- }
- uint8[] public_key;
- uint8[] private_key;
- if(!dcba_load_keys(root, out public_key, out private_key)) {
- return 1;
- }
- var public_dir = Path.build_filename(root, "public");
- var matches = new Vector<string>();
- try {
- var filename = selector.has_suffix(".dcba") ? selector : @"$selector.dcba";
- if(FileUtils.test(Path.build_filename(public_dir, filename), FileTest.EXISTS)) {
- matches.add(filename);
- }
- else {
- foreach(var file in Iterate.directory(public_dir)) {
- if(file.has_suffix(".dcba") && file.has_prefix(@"$selector-")) {
- matches.add(file);
- }
- }
- }
- if(matches.length == 0) {
- printerr(@"No artifact matching \"$selector\" found in \"public/\"\n");
- return 1;
- }
- if(matches.length > 1) {
- printerr(@"\"$selector\" matches multiple artifacts, specify an exact filename:\n");
- foreach(var file in matches) {
- printerr(@"\t$file\n");
- }
- return 1;
- }
- var match = matches.first_or_default();
- File.new_for_path(Path.build_filename(public_dir, match)).delete();
- }
- catch(Error e) {
- printerr(@"Error: $(e.message)\n");
- return e.code;
- }
- printerr(@"Removed \"$(matches.first_or_default())\"\n");
- return dcba_rebuild_listing(root, public_key, private_key);
- }
- private int dcba_repository_list(string? repository_file) {
- try {
- Usm.Repository repository;
- Usm.DcbaListing listing;
- if(repository_file != null) {
- repository = new Usm.Repository.from_file(repository_file);
- var stream = File.new_for_uri(repository.url + "/" + Usm.RepositoryClient.DCBA_LISTING_NAME).read();
- listing = new Usm.DcbaListing.from_stream(new DataInputStream(stream));
- }
- else {
- var root = dcba_find_root();
- if(root == null) {
- printerr("No DCBA repository root found (a directory containing a *.dcbar file, \"keys/\" and \"public/\" at or above the current directory)\n");
- return 1;
- }
- var repo_file = dcba_find_repo_file(root);
- if(repo_file == null) {
- printerr("No *.dcbar file found in the DCBA repository root\n");
- return 1;
- }
- repository = new Usm.Repository.from_file(repo_file);
- var listing_path = Path.build_filename(root, "public", Usm.RepositoryClient.DCBA_LISTING_NAME);
- if(!FileUtils.test(listing_path, FileTest.EXISTS)) {
- printerr("No ARTIFACTS.usml in \"public/\", add an artifact with `usm dcba repository add` or run `usm dcba repository build-list` first\n");
- return 1;
- }
- listing = new Usm.DcbaListing.from_stream(new DataInputStream(File.new_for_path(listing_path).read()));
- }
- if(!listing.valid_signature(repository.key)) {
- printerr(@"Artifact listing signature verification failed for \"$(repository.name)\"\n");
- return 1;
- }
- foreach(var entry in listing.entries) {
- print(@"$(entry.name) $(entry.version) $(dcba_format_size(entry.size))\n");
- }
- }
- catch(Error e) {
- printerr(@"Error: $(e.message)\n");
- return e.code;
- }
- return 0;
- }
- /** Human-readable byte count (`12.0 MiB`, `512 B`); size 0 (listings predating the field) prints `?`. */
- private string dcba_format_size(uint64 size) {
- if(size == 0) {
- return "?";
- }
- double value = size;
- foreach(var unit in new string[] { "B", "KiB", "MiB", "GiB", "TiB" }) {
- if(value < 1024.0 || unit == "TiB") {
- return unit == "B" ? @"$size B" : "%.1f %s".printf(value, unit);
- }
- value /= 1024.0;
- }
- assert_not_reached();
- }
- private int dcba_repository_verify(string repository_file) {
- try {
- var json = new InvercargillJson.JsonElement.from_file(repository_file);
- var repository = Usm.Repository.get_mapper().materialise(json.as<InvercargillJson.JsonObject>());
- var stream = File.new_for_uri(repository.url + "/" + Usm.RepositoryClient.DCBA_LISTING_NAME).read();
- var listing = new Usm.DcbaListing.from_stream(new DataInputStream(stream));
- if(!listing.valid_signature(repository.key)) {
- printerr("Invalid signature.\n");
- return 1;
- }
- printerr(@"Valid signature for \"$(repository.name)\" artifact listing.\n");
- var verified = 0;
- var failures = 0;
- foreach(var entry in listing.entries) {
- var artifact_uri = repository.url.has_suffix("/") ? repository.url + entry.path : repository.url + "/" + entry.path;
- try {
- var artifact_stream = File.new_for_uri(artifact_uri).read();
- if(repository_stream_checksum(artifact_stream).equals(entry.sha512sum)) {
- printerr(@"Verified \"$(entry.path)\"\n");
- verified++;
- }
- else {
- printerr(@"Checksum mismatch for \"$(entry.path)\"\n");
- failures++;
- }
- }
- catch(Error e) {
- printerr(@"Could not verify \"$(entry.path)\": $(e.message)\n");
- failures++;
- }
- }
- if(failures > 0) {
- printerr(@"$failures of $(verified + failures) artifact(s) failed verification\n");
- return 1;
- }
- printerr(@"Deep-verified $verified artifact(s) in \"$(repository.name)\"\n");
- return 0;
- }
- catch(Error e) {
- printerr(@"Error: $(e.message)\n");
- return e.code;
- }
- }
- private uint64 dcba_modification_time(string path) throws Error {
- return File.new_for_path(path).query_info(FileAttribute.TIME_MODIFIED, FileQueryInfoFlags.NONE).get_attribute_uint64(FileAttribute.TIME_MODIFIED);
- }
- private int dcba_repository_publish(string destination_path) {
- var root = dcba_find_root();
- if(root == null) {
- printerr("No DCBA repository root found (a directory containing a *.dcbar file, \"keys/\" and \"public/\" at or above the current directory)\n");
- return 1;
- }
- var repo_file = dcba_find_repo_file(root);
- if(repo_file == null) {
- printerr("No *.dcbar file found in the DCBA repository root\n");
- return 1;
- }
- var public_dir = Path.build_filename(root, "public");
- var listing_path = Path.build_filename(public_dir, Usm.RepositoryClient.DCBA_LISTING_NAME);
- if(!FileUtils.test(listing_path, FileTest.EXISTS)) {
- printerr("No ARTIFACTS.usml in \"public/\", add an artifact with `usm dcba repository add` or run `usm dcba repository build-list` first\n");
- return 1;
- }
- try {
- var destination = File.new_for_path(destination_path);
- if(!destination.query_exists()) {
- destination.make_directory_with_parents();
- }
- File.new_for_path(repo_file).copy(destination.get_child(Path.get_basename(repo_file)), FileCopyFlags.OVERWRITE);
- printerr(@"Published \"$(Path.get_basename(repo_file))\"\n");
- File.new_for_path(listing_path).copy(destination.get_child(Usm.RepositoryClient.DCBA_LISTING_NAME), FileCopyFlags.OVERWRITE);
- printerr(@"Published \"ARTIFACTS.usml\"\n");
- foreach(var file in Iterate.directory(public_dir)) {
- if(!file.has_suffix(".dcba")) {
- continue;
- }
- var source_path = Path.build_filename(public_dir, file);
- var destination_file = destination.get_child(file);
- if(destination_file.query_exists()
- && dcba_modification_time(destination_file.get_path()) >= dcba_modification_time(source_path)) {
- printerr(@"Skipped \"$file\" (up to date)\n");
- continue;
- }
- File.new_for_path(source_path).copy(destination_file, FileCopyFlags.OVERWRITE);
- printerr(@"Published \"$file\"\n");
- }
- }
- catch(Error e) {
- printerr(@"Error: $(e.message)\n");
- return e.code;
- }
- return 0;
- }
- private int dcba_repository_build_list() {
- var root = dcba_find_root();
- if(root == null) {
- root = Environment.get_current_dir();
- }
- uint8[] public_key;
- uint8[] private_key;
- if(!dcba_load_keys(root, out public_key, out private_key)) {
- return 1;
- }
- return dcba_rebuild_listing(root, public_key, private_key);
- }
- /**
- * Rebuilds and re-signs `public/ARTIFACTS.usml` for the DCBA repository
- * rooted at `root`, the shared implementation behind `add`, `remove`
- * and `build-list` — a thin CLI shell over
- * {@link Usm.DcbaListingWriter.write}.
- */
- private int dcba_rebuild_listing(string root, uint8[] public_key, uint8[] private_key) {
- try {
- var public_dir = Path.build_filename(root, "public");
- var entries = new Vector<Usm.DcbaListingEntry>();
- foreach(var file in Iterate.directory(public_dir)) {
- var path = Path.build_filename(public_dir, file);
- if(!path.has_suffix(".dcba")) {
- continue;
- }
- var identity = Usm.DcbaArtifact.parse_filename(file);
- if(identity == null) {
- throw new IOError.INVALID_ARGUMENT(@"\"$file\" does not carry a parseable version, rename it to <name>-<version>.dcba\n");
- }
- entries.add(new Usm.DcbaListingEntry() {
- name = ((!)identity).name,
- version = ((!)identity).version,
- path = file,
- sha512sum = calculate_file_checksum(path),
- size = (uint64)File.new_for_path(path).query_info(FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE).get_size()
- });
- }
- Usm.DcbaListingWriter.write(Path.build_filename(public_dir, Usm.RepositoryClient.DCBA_LISTING_NAME), entries, public_key, private_key);
- }
- catch(Error e) {
- printerr(@"Error: $(e.message)\n");
- return e.code;
- }
- return 0;
- }
- namespace Usm.Cli {
- /**
- * `usm dcba add-repo <url-or-path>` — adds a **DCBA** build-artifact
- * repository (`.dcbar` descriptor) to the configuration; the
- * {@link AddRepoRunner} shell around the `.dcbar`
- * {@link AddRepoDefinition}. Mirrors `usm add-repo` verb for verb,
- * refreshing `ARTIFACTS.usml` instead of `PACKAGES.usml`.
- */
- public class DcbaAddRepo {
- public static int run(string[] args) {
- var definition = new AddRepoDefinition() {
- extension = ".dcbar",
- refresh_noun = "artifacts",
- usage_line = "usm dcba add-repo [-y|--yes] <url-or-path>",
- followup_line = "Pre-built artifacts are now used automatically by usm install and usm downgrade\n",
- refresh = destination => {
- var state = new Usm.SystemState(paths);
- var added = new Usm.Repository.from_file(destination);
- state.refresh_dcba_list(added);
- var listing = state.get_latest_dcba_list(added);
- return listing == null ? 0 : (int)listing.entries.count();
- }
- };
- return AddRepoRunner.run(args, definition, 3);
- }
- }
- }
|