Transaction.vala 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. namespace Usm {
  4. public class Transaction {
  5. public Paths paths { get; set; }
  6. public ResourceFinder resource_finder { get; set; }
  7. public SystemState state { get; set; }
  8. public Set<CachedPackage> to_install { get; set; default = new HashSet<CachedPackage>(); }
  9. public Set<CachedPackage> to_remove { get; set; default = new HashSet<CachedPackage>(); }
  10. /**
  11. * Optional install order (package names) taken from a
  12. * {@link ResolutionResult}'s {@link ResolutionResult.install_order};
  13. * when set, packages build and install in exactly this order.
  14. */
  15. public Vector<string>? install_order { get; set; }
  16. /**
  17. * Optional removal order (package names) — the reverse of a
  18. * {@link ResolutionResult}'s install order; when set, removals follow
  19. * it exactly (dependents before their providers).
  20. */
  21. public Vector<string>? remove_order { get; set; }
  22. /**
  23. * Manifest names of the packages the user asked for by name (the
  24. * `usm install` arguments): each install records
  25. * {@link OriginInformation.explicitly_installed} true for these and
  26. * false for everything the resolver pulled in as a dependency.
  27. * Null — every command but `usm install` — marks the whole install
  28. * set implicit.
  29. */
  30. public Vector<string>? explicit_packages { get; set; }
  31. /**
  32. * Version downgrades executed by this transaction: each entry
  33. * removes {@link DowngradeEntry.current}'s installed resources —
  34. * keeping its state directory for a later downgrade back — and
  35. * installs {@link DowngradeEntry.target} in its place.
  36. */
  37. public Vector<DowngradeEntry> to_downgrade { get; set; default = new Vector<DowngradeEntry>(); }
  38. public signal void progress_updated(TransactionTask task_type, string subject, uint current_task, uint total_tasks, float task_progress);
  39. private uint task_count = 0;
  40. private uint current_task = 0;
  41. private string current_subject = "transaction";
  42. private TransactionTask current_task_type = TransactionTask.STRATEGISING;
  43. /**
  44. * Active rollback journal, non-null only for the duration of
  45. * {@link run} and {@link rebuild_package}: every install and
  46. * remove records its changes before executing them so a failure
  47. * can be undone in reverse (see {@link Journal}).
  48. */
  49. private Journal? journal = null;
  50. /**
  51. * Working sets computed by {@link strategise}: {@link to_install}
  52. * plus downgrade targets, and {@link to_remove} plus downgrade
  53. * currents — the whole transaction runs against these.
  54. */
  55. private Set<CachedPackage> planned_install = new HashSet<CachedPackage>();
  56. private Set<CachedPackage> planned_removal = new HashSet<CachedPackage>();
  57. /** Downgrade targets (installed with {@link InstallType.DOWNGRADE}) and currents (removed with {@link RemoveType.DOWNGRADE}). */
  58. private Set<CachedPackage> downgrade_targets = new HashSet<CachedPackage>();
  59. private Set<CachedPackage> downgrade_currents = new HashSet<CachedPackage>();
  60. /** Removals whose package name this transaction reinstalls — update removals ({@link RemoveType.UPGRADE}). */
  61. private Set<CachedPackage> upgrade_removals = new HashSet<CachedPackage>();
  62. /**
  63. * Lots computed by {@link strategise}: each lot builds, tests and
  64. * installs together, and lots run in dependency order. A package
  65. * joins a lot only when every BUILD-phase ref is already present or
  66. * provided by an earlier lot, so downstream builds always see the
  67. * upstream package's installed `pc:`/`vapi:` artifacts.
  68. */
  69. public Vector<Vector<CachedPackage>> install_lots { get; private set; }
  70. /** Removal order computed by {@link strategise}: dependents before their providers. */
  71. public Vector<CachedPackage> removal_order { get; private set; }
  72. /**
  73. * Rebuild entries planned by {@link strategise} for packages in
  74. * {@link to_install} flagged
  75. * {@link ManifestFlag.REBUILD_DEPENDANTS}: each entry rebuilds its
  76. * installed {@link RebuildEntry.package} — same CachedPackage,
  77. * same version, same state path — through the standard pipeline,
  78. * tagged {@link TransactionTask.REBUILDING} in progress reports.
  79. * Empty when nothing is flagged or {@link state} is unset.
  80. */
  81. public Vector<RebuildEntry> rebuilds { get; private set; default = new Vector<RebuildEntry>(); }
  82. public void run() throws TransactionError {
  83. // 1. Verify the transaction is valid
  84. strategise();
  85. journal = new Journal(journal_backup_root());
  86. try {
  87. journal.begin();
  88. var all_packages = planned_removal.concat(planned_install);
  89. var rebuild_packages = rebuilds.select<CachedPackage>(r => r.package).to_vector();
  90. // 2. Unpack packages
  91. do_for(all_packages, unpack_package, TransactionTask.UNPACKING);
  92. do_for(rebuild_packages, unpack_package, TransactionTask.REBUILDING);
  93. // 3. Remove packages
  94. do_for(removal_order, remove_package, TransactionTask.REMOVING);
  95. foreach (var lot in install_lots) {
  96. // 3. Build packages
  97. do_for(lot, build_package, TransactionTask.BUILDING);
  98. // 4. Test packages
  99. do_for(lot, test_package, TransactionTask.TESTING);
  100. // 5. Install packages
  101. do_for(lot, install_package, TransactionTask.INSTALLING);
  102. }
  103. // Rebuilds run the same pipeline once every flagged package is
  104. // installed, so dependants build against the fresh artifacts
  105. do_for(rebuild_packages, unpack_package, TransactionTask.UNPACKING);
  106. do_for(rebuild_packages, build_package, TransactionTask.REBUILDING);
  107. do_for(rebuild_packages, test_package, TransactionTask.TESTING);
  108. do_for(rebuild_packages, install_package, TransactionTask.INSTALLING);
  109. // 6. Clean up
  110. do_for(all_packages, cleanup_package, TransactionTask.CLEANING_UP);
  111. do_for(rebuild_packages, cleanup_package, TransactionTask.CLEANING_UP);
  112. journal.finish();
  113. }
  114. catch(TransactionError e) {
  115. rollback_and_report();
  116. throw e;
  117. }
  118. catch(Error e) {
  119. rollback_and_report();
  120. throw new TransactionError.UNKNOWN_ERROR(@"Error running transaction: $(e.message)");
  121. }
  122. finally {
  123. journal = null;
  124. }
  125. }
  126. /**
  127. * Rebuilds one already-installed package through the standard
  128. * pipeline — build-cache restore, clean-retry on failure,
  129. * reinstall of its resources — tagged
  130. * {@link TransactionTask.REBUILDING} throughout; the `usm
  131. * rebuild` entry point. No confirmation is needed: nothing
  132. * installed changes version.
  133. */
  134. public void rebuild_package(CachedPackage package) throws TransactionError {
  135. var packages = new Vector<CachedPackage>();
  136. packages.add(package);
  137. task_count = 5;
  138. current_task = 0;
  139. current_subject = package.package_name;
  140. current_task_type = TransactionTask.REBUILDING;
  141. journal = new Journal(journal_backup_root());
  142. try {
  143. journal.begin();
  144. do_for(packages, unpack_package, TransactionTask.UNPACKING);
  145. do_for(packages, build_package, TransactionTask.REBUILDING);
  146. do_for(packages, test_package, TransactionTask.TESTING);
  147. do_for(packages, install_package, TransactionTask.INSTALLING);
  148. do_for(packages, cleanup_package, TransactionTask.CLEANING_UP);
  149. journal.finish();
  150. }
  151. catch(TransactionError e) {
  152. rollback_and_report();
  153. throw e;
  154. }
  155. catch(Error e) {
  156. rollback_and_report();
  157. throw new TransactionError.UNKNOWN_ERROR(@"Error rebuilding package $(package.package_name): $(e.message)");
  158. }
  159. finally {
  160. journal = null;
  161. }
  162. }
  163. /**
  164. * Rolls the active journal back after a failure and prints what
  165. * was undone; a rollback failure is reported by the journal
  166. * itself and never masks the original error.
  167. */
  168. private void rollback_and_report() {
  169. if(journal == null) {
  170. return;
  171. }
  172. journal.rollback();
  173. printerr(journal.describe_rollback() + "\n");
  174. }
  175. /**
  176. * `<destination>/<local-state>/usm/backup` — the journal's home,
  177. * rooted like every other destination this transaction writes
  178. * (`/var/usm/backup` on an unredirected system).
  179. */
  180. private string journal_backup_root() {
  181. return Path.build_filename(paths.destination, paths.local_state, "usm", "backup");
  182. }
  183. private string previous_key = "";
  184. public void print_progress(TransactionTask task_type, string subject, uint current_task, uint total_tasks, float task_progress) {
  185. var verb = task_type.get_verb();
  186. verb = verb[0].toupper().to_string() + verb.substring(1);
  187. var percent = (int)(task_progress * 100.0f);
  188. var key = @"$(task_type.get_verb())_$(subject)_$current_task";
  189. var prefix = previous_key == key ? "\x1b[1F\x1b[2K" : "";
  190. previous_key = key;
  191. printerr(@"$prefix[$(current_task+1)/$total_tasks] $verb $subject ($percent%)\n");
  192. }
  193. public void print_progress_simple(TransactionTask task_type, string subject, uint current_task, uint total_tasks, float task_progress) {
  194. var verb = task_type.get_verb();
  195. verb = verb[0].toupper().to_string() + verb.substring(1);
  196. var percent = (int)(task_progress * 100.0f);
  197. printerr(@"[$(current_task+1)/$total_tasks] $verb $subject ($percent%)\n");
  198. }
  199. public void strategise() throws TransactionError {
  200. // Reset the task counters — strategise may be called twice
  201. // (once for the summary/confirm, once inside run())
  202. current_task = 0;
  203. task_count = 0;
  204. planned_install = new HashSet<CachedPackage>();
  205. planned_install.union_with(to_install);
  206. planned_removal = new HashSet<CachedPackage>();
  207. planned_removal.union_with(to_remove);
  208. downgrade_targets = new HashSet<CachedPackage>();
  209. downgrade_currents = new HashSet<CachedPackage>();
  210. foreach(var entry in to_downgrade) {
  211. planned_install.add(entry.target);
  212. downgrade_targets.add(entry.target);
  213. planned_removal.add(entry.current);
  214. downgrade_currents.add(entry.current);
  215. }
  216. // A removal whose package name this transaction reinstalls is
  217. // an update removal, not a final one
  218. upgrade_removals = new HashSet<CachedPackage>();
  219. try {
  220. var install_names = new HashSet<string>();
  221. foreach(var package in planned_install) {
  222. install_names.add(package.get_manifest().name);
  223. }
  224. foreach(var package in planned_removal) {
  225. if(install_names.contains(package.get_manifest().name)) {
  226. upgrade_removals.add(package);
  227. }
  228. }
  229. }
  230. catch(Error e) {
  231. throw new TransactionError.UNKNOWN_ERROR(@"Failed to read manifest while planning removals: $(e.message)");
  232. }
  233. task_count = (planned_install.count() * 5) + (planned_removal.count() * 3) + 1;
  234. uint strategise_worst_case_task_count = (planned_removal.count() * planned_removal.count()) + (planned_install.count() * planned_install.count());
  235. uint strategise_current_task = 0;
  236. report_progress(TransactionTask.STRATEGISING, 0.0f);
  237. // Installation strategy
  238. install_lots = new Vector<Vector<CachedPackage>>();
  239. var touched = new HashSet<CachedPackage>();
  240. var installed_by_earlier_lots = new HashSet<ResourceRef>();
  241. var ordered_install = ordered_by_names(planned_install, install_order);
  242. var round = 0;
  243. while(true) {
  244. strategise_current_task = round * planned_install.count();
  245. var lot = new Vector<CachedPackage>();
  246. var installed_by_this_lot = new HashSet<ResourceRef>();
  247. var remaining = ordered_install.exclude(touched);
  248. if(remaining.count() == 0) {
  249. break;
  250. }
  251. foreach (var package in ordered_install.exclude(touched)) {
  252. report_progress(TransactionTask.STRATEGISING, (float)strategise_current_task / (float)strategise_worst_case_task_count);
  253. try {
  254. var manifest = package.get_manifest();
  255. // Build-phase refs must be satisfied by resources
  256. // already present or installed by an EARLIER lot: a
  257. // same-lot provide is not installed yet when this
  258. // package builds, so a build dependency on it defers
  259. // the package to the next lot
  260. PredicateDelegate<ResourceRef> buildtime_satisfied = d => resource_finder.has_resource(d)
  261. || installed_by_earlier_lots.any(r => d.satisfied_by(r));
  262. // Manage-phase executables run at install time, when
  263. // everything this lot has already admitted (in
  264. // topological order) is installed
  265. PredicateDelegate<ResourceRef> installtime_satisfied = d => resource_finder.has_resource(d)
  266. || installed_by_earlier_lots.any(r => d.satisfied_by(r))
  267. || installed_by_this_lot.any(r => d.satisfied_by(r));
  268. if(manifest.dependencies.manage.is_satisfied(installtime_satisfied) &&
  269. manifest.dependencies.build.is_satisfied(buildtime_satisfied)) {
  270. lot.add(package);
  271. touched.add(package);
  272. installed_by_this_lot.union_with(manifest.provides.select<ResourceRef>(p => p.key));
  273. }
  274. strategise_current_task++;
  275. }
  276. catch(Error e) {
  277. throw new TransactionError.UNKNOWN_ERROR(@"Failed to read manifest for package \"$(package.package_name)\": $(e.message)");
  278. }
  279. }
  280. if(lot.count() == 0) {
  281. var packages = ordered_install.exclude(touched).to_string(p => p.package_name, ", ");
  282. throw new TransactionError.INVALID_TRANSACTION(@"Could not build a transaction strategy, packages $(packages) have unmet or cyclical dependencies");
  283. }
  284. installed_by_earlier_lots.union_with(installed_by_this_lot);
  285. install_lots.add(lot);
  286. round++;
  287. }
  288. var current_task_baseline = (planned_install.count() * planned_install.count());
  289. strategise_current_task = current_task_baseline;
  290. report_progress(TransactionTask.STRATEGISING, (float)strategise_current_task / (float)strategise_worst_case_task_count);
  291. // Removal strategy
  292. removal_order = new Vector<CachedPackage>();
  293. if(remove_order != null) {
  294. removal_order = ordered_by_names(planned_removal, remove_order);
  295. }
  296. else {
  297. Set<CachedPackageManifest> remaining_to_remove;
  298. try {
  299. remaining_to_remove = planned_removal
  300. .attempt_select<CachedPackageManifest>(p => new CachedPackageManifest(p))
  301. .to_set();
  302. }
  303. catch(Error e) {
  304. throw new TransactionError.UNKNOWN_ERROR(@"Failed to read manifest: $(e.message)");
  305. }
  306. round = 0;
  307. while(true) {
  308. strategise_current_task = current_task_baseline + (round * planned_removal.count());
  309. if(remaining_to_remove.count() == 0) {
  310. break;
  311. }
  312. var removed_this_round = false;
  313. foreach (var package in remaining_to_remove.sort((a, b) => a.package.package_name.collate(b.package.package_name))) {
  314. report_progress(TransactionTask.STRATEGISING, (float)strategise_current_task / (float)strategise_worst_case_task_count);
  315. if(remaining_to_remove.no(p => p.manifest.dependencies.manage.all_refs().any(d => package.manifest.provides.any(r => d.satisfied_by(r.key))))) {
  316. removal_order.add(package.package);
  317. remaining_to_remove.remove(package);
  318. strategise_current_task++;
  319. round++;
  320. removed_this_round = true;
  321. break;
  322. }
  323. strategise_current_task++;
  324. }
  325. if(!removed_this_round) {
  326. var packages = remaining_to_remove.to_string(p => p.package.package_name, ", ");
  327. throw new TransactionError.INVALID_TRANSACTION(@"Could not build a transaction strategy, packages $(packages) have unmet or cyclical dependencies");
  328. }
  329. }
  330. }
  331. plan_rebuilds();
  332. task_count += rebuilds.length * 5;
  333. report_progress(TransactionTask.STRATEGISING, 1.0f);
  334. current_task++;
  335. }
  336. /**
  337. * Plans rebuildDependants rebuilds: every package in
  338. * {@link to_install} flagged
  339. * {@link ManifestFlag.REBUILD_DEPENDANTS} names its installed
  340. * dependants via {@link SystemState.find_dependant_names}, and each
  341. * one neither installed nor removed by this transaction rebuilds
  342. * exactly once — dependants the transaction already handles are
  343. * skipped, and two flagged packages sharing a dependant rebuild it
  344. * once only. Without a {@link state} there is nothing to scan.
  345. */
  346. private void plan_rebuilds() throws TransactionError {
  347. rebuilds = new Vector<RebuildEntry>();
  348. if(state == null) {
  349. return;
  350. }
  351. try {
  352. var installed_by_name = new Dictionary<string, CachedPackage>();
  353. foreach(var installed in state.get_installed_packages()) {
  354. // Resolve through the installed symlink so a rebuild
  355. // carries the cache path: reinstalling through the
  356. // installed path itself would make mark_installed
  357. // relink the symlink onto itself
  358. var info = File.new_for_path(installed.state_path).query_info(FileAttribute.STANDARD_SYMLINK_TARGET, FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
  359. var target = info.get_symlink_target();
  360. installed_by_name.set(installed.package_name, target != null ? new CachedPackage(target) : installed);
  361. }
  362. var planned = new HashSet<string>();
  363. foreach(var package in planned_install) {
  364. planned.add(package.get_manifest().name);
  365. }
  366. foreach(var package in planned_removal) {
  367. planned.add(package.get_manifest().name);
  368. }
  369. foreach(var package in planned_install) {
  370. var manifest = package.get_manifest();
  371. if(manifest.flags == null || !manifest.flags.has(ManifestFlag.REBUILD_DEPENDANTS)) {
  372. continue;
  373. }
  374. foreach(var name in state.find_dependant_names(package)) {
  375. CachedPackage dependant;
  376. if(!installed_by_name.try_get(name, out dependant)) {
  377. continue;
  378. }
  379. var dependant_name = dependant.get_manifest().name;
  380. if(planned.has(dependant_name)) {
  381. continue;
  382. }
  383. planned.add(dependant_name);
  384. rebuilds.add(new RebuildEntry() {
  385. package = dependant,
  386. trigger = package
  387. });
  388. }
  389. }
  390. }
  391. catch(Error e) {
  392. throw new TransactionError.UNKNOWN_ERROR(@"Failed to plan dependant rebuilds: $(e.message)");
  393. }
  394. }
  395. /**
  396. * Orders a transaction package set by the given manifest-name order
  397. * (from a {@link ResolutionResult}); names with no package in the set
  398. * are skipped (for example supplied cache packages filtered out by
  399. * the caller). Without an order the set is sorted by name for
  400. * determinism. Throws when an order covers none of a package's set
  401. * entries — every package must be covered.
  402. */
  403. private Vector<CachedPackage> ordered_by_names(Set<CachedPackage> packages, Vector<string>? names) throws TransactionError {
  404. if(names == null) {
  405. return packages.sort((a, b) => a.package_name.collate(b.package_name)).to_vector();
  406. }
  407. var by_name = new Dictionary<string, CachedPackage>();
  408. foreach(var package in packages) {
  409. try {
  410. by_name.set(package.get_manifest().name, package);
  411. }
  412. catch(Error e) {
  413. throw new TransactionError.UNKNOWN_ERROR(@"Failed to read manifest for package \"$(package.package_name)\": $(e.message)");
  414. }
  415. }
  416. var ordered = new Vector<CachedPackage>();
  417. var covered = new HashSet<CachedPackage>();
  418. foreach(var name in names) {
  419. CachedPackage package;
  420. if(by_name.try_get(name, out package)) {
  421. ordered.add(package);
  422. covered.add(package);
  423. }
  424. }
  425. var uncovered_names = packages.exclude(covered).to_string(p => p.package_name, ", ");
  426. if(uncovered_names.length > 0) {
  427. throw new TransactionError.INVALID_TRANSACTION(@"The supplied package order does not cover: $(uncovered_names)");
  428. }
  429. return ordered;
  430. }
  431. private delegate void PackageDelegate(CachedPackage package) throws Error;
  432. private void do_for(Enumerable<CachedPackage> packages, PackageDelegate func, TransactionTask task_type) throws TransactionError {
  433. foreach (var package in packages) {
  434. try {
  435. current_subject = package.package_name;
  436. current_task_type = task_type;
  437. report_progress(task_type, 0.0f);
  438. func(package);
  439. current_task++;
  440. }
  441. catch (TransactionError e) {
  442. throw e;
  443. }
  444. catch(Error e) {
  445. throw new TransactionError.UNKNOWN_ERROR(@"Error $(task_type.get_verb()) package $(package.package_name): $(e.message)");
  446. }
  447. }
  448. }
  449. private void report_progress(TransactionTask task, float progress) {
  450. progress_updated(task, current_subject, current_task, task_count, progress);
  451. }
  452. public void unpack_package(CachedPackage package) throws Error {
  453. // A clean copy of the sources is all any phase needs — builds,
  454. // tests, installs and remove scripts all run from the source
  455. // directory — so removals must not require a build artifact
  456. // to exist (see {@link cleanup_package})
  457. package.clean_source();
  458. report_progress(current_task_type, 0.5f);
  459. package.get_source_directory((fraction) => {
  460. report_progress(current_task_type, 0.5f + (fraction * 0.5f));
  461. });
  462. report_progress(current_task_type, 1.0f);
  463. }
  464. /**
  465. * Builds a package through the unified build-cache path: an
  466. * existing build directory or a build archive (restored by
  467. * {@link CachedPackage.get_build_directory}) makes the build
  468. * incremental, and a failure atop either is retried once from
  469. * extracted sources — a stale cache is the likeliest culprit, and
  470. * the clean-retry error propagates when that also fails. A failure
  471. * on an already-clean build propagates directly.
  472. */
  473. private void build_package(CachedPackage package) throws Error {
  474. var reused = package.has_build_directory() || package.has_build_archive();
  475. try {
  476. attempt_build(package);
  477. }
  478. catch(Error e) {
  479. if(!reused) {
  480. throw e;
  481. }
  482. package.clean_build_directory();
  483. package.clean_source();
  484. package.get_source_directory();
  485. package.create_build_directory();
  486. attempt_build(package);
  487. }
  488. report_progress(current_task_type, 1.0f);
  489. }
  490. /**
  491. * Runs the package's build script against the unified
  492. * build-directory path: an existing build tree is reused and a
  493. * build archive is restored in place (both incremental), and only
  494. * with neither does a fresh build directory appear — a missing
  495. * cache is fine, the user may have cleaned it up. The caller owns
  496. * the clean-retry decision (see {@link build_package}).
  497. */
  498. private void attempt_build(CachedPackage package) throws Error {
  499. // Get source directory, and reuse, restore or create the build directory
  500. string build_dir;
  501. if(package.has_build_directory() || package.has_build_archive()) {
  502. build_dir = package.get_build_directory();
  503. }
  504. else {
  505. build_dir = package.create_build_directory();
  506. }
  507. var source_dir = package.get_source_directory();
  508. // Change directory to sources
  509. Environment.set_current_dir(source_dir);
  510. var manifest = new Usm.Manifest.from_file("MANIFEST.usm");
  511. // Build package
  512. var build_proc = manifest.run_build(build_dir, paths, SubprocessFlags.STDOUT_SILENCE, (progress) => {
  513. report_progress(current_task_type, progress);
  514. });
  515. build_proc.wait_check();
  516. }
  517. private void test_package(CachedPackage package) throws Error {
  518. var source_dir = package.get_source_directory();
  519. var build_dir = package.get_build_directory();
  520. // "cd" into the source directory and read the manifest
  521. Environment.set_current_dir(source_dir);
  522. var manifest = new Usm.Manifest.from_file("MANIFEST.usm");
  523. // Run test process if present
  524. var test_proc = manifest.run_test(build_dir, SubprocessFlags.STDOUT_SILENCE);
  525. if(test_proc != null) {
  526. test_proc.wait_check();
  527. }
  528. // A package with no test executable still transitions through
  529. // the testing task, so completion is reported either way
  530. report_progress(current_task_type, 1.0f);
  531. }
  532. private void remove_package(CachedPackage package) throws Error {
  533. // Get source and build directories
  534. var source_dir = package.get_source_directory();
  535. // "cd" into the source directory and read the manifest
  536. Environment.set_current_dir(source_dir);
  537. var manifest = new Usm.Manifest.from_file("MANIFEST.usm");
  538. // A downgrade removal keeps the state directory for the way
  539. // back; an update removal is followed by a reinstall; anything
  540. // else is final
  541. var removal_type = downgrade_currents.has(package)
  542. ? RemoveType.DOWNGRADE
  543. : upgrade_removals.has(package) ? RemoveType.UPGRADE : RemoveType.FINAL;
  544. // Journal before anything is deleted: every existing resource
  545. // file is backed up and the installed mark's target recorded,
  546. // so a later failure puts the package back exactly as it was
  547. var files_removed = manifest.report_remove_resources(paths);
  548. journal.entry_remove(package, files_removed.to_array(), state.installed_mark_path(package), state.read_installed_target(package));
  549. foreach(var path in files_removed) {
  550. journal.backup_file(path, package.package_name);
  551. }
  552. // Run remove process if present
  553. var build_proc = manifest.run_remove(removal_type, SubprocessFlags.STDOUT_SILENCE);
  554. if(build_proc != null)
  555. build_proc.wait_check();
  556. manifest.remove_resources( paths, (r, cr, tr, f) => report_progress(current_task_type, ((float)cr + (float)f) / (float)tr));
  557. state.unmark_installed(package);
  558. journal.complete(package.package_name);
  559. }
  560. private void install_package(CachedPackage package) throws Error {
  561. var source_dir = package.get_source_directory();
  562. var build_dir = package.get_build_directory();
  563. // "cd" into the source directory and read the manifest
  564. Environment.set_current_dir(source_dir);
  565. var manifest = new Usm.Manifest.from_file("MANIFEST.usm");
  566. report_progress(current_task_type, 0.0f);
  567. string? install_dir = null;
  568. // Downgrade targets install over their current version rather
  569. // than fresh
  570. var install_type = downgrade_targets.has(package) ? InstallType.DOWNGRADE : InstallType.FRESH;
  571. // Journal before anything changes on disk: the destination
  572. // set comes from the manifest's provides, every file about to
  573. // be overwritten is backed up, and the installed mark's
  574. // previous target is recorded so the rollback restores it
  575. Vector<string> files_created;
  576. Vector<string> files_overwritten;
  577. manifest.report_install_resources(paths, out files_created, out files_overwritten);
  578. journal.entry_install(package, files_created.to_array(), files_overwritten.to_array(),
  579. state.installed_mark_path(package), state.read_installed_target(package));
  580. foreach(var path in files_overwritten) {
  581. journal.backup_file(path, package.package_name);
  582. }
  583. // Run install process if present
  584. if(manifest.executables.install != null) {
  585. install_dir = package.create_install_directory();
  586. // Only a ninjaStyleProgress install script streams
  587. // parseable Installing lines; everything else stays
  588. // silenced (see {@link Manifest.run_install})
  589. ProgressDelegate? install_progress = null;
  590. if(manifest.flags != null && manifest.flags.has(ManifestFlag.NINJA_STYLE_PROGRESS)) {
  591. install_progress = (fraction) => {
  592. report_progress(current_task_type, fraction);
  593. };
  594. }
  595. var install_proc = manifest.run_install(build_dir, install_dir, paths, install_type, SubprocessFlags.STDOUT_SILENCE, install_progress);
  596. install_proc.wait_check();
  597. }
  598. // Install the package's resources
  599. manifest.install_resources(source_dir, build_dir, install_dir, paths, (r, cr, tr, f) => report_progress(current_task_type, ((float)cr + (float)f) / (float)tr));
  600. // Run post install process if present
  601. report_progress(current_task_type, 1.0f);
  602. var post_install_proc = manifest.run_post_install(build_dir, install_type, SubprocessFlags.STDOUT_SILENCE);
  603. if(post_install_proc != null)
  604. post_install_proc.wait_check();
  605. // Update the system state, and cleanup
  606. state.mark_installed(package);
  607. record_origin(package, manifest);
  608. journal.complete(package.package_name);
  609. }
  610. /**
  611. * Writes the package's origin record after install: a record
  612. * already in this cache directory (a rebuild reinstalls the same
  613. * one) keeps its provenance and its explicit mark — a rebuild
  614. * must not demote an explicitly installed package to
  615. * orphan-removable — while a fresh directory starts a record
  616. * whose only known field is the mark, set from whether the user
  617. * named the package in this transaction.
  618. */
  619. private void record_origin(CachedPackage package, Manifest manifest) throws Error {
  620. OriginInformation origin;
  621. try {
  622. origin = package.get_origin_information();
  623. }
  624. catch(Error e) {
  625. origin = new OriginInformation();
  626. }
  627. var named = explicit_packages != null && explicit_packages.contains(manifest.name);
  628. origin.explicitly_installed = named || origin.explicitly_installed;
  629. package.update_origin_information(origin);
  630. }
  631. private void cleanup_package(CachedPackage package) throws Error {
  632. // A package with no build artifact (for example after `usm
  633. // clean builds`) has nothing to archive; its cache stays as-is
  634. if(package.has_build_directory() || package.has_build_archive()) {
  635. package.archive_build((compression) => {
  636. report_progress(TransactionTask.CLEANING_UP, compression * 0.25f);
  637. });
  638. }
  639. report_progress(TransactionTask.CLEANING_UP, 0.25f);
  640. package.clean_source();
  641. report_progress(TransactionTask.CLEANING_UP, 0.5f);
  642. package.clean_install();
  643. report_progress(TransactionTask.CLEANING_UP, 0.75f);
  644. report_progress(TransactionTask.CLEANING_UP, 1.0f);
  645. }
  646. }
  647. /**
  648. * One planned rebuildDependants rebuild: {@link package} is the
  649. * installed {@link CachedPackage} — same version, same state path —
  650. * that rebuilds through the standard pipeline tagged
  651. * {@link TransactionTask.REBUILDING}, and {@link trigger} is the
  652. * flagged package whose install/update pulled it in.
  653. */
  654. public class RebuildEntry {
  655. public CachedPackage package { get; set; }
  656. public CachedPackage trigger { get; set; }
  657. }
  658. /**
  659. * One planned version downgrade: {@link current} is the installed
  660. * {@link CachedPackage} whose resources are removed — its state
  661. * directory is kept, enabling the downgrade back — and {@link target}
  662. * is the older version installed in its place.
  663. */
  664. public class DowngradeEntry {
  665. public CachedPackage target { get; set; }
  666. public CachedPackage current { get; set; }
  667. }
  668. public enum TransactionTask {
  669. STRATEGISING,
  670. UNPACKING,
  671. BUILDING,
  672. TESTING,
  673. REMOVING,
  674. INSTALLING,
  675. CLEANING_UP,
  676. /** The rebuildDependants pipeline for an already-installed package (see {@link RebuildEntry}). */
  677. REBUILDING;
  678. public string get_verb() {
  679. switch (this) {
  680. case STRATEGISING:
  681. return "preparing a strategy for";
  682. case UNPACKING:
  683. return "unpacking";
  684. case BUILDING:
  685. return "building";
  686. case TESTING:
  687. return "testing";
  688. case REMOVING:
  689. return "removing";
  690. case INSTALLING:
  691. return "installing";
  692. case CLEANING_UP:
  693. return "cleaning up";
  694. case REBUILDING:
  695. return "rebuilding";
  696. default:
  697. assert_not_reached();
  698. }
  699. }
  700. }
  701. public errordomain TransactionError {
  702. INVALID_TRANSACTION,
  703. UNKNOWN_ERROR,
  704. BUILD_ERROR,
  705. INSTALL_ERROR
  706. }
  707. }