using Invercargill; namespace Usm { public class Transaction { public Paths paths { get; set; } public SystemState state { get; set; } public Set to_install { get; set; } public Set 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> install_lots; private Vector 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+1)/$total_tasks] $verb $subject ($percent%)\n"); } public void print_progress_simple(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); printerr(@"[$(current_task+1)/$total_tasks] $verb $subject ($percent%)\n"); } public void strategise() throws TransactionError { task_count = (to_install.count() * 4) + (to_remove.count() * 3) + 1; var strategise_worst_case_task_count = (to_remove.count() * to_remove.count()) + (to_install.count() * to_install.count()); var strategise_current_task = 0; report_progress(TransactionTask.STRATEGISING, 0.0f); // Installation strategy install_lots = new Vector>(); var touched = new Set(); var available_resources = new Set(); var round = 0; while(true) { strategise_current_task = round * to_install.count(); var lot = new Vector(); var remaining = to_install.difference(touched); if(remaining.count() == 0) { break; } foreach (var package in to_install.difference(touched)) { report_progress(TransactionTask.STRATEGISING, (float)strategise_current_task / (float)strategise_worst_case_task_count); try { var manifest = package.get_manifest(); var installtime_dependencies = manifest.dependencies.manage.concat(manifest.dependencies.build); if(installtime_dependencies.all(d => d.is_satisfied() || available_resources.any(r => d.satisfied_by(r)))) { lot.add(package); touched.add(package); available_resources.add_all(manifest.provides.select(p => p.key)); } strategise_current_task++; } catch(Error e) { throw new TransactionError.UNKNOWN_ERROR(@"Failed to read manifest for package \"$(package.package_name)\": $(e.message)"); } } if(lot.count() == 0) { var packages = to_install.difference(touched).to_string(p => p.package_name, ", "); throw new TransactionError.INVALID_TRANSACTION(@"Could not build a transaction strategy, packages $(packages) have unmet or cyclical dependencies"); } install_lots.add(lot); round++; } var current_task_baseline = (to_install.count() * to_install.count()); strategise_current_task = current_task_baseline; report_progress(TransactionTask.STRATEGISING, (float)strategise_current_task / (float)strategise_worst_case_task_count); // Removal strategy remove_order = new Vector(); touched = new Set(); Set remaining_to_remove; try { remaining_to_remove = to_remove .attempt_select(p => new CachedPackageManifest(p)) .to_set(); } catch(Error e) { throw new TransactionError.UNKNOWN_ERROR(@"Failed to read manifest: $(e.message)"); } round = 0; while(true) { strategise_current_task = current_task_baseline + (round * to_remove.count()); if(remaining_to_remove.count() == 0) { break; } foreach (var package in remaining_to_remove) { report_progress(TransactionTask.STRATEGISING, (float)strategise_current_task / (float)strategise_worst_case_task_count); if(remaining_to_remove.no(p => p.manifest.dependencies.manage.any(d => package.manifest.provides.any(r => d.satisfied_by(r.key))))) { remove_order.add(package.package); touched.add(package.package); remaining_to_remove.remove(package); strategise_current_task++; round++; break; } strategise_current_task++; } var packages = remaining_to_remove.to_string(p => p.package.package_name, ", "); throw new TransactionError.INVALID_TRANSACTION(@"Could not build a transaction strategy, packages $(packages) have unmet or cyclical dependencies"); } report_progress(TransactionTask.STRATEGISING, 1.0f); current_task++; } private delegate void PackageDelegate(CachedPackage package) throws Error; private void do_for(Enumerable 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(); // "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(); manifest.remove_resources( paths, (r, cr, tr, f) => report_progress(TransactionTask.INSTALLING, ((float)cr + (float)f) / (float)tr)); 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); string? install_dir = null; // Run install process if present if(manifest.executables.install != null) { install_dir = package.create_install_directory(); var install_proc = manifest.run_install(build_dir, install_dir, paths, InstallType.FRESH, SubprocessFlags.STDOUT_SILENCE); install_proc.wait_check(); } // Install the package's resources manifest.install_resources(source_dir, build_dir, install_dir, paths, (r, cr, tr, f) => report_progress(TransactionTask.INSTALLING, ((float)cr + (float)f) / (float)tr)); // Run post install process if present report_progress(TransactionTask.INSTALLING, 1.0f); var post_install_proc = manifest.run_post_install(build_dir, InstallType.FRESH, SubprocessFlags.STDOUT_SILENCE); if(post_install_proc != null) post_install_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.33334f); package.clean_source(); report_progress(TransactionTask.CLEANING_UP, 0.66667f); package.clean_install(); 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 } }