Просмотр исходного кода

Move state path into config, move resource install/remove logic into manifest class, implement transaction strategy

Billy Barrow 6 месяцев назад
Родитель
Сommit
b0fadb1376

+ 14 - 11
src/cli/Install.vala

@@ -20,24 +20,27 @@ private int install_main(string[] args) {
         printerr("\n");
     }
 
-    var target = resolver.find_package(args[2]);
-    var client = target.repository.get_client();
-
-    printerr("3\n");
-    var path = state.generate_cache_path(target.manifest);
-    File.new_for_path(path).make_directory();
-    var package_path = Path.build_filename(path, "package.usmc");
-    client.download_package(package_path, target.repository_entry, (f, c, t) => printerr(@"Downloading $f $c/$t bytes\r"));
-    client.verify_package(package_path, target.repository_entry, (f, c, t) => printerr(@"Verifying $f $c/$t bytes\r"));
-    var cached_package = new Usm.CachedPackage(path);
+    var cached_packages = new Set<Usm.CachedPackage>();
+    for(int i = 2; i < args.length; i++) {
+        var target = resolver.find_package(args[i]);
+        var client = target.repository.get_client();
+        var path = state.generate_cache_path(target.manifest);
+        File.new_for_path(path).make_directory();
+        var package_path = Path.build_filename(path, "package.usmc");
+        client.download_package(package_path, target.repository_entry, (f, c, t) => printerr(@"Downloading $f $c/$t bytes\r"));
+        client.verify_package(package_path, target.repository_entry, (f, c, t) => printerr(@"Verifying $f $c/$t bytes\r"));
+        cached_packages.add(new Usm.CachedPackage(path));
+        printerr("\n");
+    }
 
     var transaction = new Usm.Transaction() {
         paths = paths,
         to_remove = new Set<Usm.CachedPackage>(),
-        to_install = single(cached_package).to_set(),
+        to_install = cached_packages,
         state = state
     };
 
+    printerr("\nRunning transaction...\n");
     transaction.progress_updated.connect(transaction.print_progress);
     transaction.run();
 

+ 53 - 0
src/lib/Configuration.vala

@@ -0,0 +1,53 @@
+using Invercargill;
+
+namespace Usm {
+
+    public class Configuration {
+
+        public bool is_managed { get; set; }
+        public ManagedConfiguration? managed_config { get; set; }
+
+        public static PropertyMapper<Configuration> get_mapper() {
+            return PropertyMapper.build_for<Configuration>(cfg => {
+                cfg.map<bool>("is_managed", o => o.is_managed, (o, v) => o.is_managed = v);
+                cfg.map_with<ManagedConfiguration>("managed", o => o.managed_config, (o, v) => o.managed_config = v, ManagedConfiguration.get_mapper(), false);
+                cfg.set_constructor(() => new Configuration());
+            });
+        }
+
+        public Configuration.from_paths(Paths paths) throws Error {
+            var element = new InvercargillJson.JsonElement.from_file(Path.build_filename(paths.usm_config_dir, "usm.config"));
+            get_mapper().map_into(this, element.as<Invercargill.Properties>());
+        }
+
+        public static bool check_managed(Paths paths) {
+            var config_path = Path.build_filename(paths.usm_config_dir, "usm.config");
+            if(!File.new_for_path(config_path).query_exists()) {
+                return false;
+            }
+
+            try {
+                return new Configuration.from_paths(paths).is_managed;
+            }
+            catch(Error e) {
+                warning(@"Error reading configuration: $(e.message)");
+                return false;
+            }
+        }
+
+    }
+
+    public class ManagedConfiguration {
+
+        public string state_path { get; set; }
+
+        public static PropertyMapper<ManagedConfiguration> get_mapper() {
+            return PropertyMapper.build_for<ManagedConfiguration>(cfg => {
+                cfg.map<string>("state_path", o => o.state_path, (o, v) => o.state_path = v);
+                cfg.set_constructor(() => new ManagedConfiguration());
+            });
+        }     
+
+    }
+
+}

+ 67 - 1
src/lib/Manifest.vala

@@ -44,7 +44,7 @@ namespace Usm {
                 cfg.map_many<string>("screenshots", o => o.screenshot_paths, (o, v) => o.screenshot_paths = v.to_vector(), false);
                 cfg.map<string>("icon", o => o.icon_path, (o, v) => o.icon_path = v, false);
                 cfg.map<string>("metainfo", o => o.metainfo_path, (o, v) => o.metainfo_path = v, false);
-                cfg.map_with<Git>("git", o => o.git, (o, v) => o.git = v, Git.get_mapper(), false);
+                //  cfg.map_with<Git>("git", o => o.git, (o, v) => o.git = v, Git.get_mapper(), false);
                 cfg.map<Properties>("extras", o => o.extra_properties, (o, v) => o.extra_properties = v, false);
                 cfg.set_constructor(() => new Manifest());
             });
@@ -157,6 +157,72 @@ namespace Usm {
             return proc;
         }
 
+        public delegate void ResourceProgressCallback(ResourceRef resource, int current_resource, int total_resources, float resource_frac);
+        public void install_resources(string build_path, Paths paths, ResourceProgressCallback callback) throws Error {
+            // Install each resource speficied by the manifest
+            var resource_count = provides.count();
+            var resources_installed = 0;
+            foreach (var resource in provides) {
+                callback(resource.key, resources_installed, resource_count, 0.0f);
+                var path = paths.get_suggested_path(resource.key);
+                if(resource.value.file_type == Usm.ManifestFileType.REGULAR) {
+                    var src = File.new_build_filename(build_path, resource.value.path);
+                    var dest = File.new_for_path(path);
+                    src.copy(dest, FileCopyFlags.OVERWRITE, null, (c, t) => callback(resource.key, resources_installed, resource_count, (float)c / (float)t));
+                }
+                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)\"");
+                }
+                
+                callback(resource.key, resources_installed, resource_count, 1.0f);
+                resources_installed++;
+            }
+        }
+
+        public void remove_resources(Paths paths, ResourceProgressCallback callback) throws Error {
+            var non_directories = provides.where(r => r.value.file_type != Usm.ManifestFileType.DIRECTORY).cache();
+            var directories = provides.where(r => r.value.file_type == Usm.ManifestFileType.DIRECTORY).cache();
+            var total_operations = non_directories.count() + directories.count();
+            var current_operation = 0;
+
+            // Delete files and symlinks first
+            foreach (var resource in non_directories) {
+                callback(resource.key, current_operation, total_operations, 0.0f);
+                var path = paths.get_suggested_path(resource.key);
+                var file = File.new_for_path(path);
+                if(file.query_exists()) {
+                    file.delete();
+                }
+                callback(resource.key, current_operation, total_operations, 1.0f);
+                current_operation++;
+            }
+
+            // Delete directories last
+            foreach (var resource in directories) {
+                callback(resource.key, current_operation, total_operations, 0.0f);
+                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");
+                }
+                callback(resource.key, current_operation, total_operations, 1.0f);
+                current_operation++;
+            }
+        }
+
     }
 
 

+ 0 - 3
src/lib/Paths.vala

@@ -19,7 +19,6 @@ namespace Usm {
         public string sys_config { get; set; }
 
         public string usm_config_dir { get; set; }
-        public string usm_state_dir { get; set; }
 
         public void set_envs() {
             Environment.set_variable("DESTDIR", destination, true);
@@ -100,7 +99,6 @@ namespace Usm {
             sys_config = "etc";
 
             usm_config_dir = "/etc/usm";
-            usm_state_dir = "/var/usm";
         }
 
         public Paths.usm_environ() {
@@ -122,7 +120,6 @@ namespace Usm {
             sys_config = Environment.get_variable("USM_SYSCONFIGDIR") ?? defaults.sys_config;
 
             usm_config_dir = Environment.get_variable("USM_CONFIGDIR") ?? defaults.usm_config_dir;
-            usm_state_dir = Environment.get_variable("USM_STATEDIR") ?? defaults.usm_state_dir;
         }
     }
 

+ 2 - 0
src/lib/Repository/Repository.vala

@@ -7,6 +7,7 @@ namespace Usm {
         public string summary { get; set; }
         public string url { get; set; }
         public BinaryData key { get; set; }
+        public int cache_expiration { get; set; }
 
 
         public static PropertyMapper<Repository> get_mapper() {
@@ -15,6 +16,7 @@ namespace Usm {
                 cfg.map<string>("summary", o => o.summary, (o, v) => o.summary = v);
                 cfg.map<string>("url", o => o.url, (o, v) => o.url = v, false);
                 cfg.map<string>("key", o => o.key.to_base64(), (o, v) => o.key = new BinaryData.from_base64(v));
+                cfg.map<int>("cache_expiration", o => o.cache_expiration, (o, v) => o.cache_expiration = v);
                 cfg.set_constructor(() => new Repository());
             });
         }      

+ 4 - 0
src/lib/ResourceRef.vala

@@ -138,6 +138,10 @@ namespace Usm {
         public string to_string() {
             return @"$resource_type:$resource";
         }
+
+        public bool satisfied_by(ResourceRef other) {
+            return equals(other);
+        }
         
         public bool is_satisfied() {
             switch (resource_type) {

+ 10 - 0
src/lib/State/CachedPackage.vala

@@ -129,4 +129,14 @@ namespace Usm {
 
     }
 
+    public class CachedPackageManifest {
+        public CachedPackage package { get; private set; }
+        public Manifest manifest { get; private set; }
+
+        public CachedPackageManifest(CachedPackage package) throws Error {
+            this.package = package;
+            this.manifest = package.get_manifest();
+        }
+    }
+
 }

+ 16 - 3
src/lib/State/State.vala

@@ -6,10 +6,21 @@ namespace Usm {
 
         public string config_path { get; private set; }
         public string state_path { get; private set; }
+        public Configuration config { get; private set; }
 
-        public SystemState(Paths paths) {
-            config_path = paths.usm_config_dir;
-            state_path = paths.usm_state_dir;
+        public SystemState(Paths paths) throws StateError {
+            if(!Configuration.check_managed(paths)) {
+                throw new StateError.SYSTEM_NOT_MANAGED("System state cannot be created as this system/environment is not managed");
+            }
+            
+            try {
+                config = new Configuration.from_paths(paths);
+                config_path = paths.usm_config_dir;
+                state_path = config.managed_config.state_path;
+            }
+            catch (Error e) {
+                throw new StateError.CONFIGURATION_ERROR(@"Error reading configuration: $(e.message)");
+            }
         }
 
         public Enumerable<CachedPackage> get_cached_packages() throws Error {
@@ -77,6 +88,8 @@ namespace Usm {
 
     public errordomain StateError {
         NO_BUILD_ARTIFACT,
+        SYSTEM_NOT_MANAGED,
+        CONFIGURATION_ERROR
     }
 
 }

+ 93 - 50
src/lib/Transaction.vala

@@ -53,18 +53,105 @@ namespace Usm {
             var prefix = previous_key == key ? "\x1b[1F\x1b[2K" : "";
             previous_key = key;
 
-            printerr(@"$prefix[$current_task/$total_tasks] $verb $subject ($percent%)\n");
+            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<Vector<CachedPackage>>();
-            install_lots.add(to_install.to_vector());
+            var touched = new Set<CachedPackage>();
+            var available_resources = new Set<ResourceRef>();
+            var round = 0;
+            while(true) {
+                strategise_current_task = round * to_install.count();
+                var lot = new Vector<CachedPackage>();
+                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<ResourceRef>(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<CachedPackage>();
+            touched = new Set<CachedPackage>();
+            Set<CachedPackageManifest> remaining_to_remove;
+            try {
+                remaining_to_remove = to_remove
+                    .try_select<CachedPackageManifest>(p => new CachedPackageManifest(p))
+                    .unwrap_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);
-            task_count = (to_install.count() * 4) + (to_remove.count() * 3);
             current_task++;
         }
 
@@ -137,28 +224,7 @@ namespace Usm {
             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");
-                }
-            }
+                manifest.remove_resources( paths, (r, cr, tr, f) => report_progress(TransactionTask.INSTALLING, ((float)cr + (float)f) / (float)tr));
 
             state.unmark_installed(package);
         }
@@ -172,31 +238,8 @@ namespace Usm {
             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);
-            }
+            // Install the package's resources
+            manifest.install_resources(build_dir, paths, (r, cr, tr, f) => report_progress(TransactionTask.INSTALLING, ((float)cr + (float)f) / (float)tr));
 
             // Run install process if present
             var build_proc = manifest.run_install(build_dir, InstallType.FRESH, SubprocessFlags.STDOUT_SILENCE);

+ 1 - 0
src/lib/meson.build

@@ -13,6 +13,7 @@ sources += files('Version.vala')
 sources += files('Resolver.vala')
 sources += files('Util.vala')
 sources += files('Transaction.vala')
+sources += files('Configuration.vala')
 sources += files('Repository/Repository.vala')
 sources += files('Repository/RepositoryListing.vala')
 sources += files('Repository/RepositoryClient.vala')

+ 6 - 0
usm.config

@@ -0,0 +1,6 @@
+{
+    "is_managed": true,
+    "managed": {
+        "state_path": "/var/usm"
+    }
+}