|
@@ -0,0 +1,254 @@
|
|
|
|
+using Invercargill;
|
|
|
|
+
|
|
|
|
+namespace Usm {
|
|
|
|
+
|
|
|
|
+ public class Transaction {
|
|
|
|
+
|
|
|
|
+ public Paths paths { get; set; }
|
|
|
|
+ public SystemState state { get; set; }
|
|
|
|
+ public Set<CachedPackage> to_install { get; set; }
|
|
|
|
+ public Set<CachedPackage> to_remove { get; set; }
|
|
|
|
+
|
|
|
|
+ public signal void progress_updated(TransactionTask task_type, string subject, int current_task, int total_tasks, float task_progress);
|
|
|
|
+
|
|
|
|
+ private int task_count = 0;
|
|
|
|
+ private int current_task = 0;
|
|
|
|
+ private string current_subject = "transaction";
|
|
|
|
+ private Vector<Vector<CachedPackage>> install_lots;
|
|
|
|
+ private Vector<CachedPackage> remove_order;
|
|
|
|
+
|
|
|
|
+ public void run() throws TransactionError {
|
|
|
|
+
|
|
|
|
+ // 1. Verify the transaction is valid
|
|
|
|
+ strategise();
|
|
|
|
+
|
|
|
|
+ var all_packages = to_remove.concat(to_install);
|
|
|
|
+
|
|
|
|
+ // 2. Unpack packages
|
|
|
|
+ do_for(all_packages, unpack_package, TransactionTask.UNPACKING);
|
|
|
|
+
|
|
|
|
+ // 3. Remove packages
|
|
|
|
+ do_for(remove_order, remove_package, TransactionTask.REMOVING);
|
|
|
|
+
|
|
|
|
+ foreach (var lot in install_lots) {
|
|
|
|
+ // 3. Build packages
|
|
|
|
+ do_for(lot, build_package, TransactionTask.BUILDING);
|
|
|
|
+
|
|
|
|
+ // 5. Install packages
|
|
|
|
+ do_for(lot, install_package, TransactionTask.INSTALLING);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // 6. Clean up
|
|
|
|
+ do_for(all_packages, cleanup_package, TransactionTask.CLEANING_UP);
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private string previous_key = "";
|
|
|
|
+ public void print_progress(TransactionTask task_type, string subject, int current_task, int total_tasks, float task_progress) {
|
|
|
|
+ var verb = task_type.get_verb();
|
|
|
|
+ verb = verb[0].toupper().to_string() + verb.substring(1);
|
|
|
|
+ var percent = (int)(task_progress * 100.0f);
|
|
|
|
+
|
|
|
|
+ var key = @"$(task_type.get_verb())_$(subject)_$current_task";
|
|
|
|
+ var prefix = previous_key == key ? "\x1b[1F\x1b[2K" : "";
|
|
|
|
+ previous_key = key;
|
|
|
|
+
|
|
|
|
+ printerr(@"$prefix[$current_task/$total_tasks] $verb $subject ($percent%)\n");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public void strategise() throws TransactionError {
|
|
|
|
+ report_progress(TransactionTask.STRATEGISING, 0.0f);
|
|
|
|
+ install_lots = new Vector<Vector<CachedPackage>>();
|
|
|
|
+ install_lots.add(to_install.to_vector());
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+ report_progress(TransactionTask.STRATEGISING, 1.0f);
|
|
|
|
+ task_count = (to_install.count() * 4) + (to_remove.count() * 3);
|
|
|
|
+ current_task++;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private delegate void PackageDelegate(CachedPackage package) throws Error;
|
|
|
|
+ private void do_for(Enumerable<CachedPackage> packages, PackageDelegate func, TransactionTask task_type) throws TransactionError {
|
|
|
|
+ foreach (var package in packages) {
|
|
|
|
+ try {
|
|
|
|
+ current_subject = package.package_name;
|
|
|
|
+ report_progress(task_type, 0.0f);
|
|
|
|
+ func(package);
|
|
|
|
+ current_task++;
|
|
|
|
+ }
|
|
|
|
+ catch (TransactionError e) {
|
|
|
|
+ throw e;
|
|
|
|
+ }
|
|
|
|
+ catch(Error e) {
|
|
|
|
+ throw new TransactionError.UNKNOWN_ERROR(@"Error $(task_type.get_verb()) package $(package.package_name): $(e.message)");
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void report_progress(TransactionTask task, float progress) {
|
|
|
|
+ progress_updated(task, current_subject, current_task, task_count, progress);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public void unpack_package(CachedPackage package) throws Error {
|
|
|
|
+ var will_remove = to_remove.has(package);
|
|
|
|
+
|
|
|
|
+ // Get a clean copy of the sources
|
|
|
|
+ package.clean_source();
|
|
|
|
+ report_progress(TransactionTask.UNPACKING, will_remove ? 0.25f : 0.5f);
|
|
|
|
+ package.get_source_directory();
|
|
|
|
+ report_progress(TransactionTask.UNPACKING, will_remove ? 0.5f : 1.0f);
|
|
|
|
+
|
|
|
|
+ if(will_remove) {
|
|
|
|
+ // Get a clean copy of the build artifact
|
|
|
|
+ package.clean_build_directory();
|
|
|
|
+ report_progress(TransactionTask.UNPACKING, 0.75f);
|
|
|
|
+ package.get_build_directory();
|
|
|
|
+ report_progress(TransactionTask.UNPACKING, 1.0f);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void build_package(CachedPackage package) throws Error {
|
|
|
|
+ // Get source directory, and create build directory
|
|
|
|
+ var source_dir = package.get_source_directory();
|
|
|
|
+ var build_dir = package.create_build_directory();
|
|
|
|
+
|
|
|
|
+ // Change directory to sources
|
|
|
|
+ Environment.set_current_dir(source_dir);
|
|
|
|
+ var manifest = new Usm.Manifest.from_file("MANIFEST.usm");
|
|
|
|
+
|
|
|
|
+ // Build package
|
|
|
|
+ var build_proc = manifest.run_build(build_dir, paths, SubprocessFlags.STDOUT_SILENCE);
|
|
|
|
+ build_proc.wait_check();
|
|
|
|
+ report_progress(TransactionTask.BUILDING, 1.0f);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void remove_package(CachedPackage package) throws Error {
|
|
|
|
+ // Get source and build directories
|
|
|
|
+ var source_dir = package.get_source_directory();
|
|
|
|
+ var build_dir = package.get_build_directory();
|
|
|
|
+
|
|
|
|
+ // "cd" into the source directory and read the manifest
|
|
|
|
+ Environment.set_current_dir(source_dir);
|
|
|
|
+ var manifest = new Usm.Manifest.from_file("MANIFEST.usm");
|
|
|
|
+
|
|
|
|
+ // Run remove process if present
|
|
|
|
+ var build_proc = manifest.run_remove(RemoveType.FINAL, SubprocessFlags.STDOUT_SILENCE);
|
|
|
|
+ if(build_proc != null)
|
|
|
|
+ build_proc.wait_check();
|
|
|
|
+
|
|
|
|
+ // Delete files and symlinks first
|
|
|
|
+ foreach (var resource in manifest.provides.where(r => r.value.file_type != Usm.ManifestFileType.DIRECTORY)) {
|
|
|
|
+ var path = paths.get_suggested_path(resource.key);
|
|
|
|
+ var file = File.new_for_path(path);
|
|
|
|
+ if(file.query_exists()) {
|
|
|
|
+ file.delete();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Delete directories last
|
|
|
|
+ foreach (var resource in manifest.provides.where(r => r.value.file_type == Usm.ManifestFileType.DIRECTORY)) {
|
|
|
|
+ var path = paths.get_suggested_path(resource.key);
|
|
|
|
+ try {
|
|
|
|
+ var file = File.new_for_path(path);
|
|
|
|
+ if(file.query_exists()) {
|
|
|
|
+ file.delete();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ catch(IOError.NOT_EMPTY e) {
|
|
|
|
+ warning(@"Did not remove resource \"$path\": directory is not empty\n");
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ state.unmark_installed(package);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void install_package(CachedPackage package) throws Error {
|
|
|
|
+ var source_dir = package.get_source_directory();
|
|
|
|
+ var build_dir = package.get_build_directory();
|
|
|
|
+
|
|
|
|
+ // "cd" into the source directory and read the manifest
|
|
|
|
+ Environment.set_current_dir(source_dir);
|
|
|
|
+ var manifest = new Usm.Manifest.from_file("MANIFEST.usm");
|
|
|
|
+ report_progress(TransactionTask.INSTALLING, 0.0f);
|
|
|
|
+
|
|
|
|
+ // Install each resource speficied by the manifest
|
|
|
|
+ var resource_count = manifest.provides.count();
|
|
|
|
+ var resources_installed = 0;
|
|
|
|
+ foreach (var resource in manifest.provides) {
|
|
|
|
+ var path = paths.get_suggested_path(resource.key);
|
|
|
|
+ if(resource.value.file_type == Usm.ManifestFileType.REGULAR) {
|
|
|
|
+ var src = File.new_build_filename(build_dir, resource.value.path);
|
|
|
|
+ var dest = File.new_for_path(path);
|
|
|
|
+ src.copy(dest, FileCopyFlags.OVERWRITE);
|
|
|
|
+ }
|
|
|
|
+ else if(resource.value.file_type == Usm.ManifestFileType.DIRECTORY) {
|
|
|
|
+ var dest = File.new_for_path(path);
|
|
|
|
+ dest.make_directory();
|
|
|
|
+ }
|
|
|
|
+ else if(resource.value.file_type == Usm.ManifestFileType.SYMBOLIC_LINK) {
|
|
|
|
+ var dest = File.new_for_path(path);
|
|
|
|
+ dest.make_symbolic_link(resource.value.path);
|
|
|
|
+ }
|
|
|
|
+ else {
|
|
|
|
+ throw new TransactionError.INSTALL_ERROR(@"Could not understand resource key \"$(resource.key)\"");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ resources_installed++;
|
|
|
|
+ report_progress(TransactionTask.INSTALLING, (float)resources_installed / (float)resource_count);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Run install process if present
|
|
|
|
+ var build_proc = manifest.run_install(build_dir, InstallType.FRESH, SubprocessFlags.STDOUT_SILENCE);
|
|
|
|
+ if(build_proc != null)
|
|
|
|
+ build_proc.wait_check();
|
|
|
|
+
|
|
|
|
+ // Update the system state, and cleanup
|
|
|
|
+ state.mark_installed(package);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void cleanup_package(CachedPackage package) throws Error {
|
|
|
|
+ package.archive_build();
|
|
|
|
+ report_progress(TransactionTask.CLEANING_UP, 0.5f);
|
|
|
|
+ package.clean_source();
|
|
|
|
+ report_progress(TransactionTask.CLEANING_UP, 1.0f);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public enum TransactionTask {
|
|
|
|
+ STRATEGISING,
|
|
|
|
+ UNPACKING,
|
|
|
|
+ BUILDING,
|
|
|
|
+ REMOVING,
|
|
|
|
+ INSTALLING,
|
|
|
|
+ CLEANING_UP;
|
|
|
|
+
|
|
|
|
+ public string get_verb() {
|
|
|
|
+ switch (this) {
|
|
|
|
+ case STRATEGISING:
|
|
|
|
+ return "preparing a strategy for";
|
|
|
|
+ case UNPACKING:
|
|
|
|
+ return "unpacking";
|
|
|
|
+ case BUILDING:
|
|
|
|
+ return "building";
|
|
|
|
+ case REMOVING:
|
|
|
|
+ return "removing";
|
|
|
|
+ case INSTALLING:
|
|
|
|
+ return "installing";
|
|
|
|
+ case CLEANING_UP:
|
|
|
|
+ return "cleaning up";
|
|
|
|
+ default:
|
|
|
|
+ assert_not_reached();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public errordomain TransactionError {
|
|
|
|
+
|
|
|
|
+ INVALID_TRANSACTION,
|
|
|
|
+ UNKNOWN_ERROR,
|
|
|
|
+ BUILD_ERROR,
|
|
|
|
+ INSTALL_ERROR
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+
|
|
|
|
+}
|