|
@@ -0,0 +1,227 @@
|
|
|
+using Binman;
|
|
|
+using Invercargill;
|
|
|
+using InvercargillJson;
|
|
|
+
|
|
|
+public static int main(string[] args) {
|
|
|
+
|
|
|
+ if(args.length < 2 || args[1] == "--help" || args[1] == "-?") {
|
|
|
+ usage();
|
|
|
+ return 255;
|
|
|
+ }
|
|
|
+
|
|
|
+ var arguments = new Arguments(args);
|
|
|
+
|
|
|
+ try {
|
|
|
+ var header = new ManifestHeader();
|
|
|
+ header.name = arguments.name ?? arguments.directory.get_basename();
|
|
|
+ header.description = arguments.description ?? header.name;
|
|
|
+ header.remotes = arguments.remotes;
|
|
|
+ header.post_exec = arguments.post_exec;
|
|
|
+ header.serial = arguments.serial ?? 0;
|
|
|
+
|
|
|
+ var output_dir = arguments.output_path ?? Environment.get_current_dir();
|
|
|
+ var keyfile = arguments.key_file ?? generate_keyfile(output_dir);
|
|
|
+ header.key = get_public_key(keyfile);
|
|
|
+
|
|
|
+ var compressor = new ZlibCompressor(ZlibCompressorFormat.GZIP, 9);
|
|
|
+ var manifest_file = File.new_build_filename(output_dir, @"$(header.name).manifest");
|
|
|
+ var manifest_stream = new JsonlOutputStream(new ConverterOutputStream(manifest_file.create(FileCreateFlags.REPLACE_DESTINATION), compressor));
|
|
|
+ manifest_stream.write_object(ManifestHeader.get_mapper().map_from(header));
|
|
|
+
|
|
|
+ var queue = new Fifo<File>();
|
|
|
+ queue.push(arguments.directory);
|
|
|
+ queue.unblock();
|
|
|
+
|
|
|
+ var prefix = arguments.prefix ?? "/";
|
|
|
+ foreach (var item in queue) {
|
|
|
+ // Print log
|
|
|
+ write_line(arguments, @"Processing file $(item.get_path())");
|
|
|
+
|
|
|
+ // Gather information
|
|
|
+ Posix.Stat stat_info;
|
|
|
+ posix_to_error(Posix.stat(item.get_path(), out stat_info));
|
|
|
+ var relative_path = arguments.directory.get_relative_path(item);
|
|
|
+
|
|
|
+ // Create the entry
|
|
|
+ var entry = new ManifestEntry();
|
|
|
+ entry.path = Path.build_filename(prefix, relative_path);
|
|
|
+ entry.user = stat_info.st_uid;
|
|
|
+ entry.group = stat_info.st_gid;
|
|
|
+ entry.mode = stat_info.st_mode;
|
|
|
+
|
|
|
+ // Handle symlinks
|
|
|
+ if(Posix.S_ISLNK(entry.mode)) {
|
|
|
+ var target = new char[uint16.MAX];
|
|
|
+ target.length = (int)Posix.readlink(item.get_path(), target);
|
|
|
+ entry.target = (string)target;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle regular (non-empty) files
|
|
|
+ if(Posix.S_ISREG(entry.mode) && stat_info.st_size > 0) {
|
|
|
+ var binary = new ManifestFileDescriptor();
|
|
|
+ binary.size = stat_info.st_size;
|
|
|
+ binary.checksum = calculate_file_checksum(item.get_path());
|
|
|
+ binary.remote_path = generate_remote_binary_path(arguments, entry, binary.checksum);
|
|
|
+ entry.binary = binary;
|
|
|
+
|
|
|
+ // If we are copying files, copy the file
|
|
|
+ if(arguments.output_path != null) {
|
|
|
+ var dest = File.new_build_filename(arguments.output_path, binary.remote_path);
|
|
|
+ dest.get_parent().make_directory_with_parents();
|
|
|
+ item.copy(dest, FileCopyFlags.TARGET_DEFAULT_PERMS | FileCopyFlags.TARGET_DEFAULT_MODIFIED_TIME);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Handle directories
|
|
|
+ if(Posix.S_ISDIR(entry.mode)) {
|
|
|
+ var children = directory(item.get_path());
|
|
|
+ queue.push_all(children.select<File>(c => item.get_child(c)));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Query for license information
|
|
|
+ if(arguments.license_finder_exec != null) {
|
|
|
+ var license_path = get_result_from_exec(arguments.license_finder_exec, item.get_path());
|
|
|
+ entry.licence = Path.build_filename(prefix, license_path);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Query for source information TODO
|
|
|
+ if(arguments.source_finder_exec != null) {
|
|
|
+ var source_package_path = get_result_from_exec(arguments.source_finder_exec, item.get_path());
|
|
|
+ if(source_package_path != null && source_package_path.length > 0) {
|
|
|
+ var source_package = File.new_for_path(source_package_path);
|
|
|
+ var file_info = source_package.query_info("*", FileQueryInfoFlags.NONE);
|
|
|
+ var source = new ManifestFileDescriptor();
|
|
|
+ source.size = file_info.get_size();
|
|
|
+ source.checksum = calculate_file_checksum(source_package_path);
|
|
|
+ source.remote_path = generate_remote_source_path(arguments, source_package, source.checksum);
|
|
|
+ entry.source = source;
|
|
|
+
|
|
|
+ // If we are copying files, and the file hasn't been copied, copy the file
|
|
|
+ var dest = File.new_build_filename(arguments.output_path, source.remote_path);
|
|
|
+ if(arguments.output_path != null && !dest.query_exists()) {
|
|
|
+ dest.get_parent().make_directory_with_parents();
|
|
|
+ source_package.copy(dest, FileCopyFlags.TARGET_DEFAULT_PERMS | FileCopyFlags.TARGET_DEFAULT_MODIFIED_TIME);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Write the entry to the manifest
|
|
|
+ manifest_stream.write_object(ManifestEntry.get_mapper().map_from(entry));
|
|
|
+ }
|
|
|
+
|
|
|
+ // Generate signature
|
|
|
+ write_line(arguments, "Signing manifest...");
|
|
|
+ manifest_stream.flush();
|
|
|
+ var signature = new ManifestSignature.generate(manifest_file, get_signing_key(keyfile).to_array());
|
|
|
+
|
|
|
+ // Write signature
|
|
|
+ manifest_stream.write_object(ManifestSignature.get_mapper().map_from(signature));
|
|
|
+ manifest_stream.flush();
|
|
|
+ manifest_stream.close();
|
|
|
+
|
|
|
+ }
|
|
|
+ catch(Error e) {
|
|
|
+ printerr(@"Failed to build manifest: $(e.message)\n");
|
|
|
+ return e.code;
|
|
|
+ }
|
|
|
+
|
|
|
+ write_line(arguments, "Complete!");
|
|
|
+ return 0;
|
|
|
+}
|
|
|
+
|
|
|
+private void usage() {
|
|
|
+ printerr("USAGE:\n\tmanifest-build DIR [--name NAME] [--key KEYFILE] [--serial SERIAL] [--description DESCRIPTION] [--remote URI] [--post-exec POSTEXEC] [--output OUTDIR [--path-scheme SCHEME]] [--source-finder SOURCEXEC] [--license-finder LICENSEEXEC]\n");
|
|
|
+}
|
|
|
+
|
|
|
+private string generate_keyfile(string output_dir) throws Error {
|
|
|
+ var public_key = new uint8[Sodium.Asymmetric.Signing.PUBLIC_KEY_BYTES];
|
|
|
+ var secret_key = new uint8[Sodium.Asymmetric.Signing.SECRET_KEY_BYTES];
|
|
|
+ Sodium.Asymmetric.Signing.generate_keypair(public_key, secret_key);
|
|
|
+
|
|
|
+ var keyfile_data = new BinaryData.from_byte_array(public_key).to_base64();
|
|
|
+ keyfile_data += ".";
|
|
|
+ keyfile_data += new BinaryData.from_byte_array(secret_key).to_base64();
|
|
|
+
|
|
|
+ var path = Path.build_filename(output_dir, "keyfile.secret");
|
|
|
+ FileUtils.set_contents(path, keyfile_data, keyfile_data.length);
|
|
|
+ return path;
|
|
|
+}
|
|
|
+
|
|
|
+private BinaryData get_public_key(string keyfile) throws Error {
|
|
|
+ string contents;
|
|
|
+ FileUtils.get_contents(keyfile, out contents);
|
|
|
+ return new BinaryData.from_base64(contents.split(".")[0]);
|
|
|
+}
|
|
|
+
|
|
|
+private BinaryData get_signing_key(string keyfile) throws Error {
|
|
|
+ string contents;
|
|
|
+ FileUtils.get_contents(keyfile, out contents);
|
|
|
+ return new BinaryData.from_base64(contents.split(".")[1]);
|
|
|
+}
|
|
|
+
|
|
|
+private string generate_remote_binary_path(Arguments arguments, ManifestEntry entry, BinaryData checksum) {
|
|
|
+ var scheme = arguments.path_scheme ?? PathScheme.ANALOGUE;
|
|
|
+
|
|
|
+ // Analogue is forced if no output path is specified, because we don't actually copy anything
|
|
|
+ if(scheme == PathScheme.ANALOGUE || arguments.output_path == null) {
|
|
|
+ var prefix_path = File.new_for_path(arguments.prefix ?? "/");
|
|
|
+ var relative_path = prefix_path.get_relative_path(File.new_for_path(entry.path));
|
|
|
+ return Path.build_filename(arguments.remote_binary_prefix ?? "binaries", relative_path);
|
|
|
+ }
|
|
|
+
|
|
|
+ if(scheme == PathScheme.CONTENT_HASH) {
|
|
|
+ return checksum.to_base64().replace("/", "_");
|
|
|
+ }
|
|
|
+
|
|
|
+ if(scheme == PathScheme.PATH_HASH) {
|
|
|
+ var hasher = new Checksum(ChecksumType.SHA256);
|
|
|
+ hasher.update(entry.path.data, entry.path.data.length);
|
|
|
+ var result = new uint8[ChecksumType.SHA256.get_length()];
|
|
|
+ size_t size = result.length;
|
|
|
+ hasher.get_digest(result, ref size);
|
|
|
+ return new BinaryData.from_byte_array(result).to_base64().replace("/", "_");
|
|
|
+ }
|
|
|
+
|
|
|
+ assert_not_reached();
|
|
|
+}
|
|
|
+
|
|
|
+private string generate_remote_source_path(Arguments arguments, File source_package, BinaryData checksum) {
|
|
|
+ var scheme = arguments.path_scheme ?? PathScheme.ANALOGUE;
|
|
|
+
|
|
|
+ // Analogue is forced if no output path is specified, because we don't actually copy anything
|
|
|
+ if(scheme == PathScheme.ANALOGUE || arguments.output_path == null) {
|
|
|
+ return Path.build_filename(arguments.remote_source_prefix ?? "sources", source_package.get_basename());
|
|
|
+ }
|
|
|
+
|
|
|
+ if(scheme == PathScheme.CONTENT_HASH) {
|
|
|
+ return checksum.to_base64().replace("/", "_");
|
|
|
+ }
|
|
|
+
|
|
|
+ if(scheme == PathScheme.PATH_HASH) {
|
|
|
+ var hasher = new Checksum(ChecksumType.SHA256);
|
|
|
+ var path = source_package.get_basename();
|
|
|
+ hasher.update(path.data, path.data.length);
|
|
|
+ var result = new uint8[ChecksumType.SHA256.get_length()];
|
|
|
+ size_t size = result.length;
|
|
|
+ hasher.get_digest(result, ref size);
|
|
|
+ return new BinaryData.from_byte_array(result).to_base64().replace("/", "_");
|
|
|
+ }
|
|
|
+
|
|
|
+ assert_not_reached();
|
|
|
+}
|
|
|
+
|
|
|
+private string get_result_from_exec(string exec, string param) throws Error {
|
|
|
+ var process = new Subprocess.newv(new string[] {exec, param}, GLib.SubprocessFlags.STDOUT_PIPE);
|
|
|
+ var stream = process.get_stdout_pipe();
|
|
|
+ var buffer = new uint8[uint16.MAX];
|
|
|
+ size_t bytes_read;
|
|
|
+ stream.read_all(buffer, out bytes_read);
|
|
|
+ return ((string)buffer).chomp().chug();
|
|
|
+}
|
|
|
+
|
|
|
+private void write_line(Arguments arguments, string line) {
|
|
|
+ if(arguments.quiet){
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ printerr(@"$line\n");
|
|
|
+}
|