Manifest.vala 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086
  1. Usm.Manifest manifest = null;
  2. string? build_path = null;
  3. bool replace_provides = false;
  4. bool debug_mode = false;
  5. Usm.ResourceFinder resfinder = null;
  6. Usm.ResourceFinder destresfinder = null;
  7. public static int manifest_main(string[] args) {
  8. paths = new Usm.Paths();
  9. var resfinder_paths = paths.clone();
  10. resfinder_paths.destination = "/";
  11. resfinder = new Usm.ResourceFinder(resfinder_paths);
  12. destresfinder = new Usm.ResourceFinder(paths);
  13. if(args.length < 2) {
  14. manifest_usage();
  15. return 255;
  16. }
  17. var verb = args[1];
  18. // Check for --replace and --debug flags for autoprovides
  19. if(verb == "autoprovides") {
  20. for(int i = 2; i < args.length; i++) {
  21. if(args[i] == "--replace") {
  22. replace_provides = true;
  23. } else if(args[i] == "--debug") {
  24. debug_mode = true;
  25. } else if(build_path == null) {
  26. build_path = args[i];
  27. }
  28. }
  29. } else if(verb == "build") {
  30. // Optional for data packages (a no-op); enforced for regular packages in build()
  31. if(args.length >= 3) {
  32. build_path = args[2];
  33. }
  34. } else if(verb != "remove" && verb != "acquire" && verb != "install" && verb != "package" && verb != "deploy" && verb != "test" && verb != "validate") {
  35. if(args.length < 3) {
  36. manifest_usage();
  37. return 255;
  38. }
  39. build_path = args[2];
  40. }
  41. if(!File.new_for_path("MANIFEST.usm").query_exists()) {
  42. printerr("No MANIFEST.usm file found in current directory.\n");
  43. return 254;
  44. }
  45. try {
  46. var element = new InvercargillJson.JsonElement.from_file("MANIFEST.usm");
  47. manifest = Usm.Manifest.get_mapper().materialise(element.as<Invercargill.Properties>());
  48. manifest.validate();
  49. }
  50. catch (Error e) {
  51. printerr(@"Could not read MANIFEST.usm: $(e.message)\n");
  52. return 253;
  53. }
  54. // Data packages define no build step: nothing is staged, no build directory
  55. // or build.tar.xz is ever created for them
  56. if(manifest.is_data_package && verb == "build") {
  57. printerr(@"Data package \"$(manifest.name)\" has no build step, nothing to build.\n");
  58. return 0;
  59. }
  60. // Automatically create a temporary directory for these commands if one was not provided.
  61. if(!manifest.is_data_package && (verb == "install" || verb == "autoprovides" || verb == "test" || verb == "validate")) {
  62. if(build_path == null) {
  63. var dir = File.new_build_filename("/tmp", Uuid.string_random());
  64. try {
  65. dir.make_directory();
  66. }
  67. catch(Error e) {
  68. printerr(@"Could not create temporary build directory, try specifying one instead: $(e.message)\n");
  69. return 255;
  70. }
  71. build_path = dir.get_path();
  72. }
  73. }
  74. // Automatically create the specified directory for these commands if it doesn't exist
  75. if(!manifest.is_data_package && build_path != null && (verb == "install" || verb == "autoprovides" || verb == "test" || verb == "build" || verb == "validate")) {
  76. var build_dir = File.new_for_path(build_path);
  77. if(!build_dir.query_exists()) {
  78. printerr(@"Creating build directory: $build_path\n");
  79. try {
  80. build_dir.make_directory_with_parents();
  81. }
  82. catch(Error e) {
  83. printerr(@"Could not create build directory: $(e.message)\n");
  84. return 255;
  85. }
  86. }
  87. }
  88. if(verb == "build") {
  89. return build();
  90. }
  91. if(verb == "install") {
  92. return install();
  93. }
  94. if(verb == "acquire") {
  95. return acquire();
  96. }
  97. if(verb == "remove") {
  98. return uininstall();
  99. }
  100. if(verb == "package") {
  101. return package();
  102. }
  103. if(verb == "deploy") {
  104. return manifest_deploy(args);
  105. }
  106. if(verb == "autoprovides") {
  107. return autoprovides();
  108. }
  109. if(verb == "test") {
  110. return test();
  111. }
  112. if(verb == "validate") {
  113. return validate();
  114. }
  115. manifest_usage();
  116. return 255;
  117. }
  118. private void manifest_usage() {
  119. printerr("USAGE:\n\tusm manifest build <build path>\n\tusm manifest install <build path>\n\tusm manifest remove\nusm manifest acquire\nusm manifest package\nusm manifest deploy [--exec CMD] [--base IMAGE] [--repository FILE]... [--no-build] [--installer-url URL]\nusm manifest autoprovides [--replace] [--debug] [build path]\nusm manifest test [build path]\nusm manifest validate [build path]\n");
  120. }
  121. /**
  122. * Checks one dependency phase of the loaded manifest against the resources
  123. * {@link finder} can see, printing the same "Missing … dependency" lines as
  124. * before for flat phases.
  125. *
  126. * Grouped phases choose the first group whose members are all present (this
  127. * flow installs nothing, so every group costs nothing and manifest order
  128. * wins); when no group is viable an itemised per-group report is printed.
  129. * Returns false when the phase's requirements are unmet.
  130. */
  131. private bool phase_dependencies_satisfied(Usm.DependencyPhase? phase, string label, Usm.ResourceFinder finder) {
  132. if(phase == null) {
  133. return true;
  134. }
  135. if(!phase.is_grouped) {
  136. var sane = true;
  137. foreach(var resource in phase.ordered_required()) {
  138. if(!finder.has_resource(resource)) {
  139. printerr(@"Missing $label dependency \"$resource\".\n");
  140. sane = false;
  141. }
  142. }
  143. return sane;
  144. }
  145. var groups = phase.ordered_groups();
  146. foreach(var group in groups) {
  147. var missing = group.where(r => !finder.has_resource(r)).to_vector();
  148. if(missing.length == 0) {
  149. return true;
  150. }
  151. }
  152. printerr(@"No viable $label dependency group:\n");
  153. for(uint index = 0; index < groups.length; index++) {
  154. printerr(@" Group $(index + 1) ($(groups[index].to_string(r => r.to_string(), ", "))):\n");
  155. foreach(var resource in groups[index].where(r => !finder.has_resource(r))) {
  156. printerr(@" Missing $label dependency \"$resource\".\n");
  157. }
  158. }
  159. return false;
  160. }
  161. private int build() {
  162. if(build_path == null) {
  163. manifest_usage();
  164. return 255;
  165. }
  166. // Create build directory if it doesn't exist
  167. var sane = phase_dependencies_satisfied(manifest.dependencies.manage, "management", resfinder) &
  168. phase_dependencies_satisfied(manifest.dependencies.build, "build", resfinder);
  169. if(!sane) {
  170. printerr("Could not build manifest, missing dependencies\n");
  171. return 252;
  172. }
  173. try {
  174. var proc = manifest.run_build(build_path, paths, SubprocessFlags.INHERIT_FDS);
  175. proc.wait_check();
  176. }
  177. catch(Error e) {
  178. printerr(@"Error running build exec: $(e.message)\n");
  179. return 251;
  180. }
  181. return 0;
  182. }
  183. private int install() {
  184. // Data packages define no build or install execs; their provides are
  185. // copied directly from the source tree
  186. if(manifest.is_data_package) {
  187. return install_data_package();
  188. }
  189. // Create build directory if it doesn't exist
  190. var build_dir = File.new_for_path(build_path);
  191. if(!build_dir.query_exists()) {
  192. printerr(@"Creating build directory: $build_path\n");
  193. try {
  194. build_dir.make_directory_with_parents();
  195. }
  196. catch(Error e) {
  197. printerr(@"Could not create build directory: $(e.message)\n");
  198. return 255;
  199. }
  200. }
  201. var sane = phase_dependencies_satisfied(manifest.dependencies.manage, "management", resfinder) &
  202. phase_dependencies_satisfied(manifest.dependencies.build, "build", resfinder) &
  203. phase_dependencies_satisfied(manifest.dependencies.runtime, "runtime", destresfinder);
  204. if(!sane) {
  205. printerr("Could not install manifest, missing dependencies\n");
  206. return 252;
  207. }
  208. try {
  209. var proc = manifest.run_build(build_path, paths, SubprocessFlags.INHERIT_FDS, frac => printerr(@"Building '$(manifest.name)': $((int)(frac*100))%\r"));
  210. proc.wait_check();
  211. printerr("\n");
  212. }
  213. catch(Error e) {
  214. printerr(@"\nError running build exec: $(e.message)\n");
  215. return 251;
  216. }
  217. var install_dir = File.new_build_filename("/tmp", Uuid.string_random());
  218. if(manifest.executables.install != null) {
  219. try {
  220. install_dir.make_directory();
  221. }
  222. catch(Error e) {
  223. printerr(@"Could not create temporary install directory: $(e.message)\n");
  224. return 250;
  225. }
  226. try {
  227. manifest.run_install(build_path, install_dir.get_path(), paths, Usm.InstallType.FRESH, SubprocessFlags.INHERIT_FDS).wait_check();
  228. }
  229. catch(Error e) {
  230. printerr(@"Error running install exec: $(e.message)\n");
  231. return 249;
  232. }
  233. }
  234. else {
  235. install_dir = null;
  236. }
  237. try {
  238. manifest.install_resources(Environment.get_current_dir(), build_path, install_dir?.get_path(), paths, (r, c, t, f) => {
  239. if(f == 0.0f) {
  240. printerr(@"Installing resource $(c+1)/$t: \"$(r)\"\n");
  241. }
  242. });
  243. }
  244. catch(Error e) {
  245. printerr(@"Error installing manifest resources: $(e.message)\n");
  246. return 248;
  247. }
  248. try {
  249. var proc = manifest.run_post_install(build_path, Usm.InstallType.FRESH, SubprocessFlags.INHERIT_FDS);
  250. if(proc != null) {
  251. proc.wait_check();
  252. }
  253. }
  254. catch(Error e) {
  255. printerr(@"Error running post install exec: $(e.message)\n");
  256. return 247;
  257. }
  258. return 0;
  259. }
  260. /**
  261. * Installs a data package: every provide is copied directly from the source
  262. * tree to its suggested path, with no build or install staging involved.
  263. */
  264. private int install_data_package() {
  265. if(!phase_dependencies_satisfied(manifest.dependencies.runtime, "runtime", destresfinder)) {
  266. printerr("Could not install manifest, missing dependencies\n");
  267. return 252;
  268. }
  269. try {
  270. manifest.install_resources(Environment.get_current_dir(), Environment.get_current_dir(), null, paths, (r, c, t, f) => {
  271. if(f == 0.0f) {
  272. printerr(@"Installing resource $(c+1)/$t: \"$(r)\"\n");
  273. }
  274. });
  275. }
  276. catch(Error e) {
  277. printerr(@"Error installing manifest resources: $(e.message)\n");
  278. return 248;
  279. }
  280. return 0;
  281. }
  282. private int acquire() {
  283. if(manifest.executables.acquire == null) {
  284. printerr(@"Manifest does not reference an acquire script\n");
  285. return 248;
  286. }
  287. // The acquire phase's own refs gate acquisition (it runs on the host)
  288. if(!phase_dependencies_satisfied(manifest.dependencies.acquire, "acquire", resfinder)) {
  289. printerr("Could not acquire manifest, missing dependencies\n");
  290. return 247;
  291. }
  292. try {
  293. var proc = manifest.run_acquire(SubprocessFlags.INHERIT_FDS);
  294. proc.wait_check();
  295. }
  296. catch(Error e) {
  297. printerr(@"Error running build exec: $(e.message)\n");
  298. return 251;
  299. }
  300. return 0;
  301. }
  302. private int uininstall() {
  303. if(!phase_dependencies_satisfied(manifest.dependencies.manage, "management", resfinder)) {
  304. printerr("Could not remove manifest, missing dependencies\n");
  305. return 252;
  306. }
  307. try {
  308. var proc = manifest.run_remove(Usm.RemoveType.FINAL, SubprocessFlags.INHERIT_FDS);
  309. if(proc != null) {
  310. proc.wait_check();
  311. }
  312. }
  313. catch(Error e) {
  314. printerr(@"Error running remove exec: $(e.message)\n");
  315. return 249;
  316. }
  317. // Do files and symlinks first
  318. foreach (var resource in manifest.provides.where(r => r.value.file_type != Usm.ManifestFileType.DIRECTORY)) {
  319. var path = paths.get_suggested_path_for_resource(resource.key);
  320. printerr(@"Deleting resource $(resource.key) from $path...");
  321. try {
  322. var file = File.new_for_path(path);
  323. if(file.query_exists()) {
  324. file.delete();
  325. printerr(" done\n");
  326. }
  327. else {
  328. printerr(" file missing\n");
  329. }
  330. }
  331. catch(Error e) {
  332. printerr(@" $(e.message)\n");
  333. return 250;
  334. }
  335. }
  336. // Do directories last
  337. foreach (var resource in manifest.provides.where(r => r.value.file_type == Usm.ManifestFileType.DIRECTORY)) {
  338. var path = paths.get_suggested_path_for_resource(resource.key);
  339. printerr(@"Deleting resource $(resource.key) from $path...");
  340. try {
  341. var file = File.new_for_path(path);
  342. if(file.query_exists()) {
  343. file.delete();
  344. printerr(" done\n");
  345. }
  346. else {
  347. printerr(" file missing\n");
  348. }
  349. }
  350. catch(IOError.NOT_EMPTY e) {
  351. printerr(" directory not empty\n");
  352. }
  353. catch(Error e) {
  354. printerr(@" $(e.message)\n");
  355. return 250;
  356. }
  357. }
  358. return 0;
  359. }
  360. private int package() {
  361. printerr("Writing archive...\n");
  362. var output = @"../$(manifest.name)-$(manifest.version).usmc";
  363. try {
  364. // Collect everything not excluded by the root .usmignore (falling
  365. // back to the .git-only default), pruning ignored directories
  366. var ignore = new Usm.UsmIgnore.from_root(Environment.get_current_dir());
  367. var entries = new Invercargill.DataStructures.Vector<string>();
  368. package_collect(".", ignore, entries);
  369. var list_file = File.new_build_filename("/tmp", Uuid.string_random());
  370. try {
  371. var stream = new DataOutputStream(list_file.replace(null, false, FileCreateFlags.REPLACE_DESTINATION));
  372. foreach(var entry in entries) {
  373. stream.put_string(@"./$entry\n");
  374. }
  375. stream.close();
  376. var proc = new Subprocess.newv(new string[] { "tar", "-cJf", output, "--no-recursion", "-T", list_file.get_path() }, SubprocessFlags.INHERIT_FDS);
  377. proc.wait_check();
  378. }
  379. finally {
  380. try {
  381. list_file.delete();
  382. }
  383. catch(Error e) {
  384. // The temporary file list is disposable; leaving it behind is harmless
  385. }
  386. }
  387. printerr(@"Wrote package to path \"$(output)\"\n");
  388. return 0;
  389. }
  390. catch(Error e) {
  391. printerr("Failed to write package\n");
  392. return 172;
  393. }
  394. }
  395. /**
  396. * Walks the package tree rooted at {@link relative_dir}, collecting the
  397. * relative paths of every file and directory not ignored by {@link ignore}.
  398. * Ignored directories are pruned: nothing beneath them is visited.
  399. */
  400. private static void package_collect(string relative_dir, Usm.UsmIgnore ignore, Invercargill.DataStructures.Vector<string> entries) throws Error {
  401. var folder = File.new_for_path(relative_dir);
  402. var enumerator = folder.enumerate_children("*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
  403. while(true) {
  404. FileInfo? info = enumerator.next_file();
  405. if(info == null) {
  406. break;
  407. }
  408. var relative_path = relative_dir == "." ? info.get_name() : @"$relative_dir/$(info.get_name())";
  409. var is_directory = info.get_file_type() == FileType.DIRECTORY;
  410. if(ignore.matches(relative_path, is_directory)) {
  411. continue;
  412. }
  413. entries.add(relative_path);
  414. if(is_directory) {
  415. package_collect(relative_path, ignore, entries);
  416. }
  417. }
  418. }
  419. private int autoprovides() {
  420. // Create build directory if it doesn't exist
  421. var build_dir = File.new_for_path(build_path);
  422. if(!build_dir.query_exists()) {
  423. printerr(@"Creating build directory: $build_path\n");
  424. try {
  425. build_dir.make_directory_with_parents();
  426. }
  427. catch(Error e) {
  428. printerr(@"Could not create build directory: $(e.message)\n");
  429. return 255;
  430. }
  431. }
  432. if(manifest.executables.install == null) {
  433. printerr(@"Autoprovides command only works with manifests that specify an installation executable.\n");
  434. return 1;
  435. }
  436. var sane = phase_dependencies_satisfied(manifest.dependencies.manage, "management", resfinder) &
  437. phase_dependencies_satisfied(manifest.dependencies.build, "build", resfinder) &
  438. phase_dependencies_satisfied(manifest.dependencies.runtime, "runtime", destresfinder);
  439. if(!sane) {
  440. printerr("Could not install manifest, missing dependencies\n");
  441. return 252;
  442. }
  443. try {
  444. Usm.ProgressDelegate? progress_callback = null;
  445. SubprocessFlags build_flags = SubprocessFlags.STDOUT_SILENCE;
  446. if (!debug_mode) {
  447. progress_callback = frac => printerr(@"Building '$(manifest.name)': $((int)(frac*100))%\r");
  448. } else {
  449. build_flags = SubprocessFlags.INHERIT_FDS;
  450. }
  451. var proc = manifest.run_build(build_path, paths, build_flags, progress_callback);
  452. proc.wait_check();
  453. if (!debug_mode) {
  454. printerr("\n");
  455. }
  456. }
  457. catch(Error e) {
  458. printerr(@"\nError running build exec: $(e.message)\n");
  459. return 251;
  460. }
  461. var install_dir = File.new_build_filename("/tmp", Uuid.string_random());
  462. try {
  463. install_dir.make_directory();
  464. }
  465. catch(Error e) {
  466. printerr(@"Could not create temporary install directory: $(e.message)\n");
  467. return 250;
  468. }
  469. try {
  470. SubprocessFlags install_flags = debug_mode ? SubprocessFlags.INHERIT_FDS : SubprocessFlags.STDOUT_SILENCE;
  471. manifest.run_install(build_path, install_dir.get_path(), paths, Usm.InstallType.FRESH, install_flags).wait_check();
  472. }
  473. catch(Error e) {
  474. printerr(@"Error running install exec: $(e.message)\n"
  475. );
  476. return 249;
  477. }
  478. try {
  479. var provides = autoprovides_scan_tree(install_dir.get_path(), install_dir.get_path());
  480. // Create a JSON representation of the provides dictionary
  481. var json_object = new InvercargillJson.JsonObject();
  482. foreach (var entry in provides) {
  483. // Handle special case: when pathBase is as-expected, path is empty, and type is reg
  484. // output string "as-expected" instead of full JSON object
  485. if(entry.value.path_base == Usm.ManifestFilePathBase.AS_EXPECTED &&
  486. entry.value.path == "" &&
  487. entry.value.file_type == Usm.ManifestFileType.REGULAR) {
  488. json_object.set_native(entry.key.to_string(), "as-expected");
  489. } else {
  490. var mapper = Usm.ManifestFile.get_mapper();
  491. json_object.set_native(entry.key.to_string(), mapper.map_from(entry.value));
  492. }
  493. }
  494. var json_string = json_object.as_element().stringify_pretty();
  495. if(replace_provides) {
  496. try {
  497. // Update the manifest's provides property
  498. manifest.provides = provides;
  499. // Serialize the updated manifest back to JSON
  500. var mapper = Usm.Manifest.get_mapper();
  501. var properties = mapper.map_from(manifest);
  502. var manifest_json_element = new InvercargillJson.JsonElement.from_properties(properties);
  503. var manifest_json_string = manifest_json_element.stringify_pretty();
  504. // Write the updated manifest to file
  505. var file = File.new_for_path("MANIFEST.usm");
  506. var data_stream = new DataOutputStream(file.replace(null, false, FileCreateFlags.REPLACE_DESTINATION));
  507. data_stream.put_string(manifest_json_string);
  508. printerr("Successfully updated 'provides' property in MANIFEST.usm\n");
  509. }
  510. catch(Error e) {
  511. printerr(@"Error updating MANIFEST.usm: $(e.message)\n");
  512. printerr("Original file left unchanged. Generated provides:\n");
  513. print(@"$json_string\n");
  514. return 249;
  515. }
  516. } else {
  517. print(@"$json_string\n");
  518. }
  519. return 0;
  520. }
  521. catch(Error e) {
  522. printerr(@"Error scanning installation result: $(e.message)\n");
  523. return 249;
  524. }
  525. }
  526. public static Invercargill.DataStructures.Dictionary<Usm.ResourceRef, Usm.ManifestFile> autoprovides_scan_tree(string install_root, string path) throws Error {
  527. // Creating a new Directory object for the given folder path
  528. var folder = File.new_for_path(path);
  529. // Getting a list of all files and folders inside the directory
  530. var enumerator = folder.enumerate_children("*", FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
  531. var provides = new Invercargill.DataStructures.Dictionary<Usm.ResourceRef, Usm.ManifestFile>();
  532. // Looping through each file/folder and removing them
  533. while (true) {
  534. FileInfo? info = enumerator.next_file();
  535. if (info == null) {
  536. break;
  537. }
  538. // Checking if the current item is a file or a folder
  539. if (info.get_file_type() == FileType.REGULAR) {
  540. var file = folder.get_child(info.get_name());
  541. var true_path = file.get_path().substring(install_root.length);
  542. var type = guess_resource_type(true_path);
  543. var relative_path = get_relative_path_for_type(true_path, type.to_string());
  544. // Remove leading slash from relative_path if present
  545. if (relative_path.has_prefix("/")) {
  546. relative_path = relative_path.substring(1);
  547. }
  548. // Create ResourceRef and ManifestFile
  549. var resource_ref = new Usm.ResourceRef.with_type(type, relative_path);
  550. var manifest_file = new Usm.ManifestFile.from_string("as-expected");
  551. provides[resource_ref] = manifest_file;
  552. }
  553. else if (info.get_file_type() == FileType.SYMBOLIC_LINK) {
  554. var file = folder.get_child(info.get_name());
  555. var true_path = file.get_path().substring(install_root.length);
  556. var type = guess_resource_type(true_path);
  557. var relative_path = get_relative_path_for_type(true_path, type.to_string());
  558. // Remove leading slash from relative_path if present
  559. if (relative_path.has_prefix("/")) {
  560. relative_path = relative_path.substring(1);
  561. }
  562. // Create ResourceRef and ManifestFile
  563. var resource_ref = new Usm.ResourceRef.with_type(type, relative_path);
  564. var manifest_file = new Usm.ManifestFile() {
  565. path_base = Usm.ManifestFilePathBase.AS_EXPECTED,
  566. file_type = Usm.ManifestFileType.SYMBOLIC_LINK,
  567. destination = info.get_symlink_target()
  568. };
  569. provides[resource_ref] = manifest_file;
  570. }
  571. else if (info.get_file_type() == FileType.DIRECTORY) {
  572. var subfolder = folder.get_child(info.get_name());
  573. var sub_provides = autoprovides_scan_tree(install_root, subfolder.get_path());
  574. // Add all entries from subdirectory
  575. foreach (var entry in sub_provides) {
  576. provides[entry.key] = entry.value;
  577. }
  578. }
  579. }
  580. return provides;
  581. }
  582. private static Usm.ResourceType guess_resource_type(string path) {
  583. // Remove leading slash if present
  584. string clean_path = path.has_prefix("/") ? path.substring(1) : path;
  585. // Create a temporary Paths object to get the default paths
  586. var default_paths = new Usm.Paths();
  587. // Find all matching resource types and calculate directory traversals
  588. var matching_types = new Invercargill.DataStructures.Vector<Usm.ResourceType>();
  589. var traversal_counts = new Invercargill.DataStructures.Vector<int>();
  590. // Define all resource types to check
  591. var types_to_check = new Invercargill.Composition<Usm.ResourceType>();
  592. types_to_check.append(Invercargill.Iterate.these<Usm.ResourceType>(
  593. Usm.ResourceType.BINARY,
  594. Usm.ResourceType.SUPER_BINARY,
  595. Usm.ResourceType.LIBRARY,
  596. Usm.ResourceType.LIBRARY_RESOURCE,
  597. Usm.ResourceType.LIBRARY_EXECUTABLE,
  598. Usm.ResourceType.INCLUDE,
  599. Usm.ResourceType.APPLICATION,
  600. Usm.ResourceType.INFO_PAGE,
  601. Usm.ResourceType.MANUAL_PAGE,
  602. Usm.ResourceType.LOCALE,
  603. Usm.ResourceType.VALA_API,
  604. Usm.ResourceType.GOBJECT_IR,
  605. Usm.ResourceType.TAG,
  606. Usm.ResourceType.RESOURCE,
  607. Usm.ResourceType.PKG_CONFIG,
  608. Usm.ResourceType.TYPELIB,
  609. Usm.ResourceType.CONFIGURATION,
  610. Usm.ResourceType.OPTIONAL
  611. ));
  612. // If the canonlib directory is different to the regular library path, add the canonical checks
  613. if(paths.canonlib != paths.lib) {
  614. types_to_check.append(Invercargill.Iterate.these<Usm.ResourceType>(
  615. Usm.ResourceType.CANONICAL_LIBRARY,
  616. Usm.ResourceType.CANONICAL_LIBRARY_RESOURCE
  617. ));
  618. }
  619. foreach (var type in types_to_check) {
  620. var suggested_path = default_paths.get_suggested_base_path_for_type(type);
  621. // Remove the destination prefix (usually "/") to get the relative path
  622. var relative_suggested_path = suggested_path.has_prefix("/") ?
  623. suggested_path.substring(1) : suggested_path;
  624. if (clean_path.has_prefix(relative_suggested_path)) {
  625. var relative_path = clean_path.substring(relative_suggested_path.length);
  626. // Count slashes (directory traversals) in the relative path
  627. int traversal_count = 0;
  628. for (int j = 0; j < relative_path.length; j++) {
  629. if (relative_path[j] == '/') {
  630. traversal_count++;
  631. }
  632. }
  633. // Library and Library resource have the same path. Try to have actual libraries get 'lib:' type, and all others 'libres:'
  634. var basename = Path.get_basename(path);
  635. var is_library = basename.has_suffix(".so") || basename.contains(".so.") || basename.has_suffix(".a") || basename.has_suffix(".o");
  636. if(!is_library && (type == Usm.ResourceType.LIBRARY || type == Usm.ResourceType.CANONICAL_LIBRARY)) {
  637. continue;
  638. }
  639. if(is_library && (type == Usm.ResourceType.LIBRARY_RESOURCE || type == Usm.ResourceType.CANONICAL_LIBRARY_RESOURCE)) {
  640. continue;
  641. }
  642. matching_types.add(type);
  643. traversal_counts.add(traversal_count);
  644. }
  645. }
  646. // If no matches found, default to "rootpath"
  647. if (matching_types.peek_count() == 0) {
  648. return Usm.ResourceType.ROOT_PATH;
  649. }
  650. // Find the type with the fewest directory traversals
  651. int min_traversals = traversal_counts[0];
  652. Usm.ResourceType best_type = matching_types[0];
  653. for (int i = 1; i < traversal_counts.peek_count(); i++) {
  654. if (traversal_counts[i] < min_traversals) {
  655. min_traversals = traversal_counts[i];
  656. best_type = matching_types[i];
  657. }
  658. }
  659. return best_type;
  660. }
  661. private static string get_relative_path_for_type(string path, string type) {
  662. // Remove leading slash if present
  663. string clean_path = path.has_prefix("/") ? path.substring(1) : path;
  664. // Create a temporary Paths object to get the default paths
  665. var default_paths = new Usm.Paths();
  666. try {
  667. // Convert string type to ResourceType enum
  668. var resource_type = Usm.ResourceType.from_string(type);
  669. var suggested_path = default_paths.get_suggested_base_path_for_type(resource_type);
  670. // Remove the destination prefix (usually "/") to get the relative path
  671. var relative_suggested_path = suggested_path.has_prefix("/") ?
  672. suggested_path.substring(1) : suggested_path;
  673. if (clean_path.has_prefix(relative_suggested_path)) {
  674. return clean_path.substring(relative_suggested_path.length);
  675. }
  676. } catch (Error e) {
  677. // Type not found, fall through to default case
  678. }
  679. // Default case - return the path as-is
  680. return clean_path;
  681. }
  682. private int test() {
  683. if(manifest.is_data_package) {
  684. printerr(@"Data packages define no test script.\n");
  685. return 248;
  686. }
  687. if(build_path == null) {
  688. var dir = File.new_build_filename("/tmp", Uuid.string_random());
  689. try {
  690. dir.make_directory();
  691. }
  692. catch(Error e) {
  693. printerr(@"Could not create temporary build directory, try specifying one instead: $(e.message)\n");
  694. return 255;
  695. }
  696. build_path = dir.get_path();
  697. } else {
  698. // Create build directory if it doesn't exist
  699. var build_dir = File.new_for_path(build_path);
  700. if(!build_dir.query_exists()) {
  701. printerr(@"Creating build directory: $build_path\n");
  702. try {
  703. build_dir.make_directory_with_parents();
  704. }
  705. catch(Error e) {
  706. printerr(@"Could not create build directory: $(e.message)\n");
  707. return 255;
  708. }
  709. }
  710. }
  711. var sane = phase_dependencies_satisfied(manifest.dependencies.manage, "management", resfinder) &
  712. phase_dependencies_satisfied(manifest.dependencies.build, "build", resfinder);
  713. if(!sane) {
  714. printerr("Could not test manifest, missing dependencies\n");
  715. return 252;
  716. }
  717. try {
  718. // First build the package
  719. var build_proc = manifest.run_build(build_path, paths, SubprocessFlags.INHERIT_FDS, frac => printerr(@"Building '$(manifest.name)': $((int)(frac*100))%\r"));
  720. build_proc.wait_check();
  721. printerr("\n");
  722. }
  723. catch(Error e) {
  724. printerr(@"\nError running build exec: $(e.message)\n");
  725. return 251;
  726. }
  727. if(manifest.executables.test == null) {
  728. printerr(@"Manifest does not reference a test script\n");
  729. return 248;
  730. }
  731. try {
  732. var test_proc = manifest.run_test(build_path, SubprocessFlags.INHERIT_FDS);
  733. test_proc.wait_check();
  734. }
  735. catch(Error e) {
  736. printerr(@"Error running test exec: $(e.message)\n");
  737. return 251;
  738. }
  739. return 0;
  740. }
  741. private int validate() {
  742. if(build_path == null) {
  743. var dir = File.new_build_filename("/tmp", Uuid.string_random());
  744. try {
  745. dir.make_directory();
  746. }
  747. catch(Error e) {
  748. printerr(@"Could not create temporary build directory, try specifying one instead: $(e.message)\n");
  749. return 255;
  750. }
  751. build_path = dir.get_path();
  752. } else {
  753. // Create build directory if it doesn't exist
  754. var build_dir = File.new_for_path(build_path);
  755. if(!build_dir.query_exists()) {
  756. printerr(@"Creating build directory: $build_path\n");
  757. try {
  758. build_dir.make_directory_with_parents();
  759. }
  760. catch(Error e) {
  761. printerr(@"Could not create build directory: $(e.message)\n");
  762. return 255;
  763. }
  764. }
  765. }
  766. var sane = phase_dependencies_satisfied(manifest.dependencies.manage, "management", resfinder) &
  767. phase_dependencies_satisfied(manifest.dependencies.build, "build", resfinder) &
  768. phase_dependencies_satisfied(manifest.dependencies.runtime, "runtime", destresfinder);
  769. if(!sane) {
  770. printerr("Could not validate manifest, missing dependencies\n");
  771. return 252;
  772. }
  773. // Check for license
  774. if(manifest.licences == null || manifest.licences.count() == 0) {
  775. printerr("Warning: No licence has been provided in the manifest.\n");
  776. }
  777. // Check for markdown
  778. if(manifest.markdown_path == null) {
  779. printerr("Warning: No markdown description has been provided in the manifest.\n");
  780. }
  781. // Check for url
  782. if(manifest.url == null) {
  783. printerr("Warning: No URL has been provided in the manifest.\n");
  784. }
  785. // Data packages define no build or install execs; their provides resolve
  786. // directly from the source tree, so validate them there and stop
  787. if(manifest.is_data_package) {
  788. return validate_data_package();
  789. }
  790. try {
  791. // Run build
  792. var build_proc = manifest.run_build(build_path, paths, SubprocessFlags.STDOUT_SILENCE, frac => printerr(@"Building '$(manifest.name)': $((int)(frac*100))%\r"));
  793. build_proc.wait_check();
  794. printerr("\n");
  795. }
  796. catch(Error e) {
  797. printerr(@"\nError running build exec: $(e.message)\n");
  798. return 251;
  799. }
  800. var install_dir = File.new_build_filename("/tmp", Uuid.string_random());
  801. if(manifest.executables.install != null) {
  802. try {
  803. install_dir.make_directory();
  804. }
  805. catch(Error e) {
  806. printerr(@"Could not create temporary install directory: $(e.message)\n");
  807. return 250;
  808. }
  809. try {
  810. manifest.run_install(build_path, install_dir.get_path(), paths, Usm.InstallType.FRESH, SubprocessFlags.STDOUT_SILENCE).wait_check();
  811. }
  812. catch(Error e) {
  813. printerr(@"Error running install exec: $(e.message)\n");
  814. return 249;
  815. }
  816. }
  817. else {
  818. printerr("Warning: No install script specified, cannot validate installation.\n");
  819. return 0;
  820. }
  821. // Validate each expected resource exists in its expected location
  822. var missing_resources = new Invercargill.DataStructures.Vector<Usm.ResourceRef>();
  823. foreach (var expected in manifest.provides) {
  824. bool found = false;
  825. string expected_path = "";
  826. try {
  827. switch(expected.value.path_base) {
  828. case Usm.ManifestFilePathBase.AS_EXPECTED:
  829. // For as-expected, check in install directory at suggested path
  830. var install_paths = paths.clone();
  831. install_paths.destination = install_dir.get_path();
  832. expected_path = install_paths.get_suggested_path_for_resource(expected.key);
  833. var file = File.new_for_path(expected_path);
  834. found = file.query_exists();
  835. break;
  836. case Usm.ManifestFilePathBase.BUILD:
  837. // For build:, check in build directory at specified path
  838. expected_path = Path.build_filename(build_path, expected.value.path ?? "");
  839. var file = File.new_for_path(expected_path);
  840. found = file.query_exists();
  841. break;
  842. case Usm.ManifestFilePathBase.SOURCE:
  843. // For source:, check in source directory at specified path
  844. expected_path = Path.build_filename(Environment.get_current_dir(), expected.value.path ?? "");
  845. var file = File.new_for_path(expected_path);
  846. found = file.query_exists();
  847. break;
  848. case Usm.ManifestFilePathBase.INSTALL:
  849. // For install:, check in install directory at specified path
  850. expected_path = Path.build_filename(install_dir.get_path(), expected.value.path ?? "");
  851. var file = File.new_for_path(expected_path);
  852. found = file.query_exists();
  853. break;
  854. default:
  855. assert_not_reached();
  856. }
  857. }
  858. catch(Error e) {
  859. printerr(@"Error validating resource $(expected.key.to_string()): $(e.message)\n");
  860. found = false;
  861. }
  862. if(!found) {
  863. missing_resources.add(expected.key);
  864. }
  865. }
  866. if(missing_resources.any()) {
  867. printerr("Error: Expected resources not found:\n");
  868. foreach (var resource in missing_resources) {
  869. var expected_resource = manifest.provides[resource];
  870. string location_desc = "";
  871. switch(expected_resource.path_base) {
  872. case Usm.ManifestFilePathBase.AS_EXPECTED:
  873. location_desc = "at expected install location";
  874. break;
  875. case Usm.ManifestFilePathBase.BUILD:
  876. location_desc = @"in build directory at \"$(expected_resource.path ?? "")\"";
  877. break;
  878. case Usm.ManifestFilePathBase.SOURCE:
  879. location_desc = @"in source directory at \"$(expected_resource.path ?? "")\"";
  880. break;
  881. case Usm.ManifestFilePathBase.INSTALL:
  882. location_desc = @"in install directory at \"$(expected_resource.path ?? "")\"";
  883. break;
  884. default:
  885. location_desc = "at unknown location";
  886. break;
  887. }
  888. printerr(@" - $(resource.to_string()) ($location_desc)\n");
  889. }
  890. return 248;
  891. }
  892. // Scan for unexpected resources in the install directory
  893. try {
  894. var installed_resources = autoprovides_scan_tree(install_dir.get_path(), install_dir.get_path());
  895. var expected_resources = manifest.provides;
  896. // Check for unexpected resources
  897. var unexpected_resources = new Invercargill.DataStructures.Vector<Usm.ResourceRef>();
  898. foreach (var installed in installed_resources) {
  899. bool found = false;
  900. foreach (var expected in expected_resources) {
  901. if(installed.key.to_string() == expected.key.to_string()) {
  902. found = true;
  903. break;
  904. }
  905. }
  906. if(!found) {
  907. unexpected_resources.add(installed.key);
  908. }
  909. }
  910. if(unexpected_resources.any()) {
  911. printerr("Warning: Unexpected resources found in install directory:\n");
  912. foreach (var resource in unexpected_resources) {
  913. printerr(@" - $(resource.to_string())\n");
  914. }
  915. }
  916. }
  917. catch(Error e) {
  918. printerr(@"Error validating installation result: $(e.message)\n");
  919. return 247;
  920. }
  921. printerr("Manifest validation completed successfully.\n");
  922. return 0;
  923. }
  924. /**
  925. * Validates a data package: every provide must exist in the source tree at
  926. * its declared path; no build or install staging is involved.
  927. */
  928. private int validate_data_package() {
  929. var missing_resources = new Invercargill.DataStructures.Vector<Usm.ResourceRef>();
  930. foreach (var expected in manifest.provides) {
  931. var expected_path = Path.build_filename(Environment.get_current_dir(), expected.value.path ?? "");
  932. if(!File.new_for_path(expected_path).query_exists()) {
  933. missing_resources.add(expected.key);
  934. }
  935. }
  936. if(missing_resources.any()) {
  937. printerr("Error: Expected resources not found in the source tree:\n");
  938. foreach (var resource in missing_resources) {
  939. printerr(@" - $(resource.to_string()) (in source directory at \"$(manifest.provides[resource].path ?? "")\")\n");
  940. }
  941. return 248;
  942. }
  943. printerr("Manifest validation completed successfully.\n");
  944. return 0;
  945. }