Dcba.vala 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. private int dcba_main(string[] args) {
  4. if(args.length < 3) {
  5. return dcba_usage();
  6. }
  7. if(args[2] == "repository") {
  8. return dcba_repository_main(args);
  9. }
  10. if(args[2] == "add-repo") {
  11. return Usm.Cli.DcbaAddRepo.run(args);
  12. }
  13. return dcba_usage();
  14. }
  15. private int dcba_usage() {
  16. printerr("USAGE:\n\tusm dcba repository <init|add|remove|list|verify|publish|build-list>\n\tusm dcba add-repo <url-or-path>\n");
  17. return 255;
  18. }
  19. private int dcba_repository_main(string[] args) {
  20. if(args.length < 4) {
  21. return dcba_repository_usage();
  22. }
  23. if(args[2] != "repository") {
  24. return dcba_repository_usage();
  25. }
  26. if(args[3] == "init") {
  27. if(args.length != 5) {
  28. return dcba_repository_usage();
  29. }
  30. return dcba_repository_init(args[4]);
  31. }
  32. if(args[3] == "add") {
  33. if(args.length != 5) {
  34. return dcba_repository_usage();
  35. }
  36. return dcba_repository_add(args[4]);
  37. }
  38. if(args[3] == "remove") {
  39. if(args.length != 5) {
  40. return dcba_repository_usage();
  41. }
  42. return dcba_repository_remove(args[4]);
  43. }
  44. if(args[3] == "list") {
  45. if(args.length > 5) {
  46. return dcba_repository_usage();
  47. }
  48. return dcba_repository_list(args.length == 5 ? args[4] : null);
  49. }
  50. if(args[3] == "verify") {
  51. if(args.length != 5) {
  52. return dcba_repository_usage();
  53. }
  54. return dcba_repository_verify(args[4]);
  55. }
  56. if(args[3] == "publish") {
  57. if(args.length != 5) {
  58. return dcba_repository_usage();
  59. }
  60. return dcba_repository_publish(args[4]);
  61. }
  62. if(args[3] == "build-list") {
  63. if(args.length != 4) {
  64. return dcba_repository_usage();
  65. }
  66. return dcba_repository_build_list();
  67. }
  68. return dcba_repository_usage();
  69. }
  70. private int dcba_repository_usage() {
  71. 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");
  72. return 255;
  73. }
  74. private int dcba_repository_init(string name) {
  75. if(dcba_find_repo_file(Environment.get_current_dir()) != null
  76. || FileUtils.test(Path.build_filename("keys", "private-key"), FileTest.EXISTS)) {
  77. printerr("A DCBA repository has already been initialised in this directory\n");
  78. return 1;
  79. }
  80. uint8[] public_key;
  81. uint8[] private_key;
  82. dcba_generate_keypair(out public_key, out private_key);
  83. DirUtils.create("keys", 0700);
  84. DirUtils.create("public", 0755);
  85. try {
  86. FileUtils.set_data(Path.build_filename("keys", "public-key"), public_key);
  87. FileUtils.set_data(Path.build_filename("keys", "private-key"), private_key);
  88. FileUtils.chmod(Path.build_filename("keys", "private-key"), 0600);
  89. var repository = new Usm.Repository();
  90. repository.key = Wrap.byte_array(public_key);
  91. repository.name = name;
  92. repository.url = @"file://$(Environment.get_current_dir())/public";
  93. repository.summary = @"My $name DCBA repository";
  94. var properties = Usm.Repository.get_mapper().map_from(repository);
  95. var json = new InvercargillJson.JsonElement.from_properties(properties);
  96. json.write_to_file(@"$name.dcbar");
  97. var gitignore = "keys/private*\n";
  98. if(FileUtils.test(".gitignore", FileTest.EXISTS)) {
  99. string existing;
  100. FileUtils.get_contents(".gitignore", out existing);
  101. if(!existing.has_suffix("\n")) {
  102. existing += "\n";
  103. }
  104. if(existing.index_of("keys/private*") == -1) {
  105. FileUtils.set_contents(".gitignore", existing + gitignore);
  106. }
  107. }
  108. else {
  109. FileUtils.set_contents(".gitignore", gitignore);
  110. }
  111. }
  112. catch(Error e) {
  113. printerr(@"Error: $(e.message)\n");
  114. return e.code;
  115. }
  116. printerr("DCBA repository initialised, add artifacts with `usm dcba repository add <name-version.dcba>` then publish them with `usm dcba repository publish <path>`\n");
  117. return 0;
  118. }
  119. /**
  120. * Locates the nearest DCBA repository root: the first directory at or
  121. * above the current directory holding a `*.dcbar` file, a `keys/`
  122. * directory and a `public/` directory — the `.dcbar`-suffix counterpart
  123. * of `repository_find_root`, deliberately separate so a USM source
  124. * repository and a DCBA repository can never be mistaken for one
  125. * another even when nested in the same tree.
  126. */
  127. private string? dcba_find_root() {
  128. var directory = Environment.get_current_dir();
  129. while(true) {
  130. if(dcba_find_repo_file(directory) != null
  131. && FileUtils.test(Path.build_filename(directory, "keys"), FileTest.IS_DIR)
  132. && FileUtils.test(Path.build_filename(directory, "public"), FileTest.IS_DIR)) {
  133. return directory;
  134. }
  135. var parent = Path.get_dirname(directory);
  136. if(parent == directory) {
  137. return null;
  138. }
  139. directory = parent;
  140. }
  141. }
  142. /**
  143. * Finds the first `*.dcbar` file directly inside `root`, or null when
  144. * there is none.
  145. */
  146. private string? dcba_find_repo_file(string root) {
  147. try {
  148. foreach(var file in Iterate.directory(root)) {
  149. if(file.has_suffix(".dcbar")) {
  150. return Path.build_filename(root, file);
  151. }
  152. }
  153. }
  154. catch(Error e) {
  155. return null;
  156. }
  157. return null;
  158. }
  159. /**
  160. * Loads the signing keys from `keys/public-key` and `keys/private-key` in
  161. * the DCBA repository root, falling back to the legacy loose
  162. * `public-key` and `private-key` files there.
  163. */
  164. private bool dcba_load_keys(string root, out uint8[] public_key, out uint8[] private_key) {
  165. public_key = new uint8[0];
  166. private_key = new uint8[0];
  167. var key_dir = Path.build_filename(root, "keys");
  168. if(dcba_read_key(Path.build_filename(key_dir, "public-key"), out public_key)
  169. && dcba_read_key(Path.build_filename(key_dir, "private-key"), out private_key)) {
  170. return true;
  171. }
  172. if(dcba_read_key(Path.build_filename(root, "public-key"), out public_key)
  173. && dcba_read_key(Path.build_filename(root, "private-key"), out private_key)) {
  174. return true;
  175. }
  176. printerr("No signing keys found in \"keys/\" (or as legacy \"public-key\"/\"private-key\") within the DCBA repository root\n");
  177. return false;
  178. }
  179. private bool dcba_read_key(string path, out uint8[] key) {
  180. key = new uint8[0];
  181. if(!FileUtils.test(path, FileTest.EXISTS)) {
  182. return false;
  183. }
  184. try {
  185. FileUtils.get_data(path, out key);
  186. return true;
  187. }
  188. catch(Error e) {
  189. printerr(@"Error: $(e.message)\n");
  190. return false;
  191. }
  192. }
  193. private void dcba_generate_keypair(out uint8[] public_key, out uint8[] private_key) {
  194. public_key = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
  195. private_key = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
  196. Sodium.Asymmetric.Signing.generate_keypair(public_key, private_key);
  197. }
  198. /**
  199. * Validates that the file at `path` is a readable tar.xz archive by
  200. * listing it through `tar -tf` — the same archive family USM's build
  201. * artifacts (`build.tar.xz`) and `.dcba` files use.
  202. */
  203. private void dcba_validate_tar_xz(string path) throws Error {
  204. if(!FileUtils.test(path, FileTest.EXISTS)) {
  205. throw new IOError.NOT_FOUND(@"\"$path\" does not exist");
  206. }
  207. var process = new Subprocess.newv(
  208. new string[] { "tar", "-tf", path },
  209. SubprocessFlags.STDOUT_PIPE | SubprocessFlags.STDERR_PIPE);
  210. // Drain both pipes so a large archive listing cannot deadlock on a full buffer
  211. var buffer = new uint8[64 * 1024];
  212. while(process.get_stdout_pipe().read(buffer) > 0) {
  213. }
  214. while(process.get_stderr_pipe().read(buffer) > 0) {
  215. }
  216. try {
  217. process.wait_check();
  218. }
  219. catch(Error e) {
  220. throw new IOError.INVALID_ARGUMENT(@"\"$(Path.get_basename(path))\" is not a readable tar.xz archive\n");
  221. }
  222. }
  223. private int dcba_repository_add(string artifact_path) {
  224. var root = dcba_find_root();
  225. if(root == null) {
  226. printerr(@"No DCBA repository root found (a directory containing a *.dcbar file, \"keys/\" and \"public/\" at or above the current directory)\n");
  227. return 1;
  228. }
  229. uint8[] public_key;
  230. uint8[] private_key;
  231. if(!dcba_load_keys(root, out public_key, out private_key)) {
  232. return 1;
  233. }
  234. var filename = Path.get_basename(artifact_path);
  235. if(!filename.has_suffix(".dcba")) {
  236. printerr(@"\"$filename\" is not a .dcba artifact\n");
  237. return 1;
  238. }
  239. var identity = Usm.DcbaArtifact.parse_filename(filename);
  240. if(identity == null) {
  241. printerr(@"\"$filename\" does not carry a parseable version, rename it to <name>-<version>.dcba (e.g. app-1.0.0.dcba)\n");
  242. return 1;
  243. }
  244. Invercargill.BinaryData checksum;
  245. try {
  246. dcba_validate_tar_xz(artifact_path);
  247. checksum = calculate_file_checksum(artifact_path);
  248. }
  249. catch(Error e) {
  250. printerr(@"Invalid artifact \"$artifact_path\": $(e.message)\n");
  251. return 1;
  252. }
  253. var public_dir = Path.build_filename(root, "public");
  254. try {
  255. foreach(var file in Iterate.directory(public_dir)) {
  256. if(!file.has_suffix(".dcba")) {
  257. continue;
  258. }
  259. var existing_path = Path.build_filename(public_dir, file);
  260. var existing_identity = Usm.DcbaArtifact.parse_filename(file);
  261. var same_artifact = existing_identity != null && ((!)existing_identity).same_artifact((!)identity);
  262. if(!same_artifact && file != filename) {
  263. continue;
  264. }
  265. if(calculate_file_checksum(existing_path).equals(checksum)) {
  266. printerr(@"\"$(((!)identity).name)-$(((!)identity).version)\" is already present as \"$file\", nothing to add\n");
  267. return dcba_rebuild_listing(root, public_key, private_key);
  268. }
  269. printerr(@"\"$file\" already exists with different content, remove it first with \"usm dcba repository remove\"\n");
  270. return 1;
  271. }
  272. File.new_for_path(artifact_path).copy(File.new_for_path(Path.build_filename(public_dir, filename)), FileCopyFlags.OVERWRITE);
  273. }
  274. catch(Error e) {
  275. printerr(@"Error: $(e.message)\n");
  276. return e.code;
  277. }
  278. printerr(@"Added \"$(((!)identity).name)-$(((!)identity).version)\" as \"$filename\"\n");
  279. return dcba_rebuild_listing(root, public_key, private_key);
  280. }
  281. private int dcba_repository_remove(string selector) {
  282. var root = dcba_find_root();
  283. if(root == null) {
  284. printerr("No DCBA repository root found (a directory containing a *.dcbar file, \"keys/\" and \"public/\" at or above the current directory)\n");
  285. return 1;
  286. }
  287. uint8[] public_key;
  288. uint8[] private_key;
  289. if(!dcba_load_keys(root, out public_key, out private_key)) {
  290. return 1;
  291. }
  292. var public_dir = Path.build_filename(root, "public");
  293. var matches = new Vector<string>();
  294. try {
  295. var filename = selector.has_suffix(".dcba") ? selector : @"$selector.dcba";
  296. if(FileUtils.test(Path.build_filename(public_dir, filename), FileTest.EXISTS)) {
  297. matches.add(filename);
  298. }
  299. else {
  300. foreach(var file in Iterate.directory(public_dir)) {
  301. if(file.has_suffix(".dcba") && file.has_prefix(@"$selector-")) {
  302. matches.add(file);
  303. }
  304. }
  305. }
  306. if(matches.length == 0) {
  307. printerr(@"No artifact matching \"$selector\" found in \"public/\"\n");
  308. return 1;
  309. }
  310. if(matches.length > 1) {
  311. printerr(@"\"$selector\" matches multiple artifacts, specify an exact filename:\n");
  312. foreach(var file in matches) {
  313. printerr(@"\t$file\n");
  314. }
  315. return 1;
  316. }
  317. var match = matches.first_or_default();
  318. File.new_for_path(Path.build_filename(public_dir, match)).delete();
  319. }
  320. catch(Error e) {
  321. printerr(@"Error: $(e.message)\n");
  322. return e.code;
  323. }
  324. printerr(@"Removed \"$(matches.first_or_default())\"\n");
  325. return dcba_rebuild_listing(root, public_key, private_key);
  326. }
  327. private int dcba_repository_list(string? repository_file) {
  328. try {
  329. Usm.Repository repository;
  330. Usm.DcbaListing listing;
  331. if(repository_file != null) {
  332. repository = new Usm.Repository.from_file(repository_file);
  333. var stream = File.new_for_uri(repository.url + "/" + Usm.RepositoryClient.DCBA_LISTING_NAME).read();
  334. listing = new Usm.DcbaListing.from_stream(new DataInputStream(stream));
  335. }
  336. else {
  337. var root = dcba_find_root();
  338. if(root == null) {
  339. printerr("No DCBA repository root found (a directory containing a *.dcbar file, \"keys/\" and \"public/\" at or above the current directory)\n");
  340. return 1;
  341. }
  342. var repo_file = dcba_find_repo_file(root);
  343. if(repo_file == null) {
  344. printerr("No *.dcbar file found in the DCBA repository root\n");
  345. return 1;
  346. }
  347. repository = new Usm.Repository.from_file(repo_file);
  348. var listing_path = Path.build_filename(root, "public", Usm.RepositoryClient.DCBA_LISTING_NAME);
  349. if(!FileUtils.test(listing_path, FileTest.EXISTS)) {
  350. printerr("No ARTIFACTS.usml in \"public/\", add an artifact with `usm dcba repository add` or run `usm dcba repository build-list` first\n");
  351. return 1;
  352. }
  353. listing = new Usm.DcbaListing.from_stream(new DataInputStream(File.new_for_path(listing_path).read()));
  354. }
  355. if(!listing.valid_signature(repository.key)) {
  356. printerr(@"Artifact listing signature verification failed for \"$(repository.name)\"\n");
  357. return 1;
  358. }
  359. foreach(var entry in listing.entries) {
  360. print(@"$(entry.name) $(entry.version) $(dcba_format_size(entry.size))\n");
  361. }
  362. }
  363. catch(Error e) {
  364. printerr(@"Error: $(e.message)\n");
  365. return e.code;
  366. }
  367. return 0;
  368. }
  369. /** Human-readable byte count (`12.0 MiB`, `512 B`); size 0 (listings predating the field) prints `?`. */
  370. private string dcba_format_size(uint64 size) {
  371. if(size == 0) {
  372. return "?";
  373. }
  374. double value = size;
  375. foreach(var unit in new string[] { "B", "KiB", "MiB", "GiB", "TiB" }) {
  376. if(value < 1024.0 || unit == "TiB") {
  377. return unit == "B" ? @"$size B" : "%.1f %s".printf(value, unit);
  378. }
  379. value /= 1024.0;
  380. }
  381. assert_not_reached();
  382. }
  383. private int dcba_repository_verify(string repository_file) {
  384. try {
  385. var json = new InvercargillJson.JsonElement.from_file(repository_file);
  386. var repository = Usm.Repository.get_mapper().materialise(json.as<InvercargillJson.JsonObject>());
  387. var stream = File.new_for_uri(repository.url + "/" + Usm.RepositoryClient.DCBA_LISTING_NAME).read();
  388. var listing = new Usm.DcbaListing.from_stream(new DataInputStream(stream));
  389. if(!listing.valid_signature(repository.key)) {
  390. printerr("Invalid signature.\n");
  391. return 1;
  392. }
  393. printerr(@"Valid signature for \"$(repository.name)\" artifact listing.\n");
  394. var verified = 0;
  395. var failures = 0;
  396. foreach(var entry in listing.entries) {
  397. var artifact_uri = repository.url.has_suffix("/") ? repository.url + entry.path : repository.url + "/" + entry.path;
  398. try {
  399. var artifact_stream = File.new_for_uri(artifact_uri).read();
  400. if(repository_stream_checksum(artifact_stream).equals(entry.sha512sum)) {
  401. printerr(@"Verified \"$(entry.path)\"\n");
  402. verified++;
  403. }
  404. else {
  405. printerr(@"Checksum mismatch for \"$(entry.path)\"\n");
  406. failures++;
  407. }
  408. }
  409. catch(Error e) {
  410. printerr(@"Could not verify \"$(entry.path)\": $(e.message)\n");
  411. failures++;
  412. }
  413. }
  414. if(failures > 0) {
  415. printerr(@"$failures of $(verified + failures) artifact(s) failed verification\n");
  416. return 1;
  417. }
  418. printerr(@"Deep-verified $verified artifact(s) in \"$(repository.name)\"\n");
  419. return 0;
  420. }
  421. catch(Error e) {
  422. printerr(@"Error: $(e.message)\n");
  423. return e.code;
  424. }
  425. }
  426. private uint64 dcba_modification_time(string path) throws Error {
  427. return File.new_for_path(path).query_info(FileAttribute.TIME_MODIFIED, FileQueryInfoFlags.NONE).get_attribute_uint64(FileAttribute.TIME_MODIFIED);
  428. }
  429. private int dcba_repository_publish(string destination_path) {
  430. var root = dcba_find_root();
  431. if(root == null) {
  432. printerr("No DCBA repository root found (a directory containing a *.dcbar file, \"keys/\" and \"public/\" at or above the current directory)\n");
  433. return 1;
  434. }
  435. var repo_file = dcba_find_repo_file(root);
  436. if(repo_file == null) {
  437. printerr("No *.dcbar file found in the DCBA repository root\n");
  438. return 1;
  439. }
  440. var public_dir = Path.build_filename(root, "public");
  441. var listing_path = Path.build_filename(public_dir, Usm.RepositoryClient.DCBA_LISTING_NAME);
  442. if(!FileUtils.test(listing_path, FileTest.EXISTS)) {
  443. printerr("No ARTIFACTS.usml in \"public/\", add an artifact with `usm dcba repository add` or run `usm dcba repository build-list` first\n");
  444. return 1;
  445. }
  446. try {
  447. var destination = File.new_for_path(destination_path);
  448. if(!destination.query_exists()) {
  449. destination.make_directory_with_parents();
  450. }
  451. File.new_for_path(repo_file).copy(destination.get_child(Path.get_basename(repo_file)), FileCopyFlags.OVERWRITE);
  452. printerr(@"Published \"$(Path.get_basename(repo_file))\"\n");
  453. File.new_for_path(listing_path).copy(destination.get_child(Usm.RepositoryClient.DCBA_LISTING_NAME), FileCopyFlags.OVERWRITE);
  454. printerr(@"Published \"ARTIFACTS.usml\"\n");
  455. foreach(var file in Iterate.directory(public_dir)) {
  456. if(!file.has_suffix(".dcba")) {
  457. continue;
  458. }
  459. var source_path = Path.build_filename(public_dir, file);
  460. var destination_file = destination.get_child(file);
  461. if(destination_file.query_exists()
  462. && dcba_modification_time(destination_file.get_path()) >= dcba_modification_time(source_path)) {
  463. printerr(@"Skipped \"$file\" (up to date)\n");
  464. continue;
  465. }
  466. File.new_for_path(source_path).copy(destination_file, FileCopyFlags.OVERWRITE);
  467. printerr(@"Published \"$file\"\n");
  468. }
  469. }
  470. catch(Error e) {
  471. printerr(@"Error: $(e.message)\n");
  472. return e.code;
  473. }
  474. return 0;
  475. }
  476. private int dcba_repository_build_list() {
  477. var root = dcba_find_root();
  478. if(root == null) {
  479. root = Environment.get_current_dir();
  480. }
  481. uint8[] public_key;
  482. uint8[] private_key;
  483. if(!dcba_load_keys(root, out public_key, out private_key)) {
  484. return 1;
  485. }
  486. return dcba_rebuild_listing(root, public_key, private_key);
  487. }
  488. /**
  489. * Rebuilds and re-signs `public/ARTIFACTS.usml` for the DCBA repository
  490. * rooted at `root`, the shared implementation behind `add`, `remove`
  491. * and `build-list` — a thin CLI shell over
  492. * {@link Usm.DcbaListingWriter.write}.
  493. */
  494. private int dcba_rebuild_listing(string root, uint8[] public_key, uint8[] private_key) {
  495. try {
  496. var public_dir = Path.build_filename(root, "public");
  497. var entries = new Vector<Usm.DcbaListingEntry>();
  498. foreach(var file in Iterate.directory(public_dir)) {
  499. var path = Path.build_filename(public_dir, file);
  500. if(!path.has_suffix(".dcba")) {
  501. continue;
  502. }
  503. var identity = Usm.DcbaArtifact.parse_filename(file);
  504. if(identity == null) {
  505. throw new IOError.INVALID_ARGUMENT(@"\"$file\" does not carry a parseable version, rename it to <name>-<version>.dcba\n");
  506. }
  507. entries.add(new Usm.DcbaListingEntry() {
  508. name = ((!)identity).name,
  509. version = ((!)identity).version,
  510. path = file,
  511. sha512sum = calculate_file_checksum(path),
  512. size = (uint64)File.new_for_path(path).query_info(FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE).get_size()
  513. });
  514. }
  515. Usm.DcbaListingWriter.write(Path.build_filename(public_dir, Usm.RepositoryClient.DCBA_LISTING_NAME), entries, public_key, private_key);
  516. }
  517. catch(Error e) {
  518. printerr(@"Error: $(e.message)\n");
  519. return e.code;
  520. }
  521. return 0;
  522. }
  523. namespace Usm.Cli {
  524. /**
  525. * `usm dcba add-repo <url-or-path>` — adds a **DCBA** build-artifact
  526. * repository (`.dcbar` descriptor) to the configuration; the
  527. * {@link AddRepoRunner} shell around the `.dcbar`
  528. * {@link AddRepoDefinition}. Mirrors `usm add-repo` verb for verb,
  529. * refreshing `ARTIFACTS.usml` instead of `PACKAGES.usml`.
  530. */
  531. public class DcbaAddRepo {
  532. public static int run(string[] args) {
  533. var definition = new AddRepoDefinition() {
  534. extension = ".dcbar",
  535. refresh_noun = "artifacts",
  536. usage_line = "usm dcba add-repo [-y|--yes] <url-or-path>",
  537. followup_line = "Pre-built artifacts are now used automatically by usm install and usm downgrade\n",
  538. refresh = destination => {
  539. var state = new Usm.SystemState(paths);
  540. var added = new Usm.Repository.from_file(destination);
  541. state.refresh_dcba_list(added);
  542. var listing = state.get_latest_dcba_list(added);
  543. return listing == null ? 0 : (int)listing.entries.count();
  544. }
  545. };
  546. return AddRepoRunner.run(args, definition, 3);
  547. }
  548. }
  549. }