├── .envrc ├── .gitignore ├── .vscode └── settings.json ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix ├── rustfmt.toml └── src ├── deb ├── mod.rs ├── source.rs └── target.rs ├── lib.rs ├── lsb.rs ├── main.rs ├── pkg ├── mod.rs ├── source.rs └── target.rs ├── rpm ├── mod.rs ├── source.rs └── target.rs ├── tgz ├── mod.rs ├── source.rs └── target.rs └── util.rs /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *.deb 3 | *.rpm 4 | *.tgz 5 | .direnv/ 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": false 3 | } -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.21.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "adler2" 22 | version = "2.0.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" 25 | 26 | [[package]] 27 | name = "ar" 28 | version = "0.9.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "d67af77d68a931ecd5cbd8a3b5987d63a1d1d1278f7f6a60ae33db485cdebb69" 31 | 32 | [[package]] 33 | name = "backtrace" 34 | version = "0.3.71" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" 37 | dependencies = [ 38 | "addr2line", 39 | "cc", 40 | "cfg-if", 41 | "libc", 42 | "miniz_oxide 0.7.4", 43 | "object", 44 | "rustc-demangle", 45 | ] 46 | 47 | [[package]] 48 | name = "base64" 49 | version = "0.22.1" 50 | source = "registry+https://github.com/rust-lang/crates.io-index" 51 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 52 | 53 | [[package]] 54 | name = "bitflags" 55 | version = "2.6.0" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 58 | 59 | [[package]] 60 | name = "bpaf" 61 | version = "0.9.15" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "50fd5174866dc2fa2ddc96e8fb800852d37f064f32a45c7b7c2f8fa2c64c77fa" 64 | dependencies = [ 65 | "bpaf_derive", 66 | ] 67 | 68 | [[package]] 69 | name = "bpaf_derive" 70 | version = "0.5.13" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "cf95d9c7e6aba67f8fc07761091e93254677f4db9e27197adecebc7039a58722" 73 | dependencies = [ 74 | "proc-macro2", 75 | "quote", 76 | "syn", 77 | ] 78 | 79 | [[package]] 80 | name = "bumpalo" 81 | version = "3.16.0" 82 | source = "registry+https://github.com/rust-lang/crates.io-index" 83 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 84 | 85 | [[package]] 86 | name = "bzip2" 87 | version = "0.5.0" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "bafdbf26611df8c14810e268ddceda071c297570a5fb360ceddf617fe417ef58" 90 | dependencies = [ 91 | "bzip2-sys", 92 | "libc", 93 | ] 94 | 95 | [[package]] 96 | name = "bzip2-sys" 97 | version = "0.1.11+1.0.8" 98 | source = "registry+https://github.com/rust-lang/crates.io-index" 99 | checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" 100 | dependencies = [ 101 | "cc", 102 | "libc", 103 | "pkg-config", 104 | ] 105 | 106 | [[package]] 107 | name = "cc" 108 | version = "1.2.4" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf" 111 | dependencies = [ 112 | "jobserver", 113 | "libc", 114 | "shlex", 115 | ] 116 | 117 | [[package]] 118 | name = "cfg-if" 119 | version = "1.0.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 122 | 123 | [[package]] 124 | name = "cfg_aliases" 125 | version = "0.2.1" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 128 | 129 | [[package]] 130 | name = "color-eyre" 131 | version = "0.6.3" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "55146f5e46f237f7423d74111267d4597b59b0dad0ffaf7303bce9945d843ad5" 134 | dependencies = [ 135 | "backtrace", 136 | "color-spantrace", 137 | "eyre", 138 | "indenter", 139 | "once_cell", 140 | "owo-colors", 141 | "tracing-error", 142 | ] 143 | 144 | [[package]] 145 | name = "color-spantrace" 146 | version = "0.2.1" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" 149 | dependencies = [ 150 | "once_cell", 151 | "owo-colors", 152 | "tracing-core", 153 | "tracing-error", 154 | ] 155 | 156 | [[package]] 157 | name = "crc32fast" 158 | version = "1.4.2" 159 | source = "registry+https://github.com/rust-lang/crates.io-index" 160 | checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" 161 | dependencies = [ 162 | "cfg-if", 163 | ] 164 | 165 | [[package]] 166 | name = "deranged" 167 | version = "0.3.11" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 170 | dependencies = [ 171 | "powerfmt", 172 | ] 173 | 174 | [[package]] 175 | name = "either" 176 | version = "1.13.0" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" 179 | 180 | [[package]] 181 | name = "enum_dispatch" 182 | version = "0.3.13" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" 185 | dependencies = [ 186 | "once_cell", 187 | "proc-macro2", 188 | "quote", 189 | "syn", 190 | ] 191 | 192 | [[package]] 193 | name = "enumflags2" 194 | version = "0.7.10" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "d232db7f5956f3f14313dc2f87985c58bd2c695ce124c8cdd984e08e15ac133d" 197 | dependencies = [ 198 | "enumflags2_derive", 199 | ] 200 | 201 | [[package]] 202 | name = "enumflags2_derive" 203 | version = "0.7.10" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" 206 | dependencies = [ 207 | "proc-macro2", 208 | "quote", 209 | "syn", 210 | ] 211 | 212 | [[package]] 213 | name = "errno" 214 | version = "0.3.10" 215 | source = "registry+https://github.com/rust-lang/crates.io-index" 216 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 217 | dependencies = [ 218 | "libc", 219 | "windows-sys", 220 | ] 221 | 222 | [[package]] 223 | name = "eyre" 224 | version = "0.6.12" 225 | source = "registry+https://github.com/rust-lang/crates.io-index" 226 | checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" 227 | dependencies = [ 228 | "indenter", 229 | "once_cell", 230 | ] 231 | 232 | [[package]] 233 | name = "fastrand" 234 | version = "2.3.0" 235 | source = "registry+https://github.com/rust-lang/crates.io-index" 236 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 237 | 238 | [[package]] 239 | name = "filetime" 240 | version = "0.2.25" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586" 243 | dependencies = [ 244 | "cfg-if", 245 | "libc", 246 | "libredox", 247 | "windows-sys", 248 | ] 249 | 250 | [[package]] 251 | name = "flate2" 252 | version = "1.0.35" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" 255 | dependencies = [ 256 | "crc32fast", 257 | "miniz_oxide 0.8.2", 258 | ] 259 | 260 | [[package]] 261 | name = "fs_extra" 262 | version = "1.3.0" 263 | source = "registry+https://github.com/rust-lang/crates.io-index" 264 | checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" 265 | 266 | [[package]] 267 | name = "gimli" 268 | version = "0.28.1" 269 | source = "registry+https://github.com/rust-lang/crates.io-index" 270 | checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" 271 | 272 | [[package]] 273 | name = "glob" 274 | version = "0.3.1" 275 | source = "registry+https://github.com/rust-lang/crates.io-index" 276 | checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" 277 | 278 | [[package]] 279 | name = "home" 280 | version = "0.5.11" 281 | source = "registry+https://github.com/rust-lang/crates.io-index" 282 | checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" 283 | dependencies = [ 284 | "windows-sys", 285 | ] 286 | 287 | [[package]] 288 | name = "indenter" 289 | version = "0.3.3" 290 | source = "registry+https://github.com/rust-lang/crates.io-index" 291 | checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" 292 | 293 | [[package]] 294 | name = "itoa" 295 | version = "1.0.14" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" 298 | 299 | [[package]] 300 | name = "jobserver" 301 | version = "0.1.32" 302 | source = "registry+https://github.com/rust-lang/crates.io-index" 303 | checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" 304 | dependencies = [ 305 | "libc", 306 | ] 307 | 308 | [[package]] 309 | name = "js-sys" 310 | version = "0.3.73" 311 | source = "registry+https://github.com/rust-lang/crates.io-index" 312 | checksum = "fb15147158e79fd8b8afd0252522769c4f48725460b37338544d8379d94fc8f9" 313 | dependencies = [ 314 | "wasm-bindgen", 315 | ] 316 | 317 | [[package]] 318 | name = "lazy_static" 319 | version = "1.5.0" 320 | source = "registry+https://github.com/rust-lang/crates.io-index" 321 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 322 | 323 | [[package]] 324 | name = "libc" 325 | version = "0.2.169" 326 | source = "registry+https://github.com/rust-lang/crates.io-index" 327 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 328 | 329 | [[package]] 330 | name = "liblzma" 331 | version = "0.3.5" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | checksum = "603222e049bf0da71529325ada5d02dc3871cbd3679cf905429f7f0de93da87b" 334 | dependencies = [ 335 | "liblzma-sys", 336 | ] 337 | 338 | [[package]] 339 | name = "liblzma-sys" 340 | version = "0.3.11" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "41e2171ce6827cbab9bc97238a58361bf9a526080475f21dbc470e1842258b2d" 343 | dependencies = [ 344 | "cc", 345 | "libc", 346 | "pkg-config", 347 | ] 348 | 349 | [[package]] 350 | name = "libredox" 351 | version = "0.1.3" 352 | source = "registry+https://github.com/rust-lang/crates.io-index" 353 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 354 | dependencies = [ 355 | "bitflags", 356 | "libc", 357 | "redox_syscall", 358 | ] 359 | 360 | [[package]] 361 | name = "linux-raw-sys" 362 | version = "0.4.14" 363 | source = "registry+https://github.com/rust-lang/crates.io-index" 364 | checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" 365 | 366 | [[package]] 367 | name = "log" 368 | version = "0.4.22" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 371 | 372 | [[package]] 373 | name = "memchr" 374 | version = "2.7.4" 375 | source = "registry+https://github.com/rust-lang/crates.io-index" 376 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 377 | 378 | [[package]] 379 | name = "miniz_oxide" 380 | version = "0.7.4" 381 | source = "registry+https://github.com/rust-lang/crates.io-index" 382 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 383 | dependencies = [ 384 | "adler", 385 | ] 386 | 387 | [[package]] 388 | name = "miniz_oxide" 389 | version = "0.8.2" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394" 392 | dependencies = [ 393 | "adler2", 394 | ] 395 | 396 | [[package]] 397 | name = "nix" 398 | version = "0.29.0" 399 | source = "registry+https://github.com/rust-lang/crates.io-index" 400 | checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" 401 | dependencies = [ 402 | "bitflags", 403 | "cfg-if", 404 | "cfg_aliases", 405 | "libc", 406 | ] 407 | 408 | [[package]] 409 | name = "num-conv" 410 | version = "0.1.0" 411 | source = "registry+https://github.com/rust-lang/crates.io-index" 412 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 413 | 414 | [[package]] 415 | name = "num_threads" 416 | version = "0.1.7" 417 | source = "registry+https://github.com/rust-lang/crates.io-index" 418 | checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" 419 | dependencies = [ 420 | "libc", 421 | ] 422 | 423 | [[package]] 424 | name = "object" 425 | version = "0.32.2" 426 | source = "registry+https://github.com/rust-lang/crates.io-index" 427 | checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" 428 | dependencies = [ 429 | "memchr", 430 | ] 431 | 432 | [[package]] 433 | name = "once_cell" 434 | version = "1.20.2" 435 | source = "registry+https://github.com/rust-lang/crates.io-index" 436 | checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" 437 | 438 | [[package]] 439 | name = "owo-colors" 440 | version = "3.5.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" 443 | 444 | [[package]] 445 | name = "pin-project-lite" 446 | version = "0.2.15" 447 | source = "registry+https://github.com/rust-lang/crates.io-index" 448 | checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff" 449 | 450 | [[package]] 451 | name = "pkg-config" 452 | version = "0.3.31" 453 | source = "registry+https://github.com/rust-lang/crates.io-index" 454 | checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" 455 | 456 | [[package]] 457 | name = "powerfmt" 458 | version = "0.2.0" 459 | source = "registry+https://github.com/rust-lang/crates.io-index" 460 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 461 | 462 | [[package]] 463 | name = "proc-macro2" 464 | version = "1.0.92" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 467 | dependencies = [ 468 | "unicode-ident", 469 | ] 470 | 471 | [[package]] 472 | name = "quote" 473 | version = "1.0.37" 474 | source = "registry+https://github.com/rust-lang/crates.io-index" 475 | checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" 476 | dependencies = [ 477 | "proc-macro2", 478 | ] 479 | 480 | [[package]] 481 | name = "redox_syscall" 482 | version = "0.5.8" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" 485 | dependencies = [ 486 | "bitflags", 487 | ] 488 | 489 | [[package]] 490 | name = "rustc-demangle" 491 | version = "0.1.24" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 494 | 495 | [[package]] 496 | name = "rustix" 497 | version = "0.38.42" 498 | source = "registry+https://github.com/rust-lang/crates.io-index" 499 | checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" 500 | dependencies = [ 501 | "bitflags", 502 | "errno", 503 | "libc", 504 | "linux-raw-sys", 505 | "windows-sys", 506 | ] 507 | 508 | [[package]] 509 | name = "serde" 510 | version = "1.0.216" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e" 513 | dependencies = [ 514 | "serde_derive", 515 | ] 516 | 517 | [[package]] 518 | name = "serde_derive" 519 | version = "1.0.216" 520 | source = "registry+https://github.com/rust-lang/crates.io-index" 521 | checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e" 522 | dependencies = [ 523 | "proc-macro2", 524 | "quote", 525 | "syn", 526 | ] 527 | 528 | [[package]] 529 | name = "sharded-slab" 530 | version = "0.1.7" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 533 | dependencies = [ 534 | "lazy_static", 535 | ] 536 | 537 | [[package]] 538 | name = "shlex" 539 | version = "1.3.0" 540 | source = "registry+https://github.com/rust-lang/crates.io-index" 541 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 542 | 543 | [[package]] 544 | name = "simple-eyre" 545 | version = "0.3.1" 546 | source = "registry+https://github.com/rust-lang/crates.io-index" 547 | checksum = "1b561532e8ffe7ecf09108c4f662896a9ec3eac4999eba84015ec3dcb8cc630a" 548 | dependencies = [ 549 | "eyre", 550 | "indenter", 551 | ] 552 | 553 | [[package]] 554 | name = "snailquote" 555 | version = "0.3.1" 556 | source = "registry+https://github.com/rust-lang/crates.io-index" 557 | checksum = "ec62a949bda7f15800481a711909f946e1204f2460f89210eaf7f57730f88f86" 558 | dependencies = [ 559 | "thiserror", 560 | "unicode_categories", 561 | ] 562 | 563 | [[package]] 564 | name = "subprocess" 565 | version = "0.2.9" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "0c2e86926081dda636c546d8c5e641661049d7562a68f5488be4a1f7f66f6086" 568 | dependencies = [ 569 | "libc", 570 | "winapi", 571 | ] 572 | 573 | [[package]] 574 | name = "syn" 575 | version = "2.0.90" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "919d3b74a5dd0ccd15aeb8f93e7006bd9e14c295087c9896a110f490752bcf31" 578 | dependencies = [ 579 | "proc-macro2", 580 | "quote", 581 | "unicode-ident", 582 | ] 583 | 584 | [[package]] 585 | name = "tar" 586 | version = "0.4.43" 587 | source = "registry+https://github.com/rust-lang/crates.io-index" 588 | checksum = "c65998313f8e17d0d553d28f91a0df93e4dbbbf770279c7bc21ca0f09ea1a1f6" 589 | dependencies = [ 590 | "filetime", 591 | "libc", 592 | "xattr", 593 | ] 594 | 595 | [[package]] 596 | name = "tempfile" 597 | version = "3.14.0" 598 | source = "registry+https://github.com/rust-lang/crates.io-index" 599 | checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c" 600 | dependencies = [ 601 | "cfg-if", 602 | "fastrand", 603 | "once_cell", 604 | "rustix", 605 | "windows-sys", 606 | ] 607 | 608 | [[package]] 609 | name = "thiserror" 610 | version = "1.0.69" 611 | source = "registry+https://github.com/rust-lang/crates.io-index" 612 | checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" 613 | dependencies = [ 614 | "thiserror-impl", 615 | ] 616 | 617 | [[package]] 618 | name = "thiserror-impl" 619 | version = "1.0.69" 620 | source = "registry+https://github.com/rust-lang/crates.io-index" 621 | checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" 622 | dependencies = [ 623 | "proc-macro2", 624 | "quote", 625 | "syn", 626 | ] 627 | 628 | [[package]] 629 | name = "thread_local" 630 | version = "1.1.8" 631 | source = "registry+https://github.com/rust-lang/crates.io-index" 632 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 633 | dependencies = [ 634 | "cfg-if", 635 | "once_cell", 636 | ] 637 | 638 | [[package]] 639 | name = "time" 640 | version = "0.3.37" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" 643 | dependencies = [ 644 | "deranged", 645 | "itoa", 646 | "libc", 647 | "num-conv", 648 | "num_threads", 649 | "powerfmt", 650 | "serde", 651 | "time-core", 652 | "time-macros", 653 | ] 654 | 655 | [[package]] 656 | name = "time-core" 657 | version = "0.1.2" 658 | source = "registry+https://github.com/rust-lang/crates.io-index" 659 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 660 | 661 | [[package]] 662 | name = "time-macros" 663 | version = "0.2.19" 664 | source = "registry+https://github.com/rust-lang/crates.io-index" 665 | checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" 666 | dependencies = [ 667 | "num-conv", 668 | "time-core", 669 | ] 670 | 671 | [[package]] 672 | name = "tracing" 673 | version = "0.1.41" 674 | source = "registry+https://github.com/rust-lang/crates.io-index" 675 | checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" 676 | dependencies = [ 677 | "pin-project-lite", 678 | "tracing-core", 679 | ] 680 | 681 | [[package]] 682 | name = "tracing-core" 683 | version = "0.1.33" 684 | source = "registry+https://github.com/rust-lang/crates.io-index" 685 | checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" 686 | dependencies = [ 687 | "once_cell", 688 | "valuable", 689 | ] 690 | 691 | [[package]] 692 | name = "tracing-error" 693 | version = "0.2.1" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" 696 | dependencies = [ 697 | "tracing", 698 | "tracing-subscriber", 699 | ] 700 | 701 | [[package]] 702 | name = "tracing-subscriber" 703 | version = "0.3.19" 704 | source = "registry+https://github.com/rust-lang/crates.io-index" 705 | checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" 706 | dependencies = [ 707 | "sharded-slab", 708 | "thread_local", 709 | "tracing-core", 710 | ] 711 | 712 | [[package]] 713 | name = "unicode-ident" 714 | version = "1.0.14" 715 | source = "registry+https://github.com/rust-lang/crates.io-index" 716 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 717 | 718 | [[package]] 719 | name = "unicode_categories" 720 | version = "0.1.1" 721 | source = "registry+https://github.com/rust-lang/crates.io-index" 722 | checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" 723 | 724 | [[package]] 725 | name = "valuable" 726 | version = "0.1.0" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 729 | 730 | [[package]] 731 | name = "wasite" 732 | version = "0.1.0" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 735 | 736 | [[package]] 737 | name = "wasm-bindgen" 738 | version = "0.2.96" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "21d3b25c3ea1126a2ad5f4f9068483c2af1e64168f847abe863a526b8dbfe00b" 741 | dependencies = [ 742 | "cfg-if", 743 | "once_cell", 744 | "wasm-bindgen-macro", 745 | ] 746 | 747 | [[package]] 748 | name = "wasm-bindgen-backend" 749 | version = "0.2.96" 750 | source = "registry+https://github.com/rust-lang/crates.io-index" 751 | checksum = "52857d4c32e496dc6537646b5b117081e71fd2ff06de792e3577a150627db283" 752 | dependencies = [ 753 | "bumpalo", 754 | "log", 755 | "once_cell", 756 | "proc-macro2", 757 | "quote", 758 | "syn", 759 | "wasm-bindgen-shared", 760 | ] 761 | 762 | [[package]] 763 | name = "wasm-bindgen-macro" 764 | version = "0.2.96" 765 | source = "registry+https://github.com/rust-lang/crates.io-index" 766 | checksum = "920b0ffe069571ebbfc9ddc0b36ba305ef65577c94b06262ed793716a1afd981" 767 | dependencies = [ 768 | "quote", 769 | "wasm-bindgen-macro-support", 770 | ] 771 | 772 | [[package]] 773 | name = "wasm-bindgen-macro-support" 774 | version = "0.2.96" 775 | source = "registry+https://github.com/rust-lang/crates.io-index" 776 | checksum = "bf59002391099644be3524e23b781fa43d2be0c5aa0719a18c0731b9d195cab6" 777 | dependencies = [ 778 | "proc-macro2", 779 | "quote", 780 | "syn", 781 | "wasm-bindgen-backend", 782 | "wasm-bindgen-shared", 783 | ] 784 | 785 | [[package]] 786 | name = "wasm-bindgen-shared" 787 | version = "0.2.96" 788 | source = "registry+https://github.com/rust-lang/crates.io-index" 789 | checksum = "e5047c5392700766601942795a436d7d2599af60dcc3cc1248c9120bfb0827b0" 790 | 791 | [[package]] 792 | name = "web-sys" 793 | version = "0.3.72" 794 | source = "registry+https://github.com/rust-lang/crates.io-index" 795 | checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" 796 | dependencies = [ 797 | "js-sys", 798 | "wasm-bindgen", 799 | ] 800 | 801 | [[package]] 802 | name = "which" 803 | version = "7.0.0" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "c9cad3279ade7346b96e38731a641d7343dd6a53d55083dd54eadfa5a1b38c6b" 806 | dependencies = [ 807 | "either", 808 | "home", 809 | "rustix", 810 | "winsafe", 811 | ] 812 | 813 | [[package]] 814 | name = "whoami" 815 | version = "1.5.2" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" 818 | dependencies = [ 819 | "redox_syscall", 820 | "wasite", 821 | "web-sys", 822 | ] 823 | 824 | [[package]] 825 | name = "winapi" 826 | version = "0.3.9" 827 | source = "registry+https://github.com/rust-lang/crates.io-index" 828 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 829 | dependencies = [ 830 | "winapi-i686-pc-windows-gnu", 831 | "winapi-x86_64-pc-windows-gnu", 832 | ] 833 | 834 | [[package]] 835 | name = "winapi-i686-pc-windows-gnu" 836 | version = "0.4.0" 837 | source = "registry+https://github.com/rust-lang/crates.io-index" 838 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 839 | 840 | [[package]] 841 | name = "winapi-x86_64-pc-windows-gnu" 842 | version = "0.4.0" 843 | source = "registry+https://github.com/rust-lang/crates.io-index" 844 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 845 | 846 | [[package]] 847 | name = "windows-sys" 848 | version = "0.59.0" 849 | source = "registry+https://github.com/rust-lang/crates.io-index" 850 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 851 | dependencies = [ 852 | "windows-targets", 853 | ] 854 | 855 | [[package]] 856 | name = "windows-targets" 857 | version = "0.52.6" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 860 | dependencies = [ 861 | "windows_aarch64_gnullvm", 862 | "windows_aarch64_msvc", 863 | "windows_i686_gnu", 864 | "windows_i686_gnullvm", 865 | "windows_i686_msvc", 866 | "windows_x86_64_gnu", 867 | "windows_x86_64_gnullvm", 868 | "windows_x86_64_msvc", 869 | ] 870 | 871 | [[package]] 872 | name = "windows_aarch64_gnullvm" 873 | version = "0.52.6" 874 | source = "registry+https://github.com/rust-lang/crates.io-index" 875 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 876 | 877 | [[package]] 878 | name = "windows_aarch64_msvc" 879 | version = "0.52.6" 880 | source = "registry+https://github.com/rust-lang/crates.io-index" 881 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 882 | 883 | [[package]] 884 | name = "windows_i686_gnu" 885 | version = "0.52.6" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 888 | 889 | [[package]] 890 | name = "windows_i686_gnullvm" 891 | version = "0.52.6" 892 | source = "registry+https://github.com/rust-lang/crates.io-index" 893 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 894 | 895 | [[package]] 896 | name = "windows_i686_msvc" 897 | version = "0.52.6" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 900 | 901 | [[package]] 902 | name = "windows_x86_64_gnu" 903 | version = "0.52.6" 904 | source = "registry+https://github.com/rust-lang/crates.io-index" 905 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 906 | 907 | [[package]] 908 | name = "windows_x86_64_gnullvm" 909 | version = "0.52.6" 910 | source = "registry+https://github.com/rust-lang/crates.io-index" 911 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 912 | 913 | [[package]] 914 | name = "windows_x86_64_msvc" 915 | version = "0.52.6" 916 | source = "registry+https://github.com/rust-lang/crates.io-index" 917 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 918 | 919 | [[package]] 920 | name = "winsafe" 921 | version = "0.0.19" 922 | source = "registry+https://github.com/rust-lang/crates.io-index" 923 | checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" 924 | 925 | [[package]] 926 | name = "xattr" 927 | version = "1.3.1" 928 | source = "registry+https://github.com/rust-lang/crates.io-index" 929 | checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" 930 | dependencies = [ 931 | "libc", 932 | "linux-raw-sys", 933 | "rustix", 934 | ] 935 | 936 | [[package]] 937 | name = "xenomorph" 938 | version = "0.1.0" 939 | dependencies = [ 940 | "ar", 941 | "base64", 942 | "bpaf", 943 | "bzip2", 944 | "color-eyre", 945 | "enum_dispatch", 946 | "enumflags2", 947 | "eyre", 948 | "flate2", 949 | "fs_extra", 950 | "glob", 951 | "liblzma", 952 | "nix", 953 | "simple-eyre", 954 | "snailquote", 955 | "subprocess", 956 | "tar", 957 | "tempfile", 958 | "time", 959 | "which", 960 | "whoami", 961 | ] 962 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "xenomorph" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | ar = "0.9" 10 | base64 = "0.22" 11 | bpaf = { version = "0.9", features = ["derive"] } 12 | bzip2 = "0.5" 13 | color-eyre = "0.6" 14 | enum_dispatch = "0.3" 15 | enumflags2 = "0.7" 16 | eyre = "0.6" 17 | flate2 = "1.0" 18 | fs_extra = "1.3" 19 | glob = "0.3" 20 | nix = { version = "0.29", default-features = false, features = ["user", "fs"] } 21 | simple-eyre = "0.3" 22 | snailquote = "0.3" 23 | subprocess = "0.2" 24 | tar = "0.4" 25 | time = { version = "0.3", features = ["local-offset", "formatting"] } 26 | which = "7.0" 27 | whoami = "1.5" 28 | liblzma = "0.3" 29 | tempfile = "3.14.0" 30 | 31 | [profile.release] 32 | strip = true 33 | opt-level = "z" 34 | lto = "thin" 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 675 Mass Ave, Cambridge, MA 02139, USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 19yy 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) 19yy name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Library General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `xenomorph` — Shapeshift between package formats 2 | 3 | A tool that converts software packages to work from one package manager to the next, 4 | originating as a rewrite of the classic [`alien`](https://sourceforge.net/projects/alien-pkg-convert/) script. 5 | 6 | Currently, the tool supports converting between: 7 | - `.deb` packages — used by `dpkg`, prevalent in Linux distributions (distros) 8 | derived from Debian and Ubuntu; 9 | - `.rpm` packages — used by `rpm`, found in Red Hat-derived distros such as RHEL, 10 | CentOS, openSUSE, Fedora and more; 11 | - LSB packages — used by Linux Standard Base and are basicaly `.rpm` packages 12 | - `.tgz` packages — used by Slackware Linux 13 | - `.pkg` packages — used by Solaris 14 | 15 | ## How is `xenomorph` different from `alien`? 16 | 17 | `xenomorph` is written in Rust and therefore does not rely on a Perl interpreter in order to function, 18 | and can attain native level speeds comparable to tools written in other native languages. 19 | It also has far more robust error handling and avoids many silent failure states that `alien` can face, 20 | as well as a much more extensible and well-documented framework for other packaging formats. 21 | 22 | `xenomorph`, however, *does not* aim to be a complete drop-in replacement of `alien`. Notably, 23 | support for `.slp` packages — once used by Stampede Linux — has been removed, since Stampede Linux 24 | had been effectively dead for over twenty years with little surviving records of its package format. 25 | `xenomorph`'s CLI interface, while currently compatible with `alien`, may change in the future, 26 | and so will the packages it generates. 27 | 28 | ## Known Issues 29 | 30 | - Names need to be mapped from `.rpm` to `.deb` - in particular, `.deb` package 31 | names cannot contain uppercase letters, whereas `.rpm` packages have no such restriction. 32 | 33 | - Currently dependencies from `.deb` files are not processed, which means `.rpm` 34 | packages converted from `.deb` packages may not install correctly. 35 | 36 | As well as issues that have been carried over from `alien`: 37 | 38 | - Relocatable conffiles, partially relocatable packages, and multipart packages are not yet supported 39 | 40 | - RPM ghost files are not yet supported 41 | 42 | - In Slackware packages, descriptions in install/slack-desc may be ignored 43 | 44 | ## License 45 | 46 | `xenomorph` is licensed under [the GNU General Public License, version 2](LICENSE), or (at your option) any later version. 47 | 48 | © 2023–2024 Leah Amelia "pluie" Chen 49 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1734600368, 6 | "narHash": "sha256-nbG9TijTMcfr+au7ZVbKpAhMJzzE2nQBYmRvSdXUD8g=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "b47fd6fa00c6afca88b8ee46cfdb00e104f50bca", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "release-24.11", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/release-24.11"; 3 | 4 | outputs = 5 | { nixpkgs, ... }: 6 | let 7 | forAllSystems = 8 | f: nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed (s: f nixpkgs.legacyPackages.${s}); 9 | in 10 | { 11 | packages = forAllSystems (pkgs: rec { 12 | default = xenomorph; 13 | 14 | xenomorph = pkgs.buildRustPackage { 15 | pname = "xenomorph"; 16 | version = "0.1.0"; 17 | }; 18 | }); 19 | 20 | devShells = forAllSystems (pkgs: { 21 | default = pkgs.mkShell { 22 | packages = with pkgs; [ 23 | cargo 24 | rustc 25 | rustfmt 26 | 27 | dpkg 28 | rpm 29 | ]; 30 | }; 31 | }); 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | use_field_init_shorthand = true 3 | hard_tabs = true 4 | newline_style = "Unix" -------------------------------------------------------------------------------- /src/deb/mod.rs: -------------------------------------------------------------------------------- 1 | pub use source::DebSource; 2 | pub use target::DebTarget; 3 | 4 | use crate::util::{ExecExt, Verbosity}; 5 | use eyre::Result; 6 | use std::path::Path; 7 | use subprocess::Exec; 8 | 9 | pub mod source; 10 | pub mod target; 11 | 12 | pub fn install(deb: &Path) -> Result<()> { 13 | Exec::cmd("dpkg") 14 | .args(&["--no-force-overwrite", "-i"]) 15 | .arg(deb) 16 | .log_and_spawn(Verbosity::VeryVerbose) 17 | } 18 | 19 | fn set_version_and_release(info: &mut super::PackageInfo, version: &str) { 20 | let (version, release) = if let Some((version, release)) = version.split_once('-') { 21 | (version, release) 22 | } else { 23 | (version, "1") 24 | }; 25 | 26 | // Ignore epochs. 27 | let version = version 28 | .split_once(':') 29 | .map_or(version, |(_epoch, version)| version); 30 | 31 | info.version = version.to_owned(); 32 | info.release = release.to_owned(); 33 | } 34 | 35 | #[cfg(test)] 36 | mod tests { 37 | #[test] 38 | fn test_set_version_and_release() { 39 | let mut info = crate::PackageInfo::default(); 40 | 41 | super::set_version_and_release(&mut info, "1.0.0"); 42 | assert_eq!(info.version, "1.0.0"); 43 | assert_eq!(info.release, "1"); 44 | 45 | // With revision 46 | super::set_version_and_release(&mut info, "1.0.0-2"); 47 | assert_eq!(info.version, "1.0.0"); 48 | assert_eq!(info.release, "2"); 49 | 50 | // With epoch 51 | super::set_version_and_release(&mut info, "3:1.0.0-2"); 52 | assert_eq!(info.version, "1.0.0"); 53 | assert_eq!(info.release, "2"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/deb/source.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fmt::Debug, 4 | fs::File, 5 | io::{Cursor, Read, Seek}, 6 | path::{Path, PathBuf}, 7 | }; 8 | 9 | use bzip2::read::BzDecoder; 10 | use flate2::read::GzDecoder; 11 | use liblzma::read::XzDecoder; 12 | 13 | use eyre::{bail, Result}; 14 | use subprocess::{Exec, NullFile}; 15 | 16 | use crate::{ 17 | util::{make_unpack_work_dir, ExecExt, Verbosity}, 18 | Args, Format, PackageInfo, Script, SourcePackage, 19 | }; 20 | 21 | pub struct DebSource { 22 | info: PackageInfo, 23 | data: Data, 24 | } 25 | impl DebSource { 26 | #[must_use] 27 | pub fn check_file(file: &Path) -> bool { 28 | file.extension() 29 | .map_or(false, |o| o.eq_ignore_ascii_case("deb")) 30 | } 31 | 32 | pub fn new(file: PathBuf, args: &Args) -> Result { 33 | let mut info = PackageInfo { 34 | file, 35 | distribution: "Debian".into(), 36 | original_format: Format::Deb, 37 | ..Default::default() 38 | }; 39 | 40 | let DebArchive { 41 | mut data, 42 | mut control_files, 43 | } = DebArchive::extract(&info.file)?; 44 | 45 | let Some(control) = control_files.remove("control") else { 46 | bail!("Control file not found!"); 47 | }; 48 | read_control(&mut info, &control); 49 | 50 | info.copyright = format!("see /usr/share/doc/{}/copyright", info.name); 51 | if info.group.is_empty() { 52 | info.group.push_str("unknown"); 53 | } 54 | info.binary_info = control; 55 | 56 | if let Some(conffiles) = control_files.remove("conffiles") { 57 | info.conffiles.extend(conffiles.lines().map(PathBuf::from)); 58 | }; 59 | 60 | info.files.extend(data.files()?); 61 | 62 | info.scripts = control_files 63 | .into_iter() 64 | .filter_map(|(k, v)| Script::from_deb_name(k).map(|k| (k, v))) 65 | .collect(); 66 | 67 | if let Some(arch) = &args.target { 68 | info.arch.clone_from(arch); 69 | } 70 | 71 | Ok(Self { info, data }) 72 | } 73 | } 74 | impl SourcePackage for DebSource { 75 | fn info(&self) -> &PackageInfo { 76 | &self.info 77 | } 78 | fn info_mut(&mut self) -> &mut PackageInfo { 79 | &mut self.info 80 | } 81 | fn into_info(self) -> PackageInfo { 82 | self.info 83 | } 84 | fn unpack(&mut self) -> Result { 85 | let work_dir = make_unpack_work_dir(&self.info)?; 86 | self.data.unpack(&work_dir)?; 87 | Ok(work_dir) 88 | } 89 | } 90 | impl Debug for DebSource { 91 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 92 | f.debug_struct("DebSource") 93 | .field("info", &self.info) 94 | .finish() 95 | } 96 | } 97 | 98 | //= Utilties 99 | struct Data(tar::Archive>>); 100 | 101 | impl Data { 102 | // In the tar file, the files are all prefixed with "./", but we want them 103 | // to be just "/". So, we gotta do this! 104 | fn files(&mut self) -> Result + '_> { 105 | let entries = self.0.entries()?; 106 | 107 | Ok(entries.filter_map(|entry| { 108 | let entry = entry.ok()?; 109 | let path = entry.path().ok()?; 110 | Some(Path::new("/").join(path.strip_prefix(".").unwrap_or(&path))) 111 | })) 112 | } 113 | 114 | fn unpack(&mut self, dst: &Path) -> std::io::Result<()> { 115 | // to unpack tar files, apparently we have to rewind first... 116 | let mut inner = 117 | std::mem::replace(&mut self.0, tar::Archive::new(Cursor::new(vec![]))).into_inner(); 118 | inner.rewind()?; 119 | tar::Archive::new(inner).unpack(dst) 120 | } 121 | } 122 | 123 | struct DebArchive { 124 | data: Data, 125 | control_files: HashMap<&'static str, String>, 126 | } 127 | 128 | impl DebArchive { 129 | const CONTROL_FILES: &[&'static str] = &[ 130 | "control", 131 | "conffiles", 132 | "postinst", 133 | "postrm", 134 | "preinst", 135 | "prerm", 136 | ]; 137 | 138 | fn extract(deb_file: &Path) -> Result { 139 | if let Ok(dpkg_deb) = which::which("dpkg-deb") { 140 | Self::extract_with_dpkg_deb(&dpkg_deb, deb_file) 141 | } else { 142 | Self::extract_manually(File::open(deb_file)?) 143 | } 144 | } 145 | 146 | fn extract_with_dpkg_deb(dpkg_deb: &Path, deb_file: &Path) -> Result { 147 | // HACK(pluie): You can't query subprocess's stdout settings once set, 148 | // and we really don't want dpkg-deb spilling bytes from tar files 149 | // into readable stdout, so we want to limit the output to commands only, 150 | // even in very verbose mode. 151 | let mut verbosity = Verbosity::get(); 152 | if verbosity == Verbosity::VeryVerbose { 153 | verbosity = Verbosity::Verbose; 154 | } 155 | 156 | let data = Exec::cmd(dpkg_deb) 157 | .arg("--fsys-tarfile") 158 | .arg(deb_file) 159 | .log_and_output(verbosity)? 160 | .stdout; 161 | 162 | let mut control_files = HashMap::new(); 163 | 164 | for file in Self::CONTROL_FILES { 165 | let out = Exec::cmd(dpkg_deb) 166 | .arg("--info") 167 | .arg(deb_file) 168 | .arg(file) 169 | .stderr(NullFile) 170 | .log_and_output_without_checking(None)?; 171 | 172 | if out.success() { 173 | control_files.insert(*file, out.stdout_str()); 174 | } 175 | } 176 | 177 | Ok(Self { 178 | data: Data(tar::Archive::new(Cursor::new(data))), 179 | control_files, 180 | }) 181 | } 182 | 183 | fn extract_manually(source: R) -> Result { 184 | let mut ar = ar::Archive::new(source); 185 | let mut control = None; 186 | let mut data = None; 187 | 188 | while let Some(entry) = ar.next_entry() { 189 | let mut entry = entry?; 190 | 191 | if control.is_none() { 192 | control = Self::try_read_tar(&mut entry, "control.tar")?; 193 | } 194 | if data.is_none() { 195 | data = Self::try_read_tar(&mut entry, "data.tar")?; 196 | } 197 | } 198 | 199 | let Some(mut control) = control else { 200 | bail!("Malformed .deb archive - control.tar not found!") 201 | }; 202 | let Some(data) = data else { 203 | bail!("Malformed .deb archive - data.tar not found!") 204 | }; 205 | 206 | // Go through all entries, and if an entry has a path, and that path's 207 | // file name matches a control file we're looking for, then add that to the map. 208 | let mut control_files = HashMap::new(); 209 | 210 | for entry in control.entries()? { 211 | let mut entry = entry?; 212 | 213 | let Ok(path) = entry.path() else { 214 | continue; 215 | }; 216 | let Some(name) = path.file_name() else { 217 | continue; 218 | }; 219 | 220 | if let Some(cf) = Self::CONTROL_FILES.iter().find(|&&s| s == name) { 221 | let mut data = String::new(); 222 | entry.read_to_string(&mut data)?; 223 | control_files.insert(*cf, data); 224 | } 225 | } 226 | 227 | Ok(Self { 228 | data: Data(data), 229 | control_files, 230 | }) 231 | } 232 | 233 | fn try_read_tar( 234 | entry: &mut ar::Entry<'_, R>, 235 | file: &str, 236 | ) -> Result>>>> { 237 | let id = entry.header().identifier(); 238 | if let Some(ext) = id.strip_prefix(file.as_bytes()) { 239 | let mut tar = vec![]; 240 | match ext { 241 | b".gz" => GzDecoder::new(entry).read_to_end(&mut tar)?, 242 | b".bz2" => BzDecoder::new(entry).read_to_end(&mut tar)?, 243 | b".xz" | b".lzma" => XzDecoder::new(entry).read_to_end(&mut tar)?, 244 | // it's already a tarball 245 | b"" => entry.read_to_end(&mut tar)?, 246 | _ => bail!( 247 | "{file} is compressed with unknown compression algorithm ({:?})!", 248 | std::str::from_utf8(ext) 249 | ), 250 | }; 251 | let tar = tar::Archive::new(Cursor::new(tar)); 252 | Ok(Some(tar)) 253 | } else { 254 | Ok(None) 255 | } 256 | } 257 | } 258 | 259 | fn read_control(info: &mut PackageInfo, control: &str) { 260 | let mut field = String::new(); 261 | 262 | for c in control.lines() { 263 | if c.starts_with(' ') && field == "description" { 264 | // Handle extended description 265 | let c = c.trim_start(); 266 | if c != "." { 267 | info.description.push_str(c); 268 | } 269 | info.description.push('\n'); 270 | } else if let Some((f, value)) = c.split_once(':') { 271 | let value = value.trim().to_owned(); 272 | field = f.to_ascii_lowercase(); 273 | 274 | match field.as_str() { 275 | "package" => info.name = value, 276 | "version" => super::set_version_and_release(info, &value), 277 | "architecture" => info.arch = value, 278 | "maintainer" => info.maintainer = value, 279 | "section" => info.group = value, 280 | "description" => info.summary = value, 281 | // TODO: think more about handling dependencies 282 | // "depends" => info.dependencies = value.split(", ").map(|s| s.to_owned()).collect(), 283 | _ => { /* ignore */ } 284 | } 285 | } 286 | } 287 | } 288 | 289 | #[cfg(test)] 290 | mod tests { 291 | use eyre::Result; 292 | 293 | fn test_deb_archive() -> Result> { 294 | let control = b" 295 | Package: xenomorph 296 | Version: 0.1.0-2 297 | Architecture: amd64 298 | Maintainer: Leah Amelia Chen 299 | Section: Utilities 300 | Description: 301 | Shapeshift between package formats 302 | "; 303 | 304 | let mut control_files = tar::Builder::new(vec![]); 305 | let mut header = tar::Header::new_gnu(); 306 | header.set_size(control.len() as u64); 307 | header.set_cksum(); 308 | control_files.append_data(&mut header, "control", &control[..])?; 309 | let control_tar = control_files.into_inner()?; 310 | 311 | let data_files = tar::Builder::new(vec![]); 312 | let data_tar = data_files.into_inner()?; 313 | 314 | let mut deb_archive = ar::Builder::new(vec![]); 315 | deb_archive.append( 316 | &ar::Header::new(b"control.tar".into(), control_tar.len() as u64), 317 | control_tar.as_slice(), 318 | )?; 319 | deb_archive.append( 320 | &ar::Header::new(b"data.tar".into(), data_tar.len() as u64), 321 | data_tar.as_slice(), 322 | )?; 323 | 324 | Ok(deb_archive.into_inner()?) 325 | } 326 | 327 | #[test] 328 | fn test_deb_archive_extract_manually() -> Result<()> { 329 | let deb_archive = super::DebArchive::extract_manually(test_deb_archive()?.as_slice())?; 330 | let control = deb_archive.control_files.get("control").unwrap(); 331 | let mut info = crate::PackageInfo::default(); 332 | super::read_control(&mut info, &control); 333 | 334 | assert_eq!(info.name, "xenomorph"); 335 | assert_eq!(info.version, "0.1.0"); 336 | assert_eq!(info.release, "2"); 337 | assert_eq!(info.arch, "amd64"); 338 | assert_eq!(info.maintainer, "Leah Amelia Chen "); 339 | assert_eq!(info.group, "Utilities"); 340 | assert_eq!(info.description, "Shapeshift between package formats\n"); 341 | 342 | Ok(()) 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/deb/target.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fmt::Write as _, 4 | fs::File, 5 | io::{BufRead, BufReader, Read, Write}, 6 | os::unix::prelude::OpenOptionsExt, 7 | path::{Path, PathBuf}, 8 | }; 9 | 10 | use eyre::{bail, Context, Result}; 11 | use flate2::read::GzDecoder; 12 | use fs_extra::dir::CopyOptions; 13 | use subprocess::{Exec, Redirection}; 14 | use time::{format_description::well_known::Rfc2822, OffsetDateTime}; 15 | 16 | use crate::{ 17 | util::{chmod, fetch_email_address, mkdir, ExecExt}, 18 | Args, PackageInfo, Script, TargetPackage, 19 | }; 20 | 21 | // FIXME: Use custom patch dirs (maybe break compat with alien?) 22 | const PATCH_DIRS: &[&str] = &["/var/lib/alien", "/usr/share/alien/patches"]; 23 | 24 | #[derive(Debug)] 25 | pub struct DebTarget { 26 | info: PackageInfo, 27 | unpacked_dir: PathBuf, 28 | debian_dir: PathBuf, 29 | dir_map: HashMap<&'static Path, &'static Path>, 30 | } 31 | impl DebTarget { 32 | pub fn new(mut info: PackageInfo, unpacked_dir: PathBuf, args: &Args) -> Result { 33 | Self::sanitize_info(&mut info)?; 34 | 35 | // Make .orig.tar.gz directory? 36 | if !args.deb_args.single && !args.generate { 37 | let option = CopyOptions { 38 | overwrite: true, 39 | ..Default::default() 40 | }; 41 | let mut target = unpacked_dir.as_os_str().to_owned(); 42 | target.push(".orig"); 43 | let target = PathBuf::from(target); 44 | 45 | if !target.exists() { 46 | mkdir(&target)?; 47 | } 48 | fs_extra::dir::copy(&unpacked_dir, target, &option)?; 49 | } 50 | 51 | let patch_file = if args.deb_args.nopatch { 52 | None 53 | } else { 54 | match &args.deb_args.patch { 55 | Some(o) => Some(o.clone()), 56 | None => get_patch(&info, args.deb_args.anypatch, PATCH_DIRS), 57 | } 58 | }; 59 | 60 | let debian_dir = unpacked_dir.join("debian"); 61 | mkdir(&debian_dir)?; 62 | 63 | // Use a patch file to debianize? 64 | if let Some(patch) = &patch_file { 65 | return Self::patch(info, unpacked_dir, patch, debian_dir); 66 | } 67 | 68 | // Automatic debianization. 69 | let mut writer = DebWriter::new(debian_dir, info)?; 70 | 71 | writer.write_changelog()?; 72 | writer.write_control()?; 73 | writer.write_copyright()?; 74 | writer.write_conffiles()?; 75 | writer.write_compat(7)?; // Use debhelper v7 76 | writer.write_rules(args.deb_args.fixperms)?; 77 | writer.write_scripts()?; 78 | 79 | let DebWriter { info, dir, .. } = writer; 80 | 81 | // Move files to FHS-compliant locations, if possible. 82 | // Note: no trailing slashes on these directory names! 83 | let mut dir_map = HashMap::new(); 84 | 85 | for (old_dir, new_dir) in [ 86 | ("/usr/man", "/usr/share/man"), 87 | ("/usr/info", "/usr/share/info"), 88 | ("/usr/doc", "/usr/doc/info"), 89 | ] { 90 | let old_dir = Path::new(old_dir); 91 | let new_dir = Path::new(new_dir); 92 | let prefixed_old_dir = dir.join(old_dir); 93 | let prefixed_new_dir = dir.join(new_dir); 94 | 95 | if prefixed_old_dir.is_dir() && !prefixed_new_dir.exists() { 96 | // Ignore failure.. 97 | let dir_base = dir.join(new_dir.parent().unwrap_or(new_dir)); 98 | Exec::cmd("install") 99 | .arg("-d") 100 | .arg(dir_base) 101 | .log_and_spawn(None)?; 102 | 103 | fs_extra::dir::move_dir(&prefixed_old_dir, &prefixed_new_dir, &CopyOptions::new())?; 104 | if prefixed_old_dir.is_dir() { 105 | std::fs::remove_dir_all(&prefixed_old_dir)?; 106 | } 107 | 108 | // store for cleantree 109 | dir_map.insert(old_dir, new_dir); 110 | } 111 | } 112 | 113 | Ok(Self { 114 | info, 115 | unpacked_dir, 116 | debian_dir: dir, 117 | dir_map, 118 | }) 119 | } 120 | 121 | fn patch( 122 | mut info: PackageInfo, 123 | unpacked_dir: PathBuf, 124 | patch: &Path, 125 | debian_dir: PathBuf, 126 | ) -> Result { 127 | let mut data = vec![]; 128 | let mut unzipped = GzDecoder::new(File::open(patch)?); 129 | unzipped.read_to_end(&mut data)?; 130 | 131 | Exec::cmd("patch") 132 | .arg("-p1") 133 | .cwd(&unpacked_dir) 134 | .stdin(data) 135 | .log_and_output(None) 136 | .wrap_err("Patch error")?; 137 | 138 | // If any .rej file exists, we dun goof'd 139 | if glob::glob("*.rej").unwrap().any(|_| true) { 140 | bail!("Patch failed with .rej files; giving up"); 141 | } 142 | for orig in glob::glob("*.orig").unwrap() { 143 | std::fs::remove_file(orig?)?; 144 | } 145 | chmod(debian_dir.join("rules"), 0o755)?; 146 | 147 | if let Ok(changelog) = File::open(debian_dir.join("changelog")) { 148 | let mut changelog = BufReader::new(changelog); 149 | let mut line = String::new(); 150 | changelog.read_line(&mut line)?; 151 | 152 | // find the version inside the parens. 153 | if let Some((a, b)) = line.find('(').zip(line.find(')')) { 154 | // ensure no whitespace 155 | let version = line[a + 1..b].replace(char::is_whitespace, ""); 156 | super::set_version_and_release(&mut info, &version); 157 | }; 158 | } 159 | 160 | Ok(Self { 161 | info, 162 | unpacked_dir, 163 | debian_dir, 164 | dir_map: HashMap::new(), 165 | }) 166 | } 167 | fn sanitize_info(info: &mut PackageInfo) -> Result<()> { 168 | // Version 169 | 170 | // filter out some characters not allowed in debian versions 171 | // see lib/dpkg/parsehelp.c parseversion 172 | fn valid_version_characters(c: char) -> bool { 173 | matches!(c, '-' | '.' | '+' | '~' | ':') || c.is_ascii_alphanumeric() 174 | } 175 | 176 | let iter = info 177 | .version 178 | .chars() 179 | .filter(|&c| valid_version_characters(c)); 180 | 181 | info.version = if info.version.starts_with(|c: char| c.is_ascii_digit()) { 182 | iter.collect() 183 | } else { 184 | // make sure the version contains a digit at the start, as required by dpkg-deb 185 | std::iter::once('0').chain(iter).collect() 186 | }; 187 | 188 | // Release 189 | // Make sure the release contains digits. 190 | if info.release.parse::().is_err() { 191 | info.release.push_str("1"); 192 | } 193 | 194 | // Description 195 | 196 | let mut desc = String::new(); 197 | for line in info.description.lines() { 198 | let line = line.replace('\t', " "); // change tabs to spaces 199 | let line = line.trim_end(); // remove trailing whitespace 200 | let line = if line.is_empty() { "." } else { line }; // empty lines become dots 201 | desc.push(' '); 202 | desc.push_str(line); 203 | desc.push('\n'); 204 | } 205 | // remove leading blank lines 206 | let mut desc = String::from(desc.trim_start_matches('\n')); 207 | if !desc.is_empty() { 208 | desc.push_str(" .\n"); 209 | } 210 | write!( 211 | desc, 212 | " (Converted from a {} package by xenomorph version {}.)", 213 | info.original_format, 214 | env!("CARGO_PKG_VERSION") 215 | )?; 216 | 217 | info.description = desc; 218 | 219 | Ok(()) 220 | } 221 | } 222 | impl TargetPackage for DebTarget { 223 | fn clean_tree(&mut self) -> Result<()> { 224 | let dir = &self.unpacked_dir; 225 | for (old_dir, new_dir) in &self.dir_map { 226 | let prefixed_old_dir = dir.join(old_dir); 227 | let prefixed_new_dir = dir.join(new_dir); 228 | 229 | if !prefixed_old_dir.exists() && prefixed_new_dir.is_dir() { 230 | // Ignore failure.. (should I?) 231 | let dir_base = dir.join(old_dir.parent().unwrap_or(old_dir)); 232 | Exec::cmd("install") 233 | .arg("-d") 234 | .arg(dir_base) 235 | .log_and_spawn(None)?; 236 | 237 | fs_extra::dir::move_dir(&prefixed_new_dir, &prefixed_old_dir, &CopyOptions::new())?; 238 | if prefixed_new_dir.is_dir() { 239 | std::fs::remove_dir_all(&prefixed_new_dir)?; 240 | } 241 | } 242 | } 243 | std::fs::remove_dir_all(&self.debian_dir)?; 244 | Ok(()) 245 | } 246 | 247 | fn build(&mut self) -> Result { 248 | let PackageInfo { 249 | arch, 250 | name, 251 | version, 252 | release, 253 | .. 254 | } = &self.info; 255 | 256 | // Detect architecture mismatch and abort with a comprehensible error message. 257 | if arch != "all" 258 | && !Exec::cmd("dpkg-architecture") 259 | .arg("-i") 260 | .arg(arch) 261 | .log_and_output_without_checking(None) 262 | .wrap_err("dpkg-architecture not found - have you installed dpkg-dev?")? 263 | .success() 264 | { 265 | bail!( 266 | "{} is for architecture {}; the package cannot be built on this system", 267 | self.info.file.display(), 268 | arch 269 | ); 270 | } 271 | 272 | let log = Exec::cmd("debian/rules") 273 | .cwd(&self.unpacked_dir) 274 | .arg("binary") 275 | .stderr(Redirection::Merge) 276 | .log_and_output_without_checking(None)?; 277 | if !log.success() { 278 | if log.stderr.is_empty() { 279 | bail!("Package build failed; could not run generated debian/rules file."); 280 | } 281 | bail!( 282 | "Package build failed. Here's the log:\n{}", 283 | log.stderr_str() 284 | ); 285 | } 286 | 287 | let path = format!("{name}_{version}-{release}_{arch}.deb"); 288 | Ok(PathBuf::from(path)) 289 | } 290 | fn test(&mut self, file_name: &Path) -> Result> { 291 | let Ok(lintian) = which::which("lintian") else { 292 | return Ok(vec!["lintian not available, so not testing".into()]); 293 | }; 294 | 295 | let output = Exec::cmd(lintian) 296 | .arg(file_name) 297 | .log_and_output(None)? 298 | .stdout; 299 | 300 | let strings = output 301 | .lines() 302 | .filter_map(|s| s.ok()) 303 | // Lintian doesn't know about our custom section 304 | .filter(|s| !s.contains("unknown-section xenomorph")) 305 | .map(|s| s.trim().to_owned()) 306 | .collect(); 307 | 308 | Ok(strings) 309 | } 310 | } 311 | 312 | struct DebWriter { 313 | dir: PathBuf, 314 | info: PackageInfo, 315 | realname: String, 316 | email: String, 317 | date: String, 318 | } 319 | impl DebWriter { 320 | fn new(dir: PathBuf, info: PackageInfo) -> Result { 321 | let realname = whoami::realname(); 322 | let email = fetch_email_address(); 323 | let date = OffsetDateTime::now_local() 324 | .unwrap_or_else(|_| OffsetDateTime::now_utc()) 325 | .format(&Rfc2822)?; 326 | 327 | Ok(Self { 328 | dir, 329 | info, 330 | realname, 331 | email, 332 | date, 333 | }) 334 | } 335 | 336 | fn write_changelog(&mut self) -> Result<()> { 337 | let Self { 338 | dir, 339 | info, 340 | realname, 341 | email, 342 | date, 343 | } = self; 344 | let PackageInfo { 345 | name, 346 | version, 347 | release, 348 | original_format, 349 | changelog: changelog_text, 350 | .. 351 | } = info; 352 | 353 | dir.push("changelog"); 354 | let mut file = File::create(&dir)?; 355 | 356 | #[rustfmt::skip] 357 | writeln!( 358 | file, 359 | r#"{name} ({version}-{release}) experimental; urgency=low 360 | 361 | * Converted from {original_format} format to .deb by xenomorph version {xenomorph_version} 362 | 363 | {changelog_text} 364 | 365 | -- {realname} <{email}> {date} 366 | "#, 367 | xenomorph_version = env!("CARGO_PKG_VERSION") 368 | )?; 369 | 370 | dir.pop(); 371 | Ok(()) 372 | } 373 | 374 | fn write_control(&mut self) -> Result<()> { 375 | let Self { 376 | dir, 377 | info, 378 | realname, 379 | email, 380 | .. 381 | } = self; 382 | let PackageInfo { 383 | name, 384 | arch, 385 | dependencies: depends, 386 | summary, 387 | description, 388 | .. 389 | } = info; 390 | 391 | dir.push("control"); 392 | let mut file = File::create(&dir)?; 393 | 394 | #[rustfmt::skip] 395 | write!( 396 | file, 397 | r#"Source: {name} 398 | Section: xenomorph 399 | Priority: extra 400 | Maintainer: {realname} <{email}> 401 | 402 | Package: {name} 403 | Architecture: {arch} 404 | Depends: ${{shlibs:Depends}}"# 405 | )?; 406 | for dep in depends { 407 | write!(file, ", {dep}")?; 408 | } 409 | #[rustfmt::skip] 410 | writeln!( 411 | file, 412 | r#" 413 | Description: {summary} 414 | {description} 415 | "#, 416 | )?; 417 | 418 | dir.pop(); 419 | Ok(()) 420 | } 421 | 422 | fn write_copyright(&mut self) -> Result<()> { 423 | let Self { 424 | dir, info, date, .. 425 | } = self; 426 | let PackageInfo { 427 | original_format, 428 | copyright, 429 | binary_info, 430 | .. 431 | } = info; 432 | 433 | dir.push("copyright"); 434 | let mut file = File::create(&dir)?; 435 | 436 | #[rustfmt::skip] 437 | writeln!( 438 | file, 439 | r#"This package was repackaged by `xenomorph` by converting 440 | a binary .{original_format} package on {date} 441 | 442 | Copyright: {copyright} 443 | 444 | Information from the binary package: 445 | {binary_info} 446 | "# 447 | )?; 448 | 449 | dir.pop(); 450 | Ok(()) 451 | } 452 | 453 | fn write_conffiles(&mut self) -> Result<()> { 454 | self.dir.push("conffiles"); 455 | 456 | let mut conffiles = self 457 | .info 458 | .conffiles 459 | .iter() 460 | // `debhelper` takes care of files in /etc. 461 | .filter(|s| !s.starts_with("/etc")) 462 | .peekable(); 463 | 464 | if conffiles.peek().is_some() { 465 | let mut file = File::create(&self.dir)?; 466 | for conffile in conffiles { 467 | writeln!(file, "{}", conffile.display())?; 468 | } 469 | } 470 | 471 | self.dir.pop(); 472 | Ok(()) 473 | } 474 | 475 | fn write_compat(&mut self, version: u32) -> Result<()> { 476 | self.dir.push("compat"); 477 | 478 | let mut file = File::create(&self.dir)?; 479 | writeln!(file, "{version}")?; 480 | 481 | self.dir.pop(); 482 | Ok(()) 483 | } 484 | 485 | fn write_rules(&mut self, fix_perms: bool) -> Result<()> { 486 | self.dir.push("rules"); 487 | 488 | let mut file = File::options() 489 | .write(true) 490 | .create(true) 491 | .truncate(true) 492 | // TODO: ignore this on windows 493 | .mode(0o755) 494 | .open(&self.dir)?; 495 | #[rustfmt::skip] 496 | writeln!( 497 | file, 498 | r#"#!/usr/bin/make -f 499 | # debian/rules for xenomorph 500 | 501 | PACKAGE = $(shell dh_listpackages) 502 | 503 | build: 504 | dh_testdir 505 | 506 | clean: 507 | dh_testdir 508 | dh_testroot 509 | dh_clean -d 510 | 511 | binary-arch: build 512 | dh_testdir 513 | dh_testroot 514 | dh_prep 515 | dh_installdirs 516 | 517 | dh_installdocs 518 | dh_installchangelogs 519 | 520 | # Copy the packages' files. 521 | find . -maxdepth 1 -mindepth 1 -not -name debian -print0 | \ 522 | xargs -0 -r -i cp -a {{}} debian/$(PACKAGE) 523 | 524 | # 525 | # If you need to move files around in debian/$(PACKAGE) or do some 526 | # binary patching, do it here 527 | # 528 | 529 | 530 | # This has been known to break on some wacky binaries. 531 | # dh_strip 532 | dh_compress 533 | {} dh_fixperms 534 | dh_makeshlibs 535 | dh_installdeb 536 | -dh_shlibdeps 537 | dh_gencontrol 538 | dh_md5sums 539 | dh_builddeb 540 | 541 | binary: binary-indep binary-arch 542 | .PHONY: build clean binary-indep binary-arch binary 543 | "#, 544 | if fix_perms { "" } else { "#" } 545 | )?; 546 | 547 | self.dir.pop(); 548 | Ok(()) 549 | } 550 | fn write_scripts(&mut self) -> Result<()> { 551 | // There may be a postinst with permissions fixups even when scripts are disabled. 552 | self.write_script(Script::AfterInstall)?; 553 | 554 | if self.info.use_scripts { 555 | self.write_script(Script::BeforeInstall)?; 556 | self.write_script(Script::AfterUninstall)?; 557 | self.write_script(Script::BeforeUninstall)?; 558 | } 559 | Ok(()) 560 | } 561 | fn write_script(&mut self, script: Script) -> Result<()> { 562 | let data = self.info.scripts.get(&script).cloned(); 563 | 564 | let data = if script == Script::AfterInstall { 565 | let mut data = data.unwrap_or_default(); 566 | self.patch_postinst(&mut data); 567 | data 568 | } else if let Some(data) = data { 569 | data 570 | } else { 571 | return Ok(()); 572 | }; 573 | 574 | if !data.trim().is_empty() { 575 | self.dir.push(script.deb_name()); 576 | std::fs::write(&self.dir, data)?; 577 | self.dir.pop(); 578 | } 579 | Ok(()) 580 | } 581 | fn patch_postinst(&self, old: &mut String) { 582 | let PackageInfo { file_info, .. } = &self.info; 583 | 584 | if file_info.is_empty() { 585 | return; 586 | } 587 | 588 | // If there is no postinst, let's make one up.. 589 | if old.is_empty() { 590 | old.push_str("#!/bin/sh\n"); 591 | } 592 | 593 | let index = old.find('\n').unwrap_or(old.len()); 594 | let first_line = &old[..index]; 595 | 596 | if let Some(s) = first_line.strip_prefix("#!") { 597 | let s = s.trim_start(); 598 | if let "/bin/bash" | "/bin/sh" = s { 599 | eprintln!("warning: unable to add ownership fixup code to postinst as the postinst is not a shell script!"); 600 | return; 601 | } 602 | } 603 | 604 | let mut injection = String::from("\n# xenomorph added permissions fixup code"); 605 | 606 | for (file, file_info) in file_info { 607 | // no single quotes in single quotes... 608 | let escaped_file = file.to_string_lossy().replace('\'', r#"'"'"'"#); 609 | let own_info = &file_info.owner; 610 | write!(injection, "\nchown '{own_info}' '{escaped_file}'").unwrap(); 611 | 612 | if let Some(mode_info) = file_info.mode { 613 | write!(injection, "\nchmod '{mode_info}' '{escaped_file}'").unwrap(); 614 | } 615 | } 616 | old.insert_str(index, &injection); 617 | } 618 | } 619 | 620 | fn get_patch(info: &PackageInfo, anypatch: bool, dirs: &[&str]) -> Option { 621 | let mut patches: Vec<_> = dirs 622 | .iter() 623 | .flat_map(|dir| { 624 | let p = format!( 625 | "{}/{}_{}-{}*.diff.gz", 626 | dir, info.name, info.version, info.release 627 | ); 628 | glob::glob(&p).unwrap() 629 | }) 630 | .collect(); 631 | 632 | if patches.is_empty() { 633 | // Try not matching the release, see if that helps. 634 | patches.extend(dirs.iter().flat_map(|dir| { 635 | let p = format!("{dir}/{}_{}*.diff.gz", info.name, info.version); 636 | glob::glob(&p).unwrap() 637 | })); 638 | 639 | if !patches.is_empty() && anypatch { 640 | // Fall back to anything that matches the name. 641 | patches.extend(dirs.iter().flat_map(|dir| { 642 | let p = format!("{dir}/{}_*.diff.gz", info.name); 643 | glob::glob(&p).unwrap() 644 | })); 645 | } 646 | } 647 | 648 | // just get the first one 649 | patches.into_iter().find_map(|p| p.ok()) 650 | } 651 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | #![warn(rust_2018_idioms, clippy::pedantic)] 3 | #![allow( 4 | clippy::let_unit_value, 5 | clippy::module_name_repetitions, 6 | clippy::missing_errors_doc, 7 | clippy::missing_panics_doc, 8 | clippy::redundant_closure_for_method_calls, 9 | clippy::struct_excessive_bools 10 | )] 11 | 12 | use std::{ 13 | collections::HashMap, 14 | fmt::Display, 15 | path::{Path, PathBuf}, 16 | }; 17 | 18 | use enum_dispatch::enum_dispatch; 19 | use eyre::{bail, Result}; 20 | use pkg::{PkgSource, PkgTarget}; 21 | use util::Args; 22 | 23 | use deb::{DebSource, DebTarget}; 24 | use lsb::{LsbSource, LsbTarget}; 25 | use rpm::{RpmSource, RpmTarget}; 26 | use tgz::{TgzSource, TgzTarget}; 27 | 28 | pub mod deb; 29 | pub mod lsb; 30 | pub mod pkg; 31 | pub mod rpm; 32 | pub mod tgz; 33 | pub mod util; 34 | 35 | /// A source package that can be unpacked, queried and modified. 36 | #[enum_dispatch] 37 | pub trait SourcePackage { 38 | /// Gets an immutable reference to the package info. 39 | fn info(&self) -> &PackageInfo; 40 | 41 | /// Gets a mutable reference to the package info. 42 | fn info_mut(&mut self) -> &mut PackageInfo; 43 | 44 | /// Extracts the package info by value, consuming the package. 45 | fn into_info(self) -> PackageInfo; 46 | 47 | /// Unpacks the package into a temporary directory, whose path is then returned. 48 | fn unpack(&mut self) -> Result; 49 | 50 | /// Increments the release field of the package by the specified bump value. 51 | /// 52 | /// If the release field is not a valid number, then it is set to the bump value. 53 | fn increment_release(&mut self, bump: u32) { 54 | let release = &mut self.info_mut().release; 55 | 56 | *release = if let Ok(num) = release.parse::() { 57 | (num + bump).to_string() 58 | } else { 59 | // Perl's string-number addition thing is... cursed. 60 | // If a string doesn't parse to a number, then it is treated as 0. 61 | // So, we will just set the release to the bump here. 62 | bump.to_string() 63 | }; 64 | } 65 | } 66 | 67 | /// A target package that can be built, tested and installed. 68 | #[enum_dispatch] 69 | pub trait TargetPackage { 70 | /// Cleans the unpacked directory of any side-effects caused by 71 | /// initialization and [building](Self::build). 72 | fn clean_tree(&mut self) -> Result<()> { 73 | Ok(()) 74 | } 75 | 76 | /// Builds a package from the completed unpacked directory, 77 | /// which is then placed in the current directory. 78 | /// 79 | /// Returns the path to the built package. 80 | fn build(&mut self) -> Result; 81 | 82 | /// Tests the given package file, and returns the test results as a list of lines. 83 | #[allow(unused_variables)] 84 | fn test(&mut self, package: &Path) -> Result> { 85 | Ok(vec![]) 86 | } 87 | } 88 | 89 | #[enum_dispatch(SourcePackage)] 90 | #[derive(Debug)] 91 | pub enum AnySourcePackage { 92 | Lsb(LsbSource), 93 | Rpm(RpmSource), 94 | Deb(DebSource), 95 | Tgz(TgzSource), 96 | Pkg(PkgSource), 97 | } 98 | impl AnySourcePackage { 99 | pub fn new(file: PathBuf, args: &Args) -> Result { 100 | if LsbSource::check_file(&file) { 101 | LsbSource::new(file, args).map(Self::Lsb) 102 | } else if RpmSource::check_file(&file) { 103 | RpmSource::new(file, args).map(Self::Rpm) 104 | } else if DebSource::check_file(&file) { 105 | DebSource::new(file, args).map(Self::Deb) 106 | } else if TgzSource::check_file(&file) { 107 | TgzSource::new(file).map(Self::Tgz) 108 | } else if PkgSource::check_file(&file) { 109 | PkgSource::new(file).map(Self::Pkg) 110 | } else { 111 | bail!("Unknown type of package, {}", file.display()); 112 | } 113 | } 114 | } 115 | 116 | #[enum_dispatch(TargetPackage)] 117 | #[derive(Debug)] 118 | pub enum AnyTargetPackage { 119 | Lsb(LsbTarget), 120 | Rpm(RpmTarget), 121 | Deb(DebTarget), 122 | Tgz(TgzTarget), 123 | Pkg(PkgTarget), 124 | } 125 | impl AnyTargetPackage { 126 | pub fn new( 127 | format: Format, 128 | info: PackageInfo, 129 | unpacked_dir: PathBuf, 130 | args: &Args, 131 | ) -> Result { 132 | let target = match format { 133 | Format::Lsb => Self::Lsb(LsbTarget::new(info, unpacked_dir)?), 134 | Format::Rpm => Self::Rpm(RpmTarget::new(info, unpacked_dir)?), 135 | Format::Deb => Self::Deb(DebTarget::new(info, unpacked_dir, args)?), 136 | Format::Tgz => Self::Tgz(TgzTarget::new(info, unpacked_dir)?), 137 | Format::Pkg => Self::Pkg(PkgTarget::new(info, unpacked_dir)?), 138 | }; 139 | Ok(target) 140 | } 141 | } 142 | 143 | /// Extracted information about a package. 144 | #[derive(Debug, Default, Clone, PartialEq, Eq)] 145 | pub struct PackageInfo { 146 | /// The path to the package. 147 | pub file: PathBuf, 148 | 149 | /// The package's name. 150 | pub name: String, 151 | /// The package's upstream version. 152 | pub version: String, 153 | /// The package's distribution-specific release number. 154 | pub release: String, 155 | /// The package's architecture, in the format used by Debian. 156 | pub arch: String, 157 | /// The package's maintainer. 158 | pub maintainer: String, 159 | /// The package's dependencies. 160 | /// 161 | /// Only dependencies that should exist on all target distributions 162 | /// can be put in here though, such as `lsb`. 163 | pub dependencies: Vec, 164 | /// The section the package is in. 165 | pub group: String, 166 | /// A one-line description of the package. 167 | pub summary: String, 168 | /// A longer description of the package. 169 | /// 170 | /// May contain multiple paragraphs. 171 | pub description: String, 172 | /// A short statement of copyright. 173 | pub copyright: String, 174 | /// The format the package was originally in. 175 | pub original_format: Format, 176 | /// The distribution family the package originated from. 177 | pub distribution: String, 178 | /// Whatever the package's package tool says when 179 | /// told to display info about the package. 180 | pub binary_info: String, 181 | /// A list of all conffiles in the package. 182 | pub conffiles: Vec, 183 | /// A list of all files in the package. 184 | pub files: Vec, 185 | /// The text of the changelog. 186 | pub changelog: String, 187 | 188 | /// When generating the package, only use the [`Self::scripts`] field 189 | /// if this is set to a true value. 190 | pub use_scripts: bool, 191 | /// A map of all [scripts](Script) in the package. 192 | pub scripts: HashMap, 193 | /// A map of file paths to ownership and mode information. 194 | /// 195 | /// Some files cannot be represented on the filesystem — typically, that is 196 | /// because the owners or groups just don't exist yet — so `xenomorph` has to 197 | /// store to preserve their ownership information (as well as mode information 198 | /// for `setuid` files) externally in this map. 199 | pub file_info: HashMap, 200 | } 201 | 202 | /// Special information about files. See [`PackageInfo::file_info`] for more. 203 | #[derive(Debug, Clone, PartialEq, Eq, Default)] 204 | pub struct FileInfo { 205 | /// The owner of the file. 206 | owner: String, 207 | /// The original mode of the file. Set for `setuid` files. 208 | mode: Option, 209 | } 210 | 211 | /// Scripts that may be run in the build process. See [`PackageInfo::scripts`] for more. 212 | /// 213 | /// Due to historical reasons, there are many names for these scripts across 214 | /// different package managers. Here's a table linking all of them together: 215 | /// 216 | /// | `xenomorph` name | Debian-style name | RPM scriptlet name | RPM query key | `tgz` script name | `pkg` script name | 217 | /// |---------------------------|-------------------|--------------------|---------------|-------------------|-------------------| 218 | /// | [`Self::BeforeInstall`] | `preinst` | `%pre` | `%{PREIN}` | `predoinst.sh` | `preinstall` | 219 | /// | [`Self::AfterInstall`] | `postinst` | `%post` | `%{POSTIN}` | `doinst.sh` | `postinstall` | 220 | /// | [`Self::BeforeUninstall`] | `prerm` | `%preun` | `%{PREUN}` | `predelete.sh` | `preremove` | 221 | /// | [`Self::AfterInstall`] | `postrm` | `%postun` | `%{POSTUN}` | `delete.sh` | `postremove` | 222 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 223 | pub enum Script { 224 | /// Script that will be run before install. 225 | BeforeInstall, 226 | /// Script that will be run after install. 227 | AfterInstall, 228 | /// Script that will be run before uninstall. 229 | BeforeUninstall, 230 | /// Script that will be run after uninstall. 231 | AfterUninstall, 232 | } 233 | impl Script { 234 | /// All recognized scripts. 235 | pub const ALL: [Script; 4] = [ 236 | Self::BeforeInstall, 237 | Self::AfterInstall, 238 | Self::BeforeUninstall, 239 | Self::AfterUninstall, 240 | ]; 241 | 242 | /// Gets a script from its Debian-style name. 243 | /// 244 | /// See the [type-level documentation](Self) for the mapping between 245 | /// Debian-style names and [`Script`] variants. 246 | #[must_use] 247 | pub fn from_deb_name(s: &str) -> Option { 248 | match s { 249 | "preinst" => Some(Self::BeforeInstall), 250 | "postinst" => Some(Self::AfterInstall), 251 | "prerm" => Some(Self::BeforeUninstall), 252 | "postrm" => Some(Self::AfterUninstall), 253 | _ => None, 254 | } 255 | } 256 | 257 | /// Returns the script's Debian-style name. 258 | /// 259 | /// See the [type-level documentation](Self) for the mapping between 260 | /// Debian-style names and [`Script`] variants. 261 | #[must_use] 262 | pub fn deb_name(&self) -> &str { 263 | match self { 264 | Self::BeforeInstall => "preinst", 265 | Self::AfterInstall => "postinst", 266 | Self::BeforeUninstall => "prerm", 267 | Self::AfterUninstall => "postrm", 268 | } 269 | } 270 | /// Returns the script's RPM query key. 271 | /// 272 | /// See the [type-level documentation](Self) for the mapping between 273 | /// RPM query keys and [`Script`] variants. 274 | #[must_use] 275 | pub fn rpm_query_key(&self) -> &str { 276 | match self { 277 | Self::BeforeInstall => "%{PREIN}", 278 | Self::AfterInstall => "%{POSTIN}", 279 | Self::BeforeUninstall => "%{PREUN}", 280 | Self::AfterUninstall => "%{POSTUN}", 281 | } 282 | } 283 | /// Returns the script's RPM scriptlet name. 284 | /// 285 | /// See the [type-level documentation](Self) for the mapping between 286 | /// RPM scriptlet names and [`Script`] variants. 287 | #[must_use] 288 | pub fn rpm_scriptlet_name(&self) -> &str { 289 | match self { 290 | Self::BeforeInstall => "%pre", 291 | Self::AfterInstall => "%post", 292 | Self::BeforeUninstall => "%preun", 293 | Self::AfterUninstall => "%postun", 294 | } 295 | } 296 | /// Gets a script from its `tgz`-style script name. 297 | /// 298 | /// See the [type-level documentation](Self) for the mapping between 299 | /// `tgz`-style script names and [`Script`] variants. 300 | #[must_use] 301 | pub fn from_tgz_script_name(s: &str) -> Option { 302 | match s { 303 | "predoinst.sh" => Some(Self::BeforeInstall), 304 | "doinst.sh" => Some(Self::AfterInstall), 305 | "predelete.sh" => Some(Self::BeforeUninstall), 306 | "delete.sh" => Some(Self::AfterUninstall), 307 | _ => None, 308 | } 309 | } 310 | /// Returns the script's `tgz`-style script name. 311 | /// 312 | /// See the [type-level documentation](Self) for the mapping between 313 | /// `tgz`-style names and [`Script`] variants. 314 | #[must_use] 315 | pub fn tgz_script_name(&self) -> &str { 316 | match self { 317 | Self::BeforeInstall => "predoinst.sh", 318 | Self::AfterInstall => "doinst.sh", 319 | Self::BeforeUninstall => "predelete.sh", 320 | Self::AfterUninstall => "delete.sh", 321 | } 322 | } 323 | /// Gets a script from its `pkg`-style script name. 324 | /// 325 | /// See the [type-level documentation](Self) for the mapping between 326 | /// `pkg`-style script names and [`Script`] variants. 327 | #[must_use] 328 | pub fn from_pkg_script_name(s: &str) -> Option { 329 | match s { 330 | "preinstall" => Some(Self::BeforeInstall), 331 | "postinstall" => Some(Self::AfterInstall), 332 | "preremove" => Some(Self::BeforeUninstall), 333 | "postremove" => Some(Self::AfterUninstall), 334 | _ => None, 335 | } 336 | } 337 | /// Returns the script's `pkg`-style script name. 338 | /// 339 | /// See the [type-level documentation](Self) for the mapping between 340 | /// `pkg`-style script names and [`Script`] variants. 341 | #[must_use] 342 | pub fn pkg_script_name(&self) -> &str { 343 | match self { 344 | Self::BeforeInstall => "preinstall", 345 | Self::AfterInstall => "postinstall", 346 | Self::BeforeUninstall => "preremove", 347 | Self::AfterUninstall => "postremove", 348 | } 349 | } 350 | } 351 | 352 | /// Format of a package. 353 | #[enumflags2::bitflags] 354 | #[repr(u8)] 355 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] 356 | pub enum Format { 357 | /// The `.deb` format, used by `dpkg` and default for Debian- 358 | /// and Ubuntu-derived distributions. 359 | #[default] 360 | Deb, 361 | /// The package format used by Linux Standard Base. 362 | /// Basically an [`rpm` file](Self::Rpm) with a `lsb-` prefix 363 | /// and a dependency on the `lsb` package. 364 | Lsb, 365 | /// The `.pkg` format, used by Solaris. 366 | Pkg, 367 | /// The `.rpm` format, used by the RPM package manager prevalent 368 | /// on many distributions derived from Red Hat Linux, 369 | /// including RHEL, CentOS, openSUSE, Fedora, and more. 370 | Rpm, 371 | /// The `.tgz` format, used by Slackware. 372 | Tgz, 373 | } 374 | impl Format { 375 | pub fn install(self, path: &Path) -> Result<()> { 376 | match self { 377 | Format::Deb => deb::install(path), 378 | Format::Lsb | Format::Rpm => rpm::install(path), 379 | Format::Pkg => pkg::install(path), 380 | Format::Tgz => tgz::install(path), 381 | } 382 | } 383 | } 384 | impl Display for Format { 385 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 386 | f.write_str(match self { 387 | Format::Deb => "deb", 388 | Format::Lsb => "lsb", 389 | Format::Pkg => "pkg", 390 | Format::Rpm => "rpm", 391 | Format::Tgz => "tgz", 392 | }) 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /src/lsb.rs: -------------------------------------------------------------------------------- 1 | use std::path::{Path, PathBuf}; 2 | 3 | use eyre::Result; 4 | 5 | use crate::{rpm::source::RpmReader, Args}; 6 | 7 | use super::{ 8 | rpm::{RpmSource, RpmTarget}, 9 | Format, PackageInfo, SourcePackage, TargetPackage, 10 | }; 11 | 12 | #[derive(Debug)] 13 | pub struct LsbSource { 14 | rpm: RpmSource, 15 | } 16 | 17 | impl LsbSource { 18 | /// `lsb` files are `rpm`s with a lsb- prefix, that depend on 19 | /// a package called 'lsb' and nothing else. 20 | #[must_use] 21 | pub fn check_file(file: &Path) -> bool { 22 | let Some(stem) = file.file_stem().and_then(|s| s.to_str()) else { 23 | return false; 24 | }; 25 | if !stem.starts_with("lsb-") { 26 | return false; 27 | } 28 | if !RpmSource::check_file(file) { 29 | return false; 30 | } 31 | 32 | let Ok(deps) = RpmReader::new(file).query("-R") else { 33 | return false; 34 | }; 35 | 36 | deps.lines().any(|s| s.trim() == "lsb") 37 | } 38 | pub fn new(lsb_file: PathBuf, args: &Args) -> Result { 39 | let mut rpm = RpmSource::new(lsb_file, args)?; 40 | let info = rpm.info_mut(); 41 | 42 | info.distribution = "Linux Standard Base".into(); 43 | info.original_format = Format::Lsb; 44 | info.dependencies.push("lsb".into()); 45 | info.use_scripts = true; 46 | 47 | Ok(Self { rpm }) 48 | } 49 | } 50 | impl SourcePackage for LsbSource { 51 | fn info(&self) -> &PackageInfo { 52 | self.rpm.info() 53 | } 54 | fn info_mut(&mut self) -> &mut PackageInfo { 55 | self.rpm.info_mut() 56 | } 57 | fn into_info(self) -> PackageInfo { 58 | self.rpm.into_info() 59 | } 60 | 61 | fn unpack(&mut self) -> Result { 62 | self.rpm.unpack() 63 | } 64 | 65 | /// LSB package versions are not changed. 66 | fn increment_release(&mut self, _bump: u32) {} 67 | } 68 | 69 | #[derive(Debug)] 70 | pub struct LsbTarget { 71 | rpm: RpmTarget, 72 | } 73 | impl LsbTarget { 74 | /// Uses [`RpmTarget::new`] to generate the spec file. 75 | /// First though, the package's name is munged to make it LSB compliant (sorta) 76 | /// and `lsb` is added to its dependencies. 77 | pub fn new(mut info: PackageInfo, unpacked_dir: PathBuf) -> Result { 78 | if !info.name.starts_with("lsb-") { 79 | info.name.insert_str(0, "lsb-"); 80 | } 81 | info.dependencies.push("lsb".into()); 82 | 83 | // Always include scripts when generating lsb package. 84 | info.use_scripts = true; 85 | 86 | let rpm = RpmTarget::new(info, unpacked_dir)?; 87 | 88 | Ok(Self { rpm }) 89 | } 90 | } 91 | impl TargetPackage for LsbTarget { 92 | fn clean_tree(&mut self) -> Result<()> { 93 | self.rpm.clean_tree() 94 | } 95 | 96 | /// Uses [`RpmTarget::build`] to build the package, using `lsb-rpmbuild` if available. 97 | fn build(&mut self) -> Result { 98 | if let Ok(lsb_rpmbuild) = which::which("lsb-rpmbuild") { 99 | self.rpm.build_with(&lsb_rpmbuild) 100 | } else { 101 | self.rpm.build() 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | #![warn(rust_2018_idioms, clippy::pedantic)] 3 | 4 | use std::{os::unix::prelude::PermissionsExt, path::Path}; 5 | 6 | use xenomorph::{ 7 | util::{args, Args, Verbosity}, 8 | AnySourcePackage, AnyTargetPackage, Format, PackageInfo, SourcePackage, TargetPackage, 9 | }; 10 | 11 | use bpaf::Parser; 12 | use eyre::{bail, Result}; 13 | 14 | #[cfg(debug_assertions)] 15 | fn eyre() -> Result<()> { 16 | color_eyre::install() 17 | } 18 | #[cfg(not(debug_assertions))] 19 | fn eyre() -> Result<()> { 20 | simple_eyre::install() 21 | } 22 | 23 | fn main() -> Result<()> { 24 | eyre()?; 25 | 26 | let args = args() 27 | .guard( 28 | |a| !(a.install && (a.generate || a.deb_args.single)), 29 | "You cannot use --generate or --single with --install.", 30 | ) 31 | .guard( 32 | |a| !(a.formats.exactly_one().is_none() && (a.generate || a.deb_args.single)), 33 | "--generate and --single may only be used when converting to a single format.", 34 | ) 35 | .guard( 36 | |a| !(a.deb_args.nopatch && a.deb_args.patch.is_some()), 37 | "The options --nopatch and --patchfile cannot be used together.", 38 | ) 39 | .to_options() 40 | .usage("Usage: xenomorph [options] file [...]") 41 | .version(env!("CARGO_PKG_VERSION")) 42 | .run(); 43 | 44 | Verbosity::set(args.verbosity); 45 | 46 | // Check xenomorph's working environment. 47 | // FIXME: We should let people decide the output directory. 48 | if std::fs::write("test", "test").is_ok() { 49 | std::fs::remove_file("test")?; 50 | } else { 51 | bail!("Cannot write to current directory. Try moving to /tmp and re-running `xenomorph`."); 52 | } 53 | 54 | // Check if we're root. 55 | if !nix::unistd::geteuid().is_root() { 56 | if args.formats.contains(Format::Deb) && !args.generate && !args.deb_args.single { 57 | bail!("Must run as root to convert to deb format (or you may use fakeroot)."); 58 | } 59 | eprintln!("Warning: `xenomorph` is not running as root!"); 60 | eprintln!("Warning: Ownerships of files in the generated packages will probably be wrong."); 61 | } 62 | 63 | for file in &args.files { 64 | if !file.try_exists()? { 65 | bail!("File \"{}\" not found.", file.display()); 66 | } 67 | let mut pkg = AnySourcePackage::new(file.clone(), &args)?; 68 | 69 | let scripts = &pkg.info().scripts; 70 | if !pkg.info().use_scripts && !scripts.is_empty() { 71 | if !args.scripts { 72 | eprint!( 73 | "Warning: Skipping conversion of scripts in package {}:", 74 | pkg.info().name, 75 | ); 76 | for (k, v) in scripts { 77 | if !v.is_empty() { 78 | eprint!(" {}", k.deb_name()); 79 | } 80 | } 81 | eprintln!("."); 82 | eprintln!("Warning: Use the --scripts parameter to include the scripts."); 83 | } 84 | pkg.info_mut().use_scripts = args.scripts; 85 | } 86 | 87 | if !args.keep_version { 88 | pkg.increment_release(args.bump); 89 | } 90 | 91 | let unpacked = pkg.unpack()?; 92 | let info = pkg.into_info(); 93 | 94 | let res = generate(file, &info, &unpacked, &args); 95 | cleanup(&unpacked)?; 96 | res?; 97 | } 98 | 99 | Ok(()) 100 | } 101 | 102 | fn generate(file: &Path, info: &PackageInfo, unpacked: &Path, args: &Args) -> Result<()> { 103 | for format in args.formats { 104 | // Convert package 105 | if args.generate || info.original_format != format { 106 | let mut pkg = 107 | AnyTargetPackage::new(format, info.clone(), unpacked.to_path_buf(), args)?; 108 | 109 | if args.generate { 110 | let tree = unpacked.display(); 111 | if format == Format::Deb && !args.deb_args.single { 112 | println!("Directories {tree} and {tree}.orig prepared."); 113 | } else { 114 | println!("Directory {tree} prepared."); 115 | } 116 | // Make sure `package` does not wipe out the 117 | // directory when it is destroyed. 118 | // unpacked.clear(); 119 | continue; 120 | } 121 | 122 | let new_file = pkg.build()?; 123 | 124 | if args.deb_args.test { 125 | let results = pkg.test(&new_file)?; 126 | if !results.is_empty() { 127 | println!("Test results:"); 128 | for result in results { 129 | println!("\t{result}"); 130 | } 131 | } 132 | } 133 | if args.install { 134 | format.install(&new_file)?; 135 | std::fs::remove_file(&new_file)?; 136 | } else { 137 | // Tell them where the package ended up. 138 | println!("{} generated", new_file.display()); 139 | } 140 | 141 | pkg.clean_tree()?; 142 | } else if args.install { 143 | // Don't convert the package, but do install it. 144 | format.install(file)?; 145 | // Note I don't remove it. I figure that might annoy 146 | // people, since it was an input file. 147 | } 148 | } 149 | Ok(()) 150 | } 151 | 152 | fn cleanup(unpacked: &Path) -> Result<()> { 153 | if !unpacked.as_os_str().is_empty() { 154 | // This should never happen, but it pays to check. 155 | if unpacked.as_os_str() == "/" { 156 | bail!( 157 | "xenomorph internal error: unpacked_tree is set to '/'. Please file a bug report!" 158 | ); 159 | } 160 | if unpacked.is_dir() { 161 | // Just in case some dir perms are too screwed up to remove 162 | // and we're not running as root. 163 | for path in glob::glob("*").unwrap() { 164 | let path = path?; 165 | if path.is_dir() { 166 | let mut perms = std::fs::metadata(&path)?.permissions(); 167 | perms.set_mode(0o755); 168 | std::fs::set_permissions(&path, perms)?; 169 | } 170 | } 171 | std::fs::remove_dir_all(unpacked)?; 172 | } 173 | } 174 | Ok(()) 175 | } 176 | -------------------------------------------------------------------------------- /src/pkg/mod.rs: -------------------------------------------------------------------------------- 1 | pub use source::PkgSource; 2 | pub use target::PkgTarget; 3 | 4 | use crate::util::{ExecExt, Verbosity}; 5 | use eyre::{bail, Context, Result}; 6 | use std::path::Path; 7 | use subprocess::Exec; 8 | 9 | pub mod source; 10 | pub mod target; 11 | 12 | /// Install a pkg with pkgadd. Pass in the filename of the pkg to install. 13 | pub fn install(pkg: &Path) -> Result<()> { 14 | if Path::new("/usr/sbin/pkgadd").exists() { 15 | Exec::cmd("/usr/sbin/pkgadd") 16 | .arg("-d") 17 | .arg(".") 18 | .arg(pkg) 19 | .log_and_spawn(Verbosity::VeryVerbose) 20 | .wrap_err("Unable to install") 21 | } else { 22 | bail!("Sorry, I cannot install the generated .pkg file because /usr/sbin/pkgadd is not present.") 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/pkg/source.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fs::File, 4 | io::{BufRead, BufReader}, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | use eyre::{bail, Context, Result}; 9 | use fs_extra::dir::CopyOptions; 10 | use subprocess::Exec; 11 | 12 | use crate::{ 13 | util::{make_unpack_work_dir, ExecExt}, 14 | Format, PackageInfo, Script, SourcePackage, 15 | }; 16 | 17 | #[derive(Debug)] 18 | pub struct PkgSource { 19 | info: PackageInfo, 20 | pkgname: String, 21 | pkg_dir: PathBuf, 22 | 23 | pkgtrans: PathBuf, 24 | } 25 | impl PkgSource { 26 | #[must_use] 27 | pub fn check_file(file: &Path) -> bool { 28 | let Ok(file) = File::open(file) else { 29 | return false; 30 | }; 31 | let mut file = BufReader::new(file); 32 | 33 | let mut line = String::new(); 34 | if file.read_line(&mut line).is_err() { 35 | return false; 36 | } 37 | 38 | line.contains("# PaCkAgE DaTaStReAm") 39 | } 40 | pub fn new(file: PathBuf) -> Result { 41 | let pkginfo = which::which("pkginfo") 42 | .wrap_err("`pkginfo` needs to be installed in order to convert from Solaris pkgs")?; 43 | let pkgtrans = which::which("pkgtrans") 44 | .wrap_err("`pkgtrans` needs to be installed in order to convert from Solaris pkgs")?; 45 | 46 | let Some(name) = file.file_name().map(|s| s.to_string_lossy()) else { 47 | bail!("Cannot extract package name from Solaris pkg file name: {} doesn't have a file name?!", file.display()); 48 | }; 49 | let Some((name, _)) = name.split_once('-') else { 50 | bail!("Cannot extract package name from Solaris pkg file name: {name} does not contain hyphens!"); 51 | }; 52 | let name = name.to_owned(); 53 | 54 | let mut reader = PkgReader::new(file, &pkginfo, &pkgtrans)?; 55 | let copyright = reader.read_copyright()?; 56 | 57 | let mut info = PackageInfo { 58 | name, 59 | group: "unknown".into(), // FIXME 60 | summary: "Converted Solaris pkg package".into(), 61 | copyright, 62 | original_format: Format::Pkg, 63 | distribution: "Solaris".into(), 64 | binary_info: "unknown".into(), // FIXME 65 | ..Default::default() 66 | }; 67 | 68 | reader.read_pkg_info(&mut info)?; 69 | reader.read_pkg_map(&mut info)?; 70 | 71 | reader.cleanup()?; 72 | 73 | let PkgReader { 74 | file, 75 | pkg_dir, 76 | pkgname, 77 | .. 78 | } = reader; 79 | info.file = file; 80 | 81 | Ok(Self { 82 | info, 83 | pkgname, 84 | pkg_dir, 85 | pkgtrans, 86 | }) 87 | } 88 | } 89 | impl SourcePackage for PkgSource { 90 | fn info(&self) -> &PackageInfo { 91 | &self.info 92 | } 93 | fn info_mut(&mut self) -> &mut PackageInfo { 94 | &mut self.info 95 | } 96 | fn into_info(self) -> PackageInfo { 97 | self.info 98 | } 99 | fn unpack(&mut self) -> Result { 100 | let work_dir = make_unpack_work_dir(&self.info)?; 101 | 102 | Exec::cmd(&self.pkgtrans) 103 | .arg(&self.info.file) 104 | .arg(&work_dir) 105 | .arg(&self.pkgname) 106 | .log_and_spawn(None)?; 107 | 108 | let mut work_dir_1 = work_dir.clone().into_os_string(); 109 | work_dir_1.push("_1"); 110 | 111 | fs_extra::dir::move_dir(&self.pkg_dir, &work_dir_1, &CopyOptions::default())?; 112 | std::fs::remove_dir(&work_dir)?; 113 | fs_extra::dir::move_dir(&work_dir_1, &work_dir, &CopyOptions::default())?; 114 | 115 | Ok(work_dir) 116 | } 117 | } 118 | 119 | struct PkgReader { 120 | file: PathBuf, 121 | pkg_dir: PathBuf, 122 | pkgname: String, 123 | } 124 | impl PkgReader { 125 | pub fn new(file: PathBuf, pkginfo: &Path, pkgtrans: &Path) -> Result { 126 | let tdir = tempfile::tempdir()?.into_path(); 127 | 128 | let pkginfo = Exec::cmd(pkginfo) 129 | .arg("-d") 130 | .arg(&file) 131 | .log_and_output(None)? 132 | .stdout_str(); 133 | let Some(pkgname) = pkginfo.lines().next() else { 134 | bail!("Received empty output from pkginfo"); 135 | }; 136 | let pkgname = pkgname 137 | .trim_start_matches(|c: char| !c.is_whitespace()) 138 | .trim_start() 139 | .trim_end() 140 | .to_owned(); 141 | 142 | Exec::cmd(pkgtrans) 143 | .arg("-i") 144 | .arg(&file) 145 | .arg(&tdir) 146 | .arg(&pkgname) 147 | .log_and_spawn(None) 148 | .wrap_err("Error running pkgtrans")?; 149 | 150 | Ok(Self { 151 | file, 152 | pkg_dir: tdir.join(&pkgname), 153 | pkgname, 154 | }) 155 | } 156 | fn read_pkg_info(&mut self, info: &mut PackageInfo) -> Result<()> { 157 | let pkginfo = std::fs::read_to_string(&self.pkg_dir.join("pkginfo"))?; 158 | parse_pkg_info(info, &pkginfo) 159 | } 160 | fn read_pkg_map(&mut self, info: &mut PackageInfo) -> Result<()> { 161 | let pkgmap = std::fs::read_to_string(&self.pkg_dir.join("pkgmap"))?; 162 | parse_pkg_map(info, &pkgmap, &self.file) 163 | } 164 | fn read_copyright(&mut self) -> Result { 165 | self.pkg_dir.push("copyright"); 166 | 167 | let copyright = if self.pkg_dir.is_file() { 168 | let mut copyright = self.file.join("install"); 169 | copyright.push("copyright"); 170 | std::fs::read_to_string(©right)? 171 | } else { 172 | "unknown".into() 173 | }; 174 | 175 | self.pkg_dir.pop(); 176 | Ok(copyright) 177 | } 178 | 179 | fn cleanup(&mut self) -> Result<()> { 180 | self.pkg_dir.pop(); 181 | std::fs::remove_dir_all(&self.pkg_dir)?; 182 | Ok(()) 183 | } 184 | } 185 | 186 | fn parse_pkg_info(info: &mut PackageInfo, content: &str) -> Result<()> { 187 | // See https://docs.oracle.com/cd/E36784_01/html/E36882/pkginfo-4.html 188 | let mut info_map: HashMap<&str, &str> = HashMap::new(); 189 | let mut key = ""; 190 | for line in content.lines() { 191 | let value = if let Some((k, v)) = line.split_once('=') { 192 | key = k; 193 | v 194 | } else { 195 | line 196 | }; 197 | *info_map.entry(key).or_default() = value; 198 | } 199 | 200 | let Some(arch) = info_map.remove("ARCH") else { 201 | bail!("ARCH field missing in pkginfo!"); 202 | }; 203 | let Some(version) = info_map.remove("VERSION") else { 204 | bail!("VERSION field missing in pkginfo!"); 205 | }; 206 | 207 | info.arch = arch.trim_matches('"').to_owned(); 208 | info.version = version.trim_matches('"').to_owned(); 209 | info.description = info_map 210 | .remove("DESC") 211 | .map(|d| d.trim_matches('"').to_owned()) 212 | .unwrap_or_default(); 213 | 214 | Ok(()) 215 | } 216 | 217 | fn parse_pkg_map(info: &mut PackageInfo, content: &str, file: &Path) -> Result<()> { 218 | // See https://docs.oracle.com/cd/E36784_01/html/E36882/pkgmap-4.html 219 | 220 | // Skip the preamble line 221 | for f in content.lines().skip(1) { 222 | let mut split = f.split(' '); 223 | 224 | // TODO: allow other part numbers 225 | let Some("1") = split.next() else { 226 | continue; 227 | }; 228 | let Some(ftype) = split.next() else { 229 | continue; 230 | }; 231 | let Some(_) = split.next() else { 232 | continue; 233 | }; 234 | let Some(path) = split.next() else { 235 | continue; 236 | }; 237 | 238 | match ftype { 239 | "f" if path.starts_with("etc/") => { 240 | let mut buf = PathBuf::from("/"); 241 | buf.push(path); 242 | info.conffiles.push(buf); 243 | } 244 | "f" | "d" => info.files.push(PathBuf::from(path)), 245 | "i" => { 246 | let Some(script) = Script::from_pkg_script_name(path) else { 247 | continue; 248 | }; 249 | info.scripts 250 | .insert(script, std::fs::read_to_string(file.join(path))?); 251 | } 252 | _ => { /* TODO handle other ftypes */ } 253 | } 254 | } 255 | 256 | Ok(()) 257 | } 258 | 259 | #[cfg(test)] 260 | mod tests { 261 | use std::path::Path; 262 | 263 | #[test] 264 | fn test_parse_pkg_info() -> eyre::Result<()> { 265 | let mut info = crate::PackageInfo::default(); 266 | 267 | super::parse_pkg_info( 268 | &mut info, 269 | r#" 270 | SUNW_PRODNAME="SunOS" 271 | SUNW_PRODVERS="5.5" 272 | SUNW_PKGTYPE="usr" 273 | SUNW_PKG_ALLZONES=false 274 | SUNW_PKG_HOLLOW=false 275 | PKG="SUNWesu" 276 | NAME="Extended System Utilities" 277 | VERSION="11.5.1" 278 | ARCH="sparc" 279 | DESC="Have a nice Sun-day!" 280 | VENDOR="Sun Microsystems, Inc." 281 | HOTLINE="Please contact your local service provider" 282 | EMAIL="" 283 | VSTOCK="0122c3f5566" 284 | CATEGORY="system" 285 | ISTATES="S 2" 286 | RSTATES="S 2" 287 | "#, 288 | )?; 289 | 290 | assert_eq!(info.arch, "sparc"); 291 | assert_eq!(info.version, "11.5.1"); 292 | assert_eq!(info.description, "Have a nice Sun-day!"); 293 | 294 | Ok(()) 295 | } 296 | 297 | #[test] 298 | fn test_parse_pkg_map() -> eyre::Result<()> { 299 | let mut info = crate::PackageInfo::default(); 300 | 301 | super::parse_pkg_map( 302 | &mut info, 303 | r#" 304 | : 2 500 305 | 1 i pkginfo 237 1179 541296672 306 | 1 d none bin 0755 root bin 307 | 1 f none bin/INSTALL 0755 root bin 11103 17954 541295535 308 | 1 f none bin/REMOVE 0755 root bin 3214 50237 541295541 309 | 1 l none bin/UNINSTALL=bin/REMOVE 310 | 1 f none bin/cmda 0755 root bin 3580 60325 541295567 311 | 1 f none bin/cmdb 0755 root bin 49107 51255 541438368 312 | 1 f class1 bin/cmdc 0755 root bin 45599 26048 541295599 313 | 1 f class1 etc/cmdd 0755 root bin 4648 8473 541461238 314 | 1 f none etc/cmde 0755 root bin 40501 1264 541295622 315 | 1 f class2 etc/cmdf 0755 root bin 2345 35889 541295574 316 | 1 f none etc/cmdg 0755 root bin 41185 47653 541461242 317 | 2 d class2 data 0755 root bin 318 | 2 p class1 data/apipe 0755 root other 319 | 2 d none log 0755 root bin 320 | 2 v none log/logfile 0755 root bin 41815 47563 541461333 321 | 2 d none save 0755 root bin 322 | 2 d none spool 0755 root bin 323 | 2 d none tmp 0755 root bin 324 | "#, 325 | Path::new(""), 326 | )?; 327 | 328 | assert_eq!( 329 | info.files, 330 | vec![ 331 | Path::new("bin"), 332 | Path::new("bin/INSTALL"), 333 | Path::new("bin/REMOVE"), 334 | Path::new("bin/cmda"), 335 | Path::new("bin/cmdb"), 336 | Path::new("bin/cmdc"), 337 | ] 338 | ); 339 | assert_eq!( 340 | info.conffiles, 341 | vec![ 342 | Path::new("/etc/cmdd"), 343 | Path::new("/etc/cmde"), 344 | Path::new("/etc/cmdf"), 345 | Path::new("/etc/cmdg"), 346 | ] 347 | ); 348 | 349 | Ok(()) 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /src/pkg/target.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::Write, path::PathBuf}; 2 | 3 | use eyre::{Context, Result}; 4 | use subprocess::Exec; 5 | 6 | use crate::{ 7 | util::{chmod, mkdir, ExecExt}, 8 | PackageInfo, TargetPackage, 9 | }; 10 | 11 | #[derive(Debug)] 12 | pub struct PkgTarget { 13 | info: PackageInfo, 14 | unpacked_dir: PathBuf, 15 | converted_name: String, 16 | } 17 | impl PkgTarget { 18 | pub fn new(mut info: PackageInfo, mut unpacked_dir: PathBuf) -> Result { 19 | let pwd = std::env::current_dir()?; 20 | std::env::set_current_dir(&unpacked_dir)?; 21 | 22 | let mut file_list = String::new(); 23 | for file in glob::glob("**/*").unwrap() { 24 | let file = file?; 25 | let Some(name) = file.file_name() else { 26 | continue; 27 | }; 28 | let name = name.to_string_lossy(); 29 | if name != "prototype" { 30 | file_list.push_str(&name); 31 | file_list.push('\n'); 32 | } 33 | } 34 | 35 | let mut pkgproto = File::create("./prototype")?; 36 | Exec::cmd("pkgproto") 37 | .stdin(file_list.as_str()) 38 | .stdout(pkgproto.try_clone()?) 39 | .log_and_spawn(None)?; 40 | std::env::set_current_dir(pwd)?; 41 | 42 | let PackageInfo { 43 | name, 44 | arch, 45 | version, 46 | description, 47 | copyright, 48 | scripts, 49 | .. 50 | } = &mut info; 51 | let mut converted_name = name.clone(); 52 | Self::convert_name(&mut converted_name); 53 | 54 | unpacked_dir.push("pkginfo"); 55 | let mut pkginfo = File::create(&unpacked_dir)?; 56 | #[rustfmt::skip] 57 | writeln!( 58 | pkginfo, 59 | r#"PKG="{converted_name}" 60 | NAME="{name}" 61 | ARCH="{arch}" 62 | VERSION="{version}" 63 | CATEGORY="application" 64 | VENDOR="Xenomorph-converted package" 65 | EMAIL= 66 | PSTAMP=xenomorph 67 | MAXINST=1000 68 | BASEDIR="/" 69 | CLASSES="none" 70 | DESC="{description}" 71 | "#)?; 72 | unpacked_dir.pop(); 73 | writeln!(pkgproto, "i pkginfo=./pkginfo")?; 74 | 75 | unpacked_dir.push("install"); 76 | mkdir(&unpacked_dir)?; 77 | 78 | unpacked_dir.push("copyright"); 79 | std::fs::write(&unpacked_dir, copyright)?; 80 | writeln!(pkgproto, "i copyright=./install/copyright")?; 81 | unpacked_dir.pop(); 82 | 83 | for (script, data) in scripts { 84 | let name = script.pkg_script_name(); 85 | unpacked_dir.push(name); 86 | if !data.trim().is_empty() { 87 | std::fs::write(&unpacked_dir, data)?; 88 | chmod(&unpacked_dir, 0o755)?; 89 | writeln!(pkgproto, "i {name}={}", unpacked_dir.display())?; 90 | } 91 | unpacked_dir.pop(); 92 | } 93 | unpacked_dir.pop(); 94 | 95 | Ok(Self { 96 | info, 97 | unpacked_dir, 98 | converted_name, 99 | }) 100 | } 101 | 102 | fn convert_name(name: &mut String) { 103 | if name.starts_with("lib") { 104 | name.replace_range(.."lib".len(), "l"); 105 | } 106 | if name.ends_with("-perl") { 107 | let index = name.len() - "-perl".len(); 108 | name.replace_range(index.., "p"); 109 | } 110 | if name.starts_with("perl-") { 111 | name.replace_range(.."perl-".len(), "pl"); 112 | } 113 | } 114 | } 115 | impl TargetPackage for PkgTarget { 116 | fn build(&mut self) -> Result { 117 | Exec::cmd("pkgmk") 118 | .args(&["-r", "/", "-d", "."]) 119 | .cwd(&self.unpacked_dir) 120 | .log_and_spawn(None) 121 | .wrap_err("Error during pkgmk")?; 122 | let name = format!("{}-{}.pkg", self.info.name, self.info.version); 123 | 124 | Exec::cmd("pkgtrans") 125 | .arg(&self.unpacked_dir) 126 | .arg(&name) 127 | .arg(&self.converted_name) 128 | .log_and_spawn(None) 129 | .wrap_err("Error during pkgtrans")?; 130 | 131 | std::fs::rename(self.unpacked_dir.join(&name), &name)?; 132 | 133 | Ok(PathBuf::from(name)) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/rpm/mod.rs: -------------------------------------------------------------------------------- 1 | pub use source::RpmSource; 2 | pub use target::RpmTarget; 3 | 4 | use crate::util::{ExecExt, Verbosity}; 5 | use eyre::Result; 6 | use std::path::Path; 7 | use subprocess::Exec; 8 | 9 | pub mod source; 10 | pub mod target; 11 | 12 | pub fn install(rpm: &Path) -> Result<()> { 13 | let mut cmd = Exec::cmd("rpm").arg("-ivh"); 14 | 15 | if let Ok(args) = std::env::var("RPMINSTALLOPT") { 16 | for arg in args.split(' ') { 17 | cmd = cmd.arg(arg); 18 | } 19 | } 20 | 21 | cmd.arg(rpm).log_and_spawn(Verbosity::VeryVerbose) 22 | } 23 | -------------------------------------------------------------------------------- /src/rpm/source.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::{HashMap, HashSet}, 3 | path::{Component, Path, PathBuf}, 4 | }; 5 | 6 | use eyre::{bail, Context, Result}; 7 | use fs_extra::dir::CopyOptions; 8 | use nix::unistd::{chown, geteuid, Gid, Group, Uid, User}; 9 | use subprocess::{Exec, NullFile}; 10 | 11 | use crate::{ 12 | util::{chmod, make_unpack_work_dir, mkdir, ExecExt}, 13 | Args, {FileInfo, Format, PackageInfo, Script, SourcePackage}, 14 | }; 15 | 16 | #[derive(Debug)] 17 | pub struct RpmSource { 18 | info: PackageInfo, 19 | prefixes: Option, 20 | } 21 | impl RpmSource { 22 | #[must_use] 23 | pub fn check_file(file: &Path) -> bool { 24 | file.extension() 25 | .map_or(false, |o| o.eq_ignore_ascii_case("rpm")) 26 | } 27 | pub fn new(file: PathBuf, args: &Args) -> Result { 28 | let rpm = RpmReader::new(&file); 29 | 30 | let prefixes = rpm.query_field("%{PREFIXES}")?.map(PathBuf::from); 31 | 32 | let conffiles = rpm.query_file_list("-c")?; 33 | let files = rpm.query_file_list("-l")?; 34 | let binary_info = rpm.query("-i")?; 35 | 36 | // Sanity check and sanitize fields. 37 | 38 | let description = rpm.query_field("%{DESCRIPTION}")?; 39 | 40 | let summary = if let Some(summary) = rpm.query_field("%{SUMMARY}")? { 41 | summary 42 | } else { 43 | // Older rpms will have no summary, but will have a description. 44 | // We'll take the 1st line out of the description, and use it for the summary. 45 | let description = description.as_deref().unwrap_or_default(); 46 | let s = description.split_once('\n').map_or(description, |t| t.0); 47 | if s.is_empty() { 48 | // Fallback. 49 | "Converted RPM package".into() 50 | } else { 51 | s.to_owned() 52 | } 53 | }; 54 | 55 | let description = description.unwrap_or_else(|| summary.clone()); 56 | 57 | // Older rpms have no license tag, but have a copyright. 58 | let copyright = match rpm.query_field("%{LICENSE}")? { 59 | Some(o) => o, 60 | None => rpm 61 | .query_field("%{COPYRIGHT}")? 62 | .unwrap_or_else(|| "unknown".into()), 63 | }; 64 | 65 | let Some(name) = rpm.query_field("%{NAME}")? else { 66 | bail!("Error querying rpm file: name not found!") 67 | }; 68 | let Some(version) = rpm.query_field("%{VERSION}")? else { 69 | bail!("Error querying rpm file: version not found!") 70 | }; 71 | let Some(release) = rpm.query_field("%{RELEASE}")? else { 72 | bail!("Error querying rpm file: release not found!") 73 | }; 74 | 75 | let mut scripts = HashMap::new(); 76 | for script in Script::ALL { 77 | let field = rpm.query_field(script.rpm_query_key())?; 78 | scripts.insert(script, sanitize_script(&prefixes, field)); 79 | } 80 | 81 | let info = PackageInfo { 82 | name, 83 | version, 84 | release, 85 | arch: rpm.query_arch(args.target.as_deref())?, 86 | changelog: rpm.query_field("%{CHANGELOGTEXT}")?.unwrap_or_default(), 87 | summary, 88 | description, 89 | scripts, 90 | copyright, 91 | 92 | conffiles, 93 | files, 94 | binary_info, 95 | 96 | file, 97 | distribution: "Red Hat".into(), 98 | original_format: Format::Rpm, 99 | ..Default::default() 100 | }; 101 | 102 | Ok(Self { info, prefixes }) 103 | } 104 | } 105 | impl SourcePackage for RpmSource { 106 | fn info(&self) -> &PackageInfo { 107 | &self.info 108 | } 109 | fn info_mut(&mut self) -> &mut PackageInfo { 110 | &mut self.info 111 | } 112 | fn into_info(self) -> PackageInfo { 113 | self.info 114 | } 115 | 116 | fn unpack(&mut self) -> Result { 117 | let work_dir = make_unpack_work_dir(&self.info)?; 118 | 119 | let rpm2cpio = || Exec::cmd("rpm2cpio").arg(&self.info.file); 120 | 121 | // Check if we need to use lzma to uncompress the cpio archive 122 | let lzma = if let Ok(lzma) = which::which("lzma") { 123 | Exec::cmd(lzma) 124 | } else { 125 | // Some distros don't have `lzma` (such as mine, Fedora)! 126 | // `xz --format=lzma` would do just fine however 127 | Exec::cmd("xz").arg("--format=lzma") 128 | }; 129 | let cmd = (rpm2cpio() | lzma.arg("-tq")).stdout(NullFile); 130 | 131 | let decomp = if cmd.log_and_output_without_checking(None)?.success() { 132 | || Exec::cmd("lzma").arg("-dq") 133 | } else { 134 | || Exec::cmd("cat") 135 | }; 136 | 137 | let cpio = Exec::cmd("cpio").cwd(&work_dir).args(&[ 138 | "--extract", 139 | "--make-directories", 140 | "--no-absolute-filenames", 141 | "--preserve-modification-time", 142 | ]); 143 | 144 | (rpm2cpio() | decomp() | cpio) 145 | .log_and_spawn(None) 146 | .wrap_err_with(|| format!("Unpacking of {} failed", self.info.file.display()))?; 147 | 148 | // `cpio` does not necessarily store all parent directories in an archive, 149 | // and so some directories, if it has to make them and has no permission info, 150 | // will come out with some random permissions. 151 | // Find those directories and make them mode 755, which is more reasonable. 152 | 153 | let cpio = Exec::cmd("cpio").args(&["-it", "--quiet"]); 154 | let seen_files: HashSet<_> = (rpm2cpio() | decomp() | cpio) 155 | .log_and_output(None) 156 | .wrap_err_with(|| format!("File list of {} failed", self.info.file.display()))? 157 | .stdout_str() 158 | .lines() 159 | .map(PathBuf::from) 160 | .collect(); 161 | 162 | let cur_dir = std::env::current_dir()?; 163 | std::env::set_current_dir(&work_dir)?; 164 | // glob doesn't allow you to specify a cwd... annoying, but ok 165 | for file in glob::glob("**/*").unwrap() { 166 | let file = file?; 167 | let new_file = work_dir.join(&file); 168 | if !seen_files.contains(&file) && new_file.exists() && !new_file.is_symlink() { 169 | chmod(&new_file, 0o755)?; 170 | } 171 | } 172 | std::env::set_current_dir(cur_dir)?; 173 | 174 | // If the package is relocatable, we'd like to move it to be under the `self.prefixes` directory. 175 | // However, it's possible that that directory is in the package - it seems some rpm's are marked 176 | // as relocatable and unpack already in the directory they can relocate to, while some are marked 177 | // relocatable and the directory they can relocate to is removed from all filenames in the package. 178 | // I suppose this is due to some change between versions of rpm, but none of this is adequately documented, 179 | // so we'll just muddle through. 180 | 181 | if let Some(prefixes) = &self.prefixes { 182 | let w_prefixes = work_dir.join(prefixes); 183 | if !w_prefixes.exists() { 184 | let mut relocate = true; 185 | 186 | // Get the files to move. 187 | let pattern = work_dir.join("*"); 188 | let file_list: Vec<_> = glob::glob(&pattern.to_string_lossy()) 189 | .unwrap() 190 | .filter_map(|p| p.ok()) 191 | .collect(); 192 | 193 | // Now, make the destination directory. 194 | let mut dest = PathBuf::new(); 195 | 196 | for comp in prefixes.components() { 197 | if comp == Component::CurDir { 198 | dest.push("/"); 199 | } 200 | dest.push(comp); 201 | 202 | if dest.is_dir() { 203 | // The package contains a parent directory of the relocation directory. 204 | // Since it's impossible to move a parent directory into its child, 205 | // bail out and do nothing. 206 | relocate = false; 207 | break; 208 | } 209 | mkdir(&dest)?; 210 | } 211 | 212 | if relocate { 213 | // Now move all files in the package to the directory we made. 214 | if !file_list.is_empty() { 215 | fs_extra::move_items(&file_list, &w_prefixes, &CopyOptions::new())?; 216 | } 217 | 218 | self.info.conffiles = self 219 | .info 220 | .conffiles 221 | .iter() 222 | .map(|f| prefixes.join(f)) 223 | .collect(); 224 | } 225 | } 226 | } 227 | 228 | // `rpm` files have two sets of permissions; the set in the cpio archive, 229 | // and the set in the control data, which override the set in the archive. 230 | // The set in the control data are more correct, so let's use those. 231 | // Some permissions setting may have to be postponed until the postinst. 232 | 233 | let out = Exec::cmd("rpm") 234 | .args(&[ 235 | "--queryformat", 236 | r#"[%{FILEMODES} %{FILEUSERNAME} %{FILEGROUPNAME} %{FILENAMES}\n]"#, 237 | "-qp", 238 | ]) 239 | .arg(&self.info.file) 240 | .log_and_output(None)? 241 | .stdout_str(); 242 | 243 | let mut owninfo: HashMap = HashMap::new(); 244 | 245 | for line in out.lines() { 246 | let mut line = line.split(' '); 247 | let Some(mode) = line.next() else { continue; }; 248 | let Some(owner) = line.next() else { continue; }; 249 | let Some(group) = line.next() else { continue; }; 250 | let Some(file) = line.next() else { continue; }; 251 | 252 | let mut mode: u32 = mode.parse()?; 253 | mode &= 0o7777; // remove filetype 254 | 255 | let file = PathBuf::from(file); 256 | let file_info = owninfo.entry(file.clone()).or_default(); 257 | 258 | // TODO: this is not gonna work on windows, is it 259 | let user_id = match User::from_name(owner)? { 260 | Some(User { uid, .. }) if uid.is_root() => uid, 261 | _ => { 262 | file_info.owner = owner.to_owned(); 263 | Uid::from_raw(0) 264 | } 265 | }; 266 | let group_id = match Group::from_name(group)? { 267 | Some(Group { gid, .. }) if gid.as_raw() == 0 => gid, 268 | _ => { 269 | file_info.owner.push(':'); 270 | file_info.owner.push_str(group); 271 | Gid::from_raw(0) 272 | } 273 | }; 274 | 275 | // If this is a `setuid` file 276 | if !file_info.owner.is_empty() && mode & 0o7000 > 0 { 277 | file_info.mode = Some(mode); 278 | } 279 | 280 | // Note that ghost files exist in the metadata but not in the cpio archive, 281 | // so check that the file exists before trying to access it. 282 | let file = work_dir.join(file); 283 | if file.exists() { 284 | if geteuid().is_root() { 285 | chown(&file, Some(user_id), Some(group_id)).wrap_err_with(|| { 286 | format!("failed chowning {} to {user_id}:{group_id}", file.display()) 287 | })?; 288 | } 289 | chmod(&file, mode) 290 | .wrap_err_with(|| format!("failed chowning {} to {mode}", file.display()))?; 291 | } 292 | } 293 | self.info.file_info = owninfo; 294 | Ok(work_dir) 295 | } 296 | } 297 | 298 | //= Utilities 299 | pub trait QueryModifier { 300 | fn modify_query(self, exec: Exec) -> Exec; 301 | } 302 | 303 | impl QueryModifier for &'_ str { 304 | fn modify_query(self, exec: Exec) -> Exec { 305 | exec.arg(self) 306 | } 307 | } 308 | 309 | impl QueryModifier for F 310 | where 311 | F: FnOnce(Exec) -> Exec, 312 | { 313 | fn modify_query(self, exec: Exec) -> Exec { 314 | self(exec) 315 | } 316 | } 317 | 318 | pub(crate) struct RpmReader<'r> { 319 | file: &'r Path, 320 | } 321 | impl<'r> RpmReader<'r> { 322 | pub fn new(file: &'r Path) -> Self { 323 | Self { file } 324 | } 325 | pub fn query(&self, flag: &str) -> Result { 326 | self.query_with(|e| e.arg(flag)) 327 | } 328 | pub fn query_with(&self, modifier: impl FnOnce(Exec) -> Exec) -> Result { 329 | let exec = Exec::cmd("rpm").env("LANG", "C").arg("-qp"); 330 | let exec = modifier(exec); 331 | 332 | Ok(exec.arg(self.file).log_and_output(None)?.stdout_str()) 333 | } 334 | pub fn query_file_list(&self, flag: &str) -> Result> { 335 | let mut files: Vec<_> = self 336 | .query(flag)? 337 | .lines() 338 | .map(|s| PathBuf::from(s.trim())) 339 | .collect(); 340 | if let Some(f) = files.first() { 341 | if f.as_os_str() == "(contains no files)" { 342 | files.clear(); 343 | } 344 | } 345 | Ok(files) 346 | } 347 | pub fn query_field(&self, name: &str) -> Result> { 348 | let res = self.query_with(|e| e.arg("--queryformat").arg(name))?; 349 | 350 | Ok(if res == "(none)" { None } else { Some(res) }) 351 | } 352 | pub fn query_arch(&self, target: Option<&str>) -> Result { 353 | if let Some(arch) = target { 354 | Ok(Self::map_arch(arch).to_owned()) 355 | } else { 356 | let arch = self.query_field("%{ARCH}")?.unwrap_or_default(); 357 | Ok(Self::map_arch(&arch).to_owned()) 358 | } 359 | } 360 | pub fn map_arch(arch: &str) -> &str { 361 | match arch.as_bytes() { 362 | // NOTE(pluie): do NOT ask me where these numbers came from. 363 | // I have NO clue. 364 | b"1" => "i386", 365 | b"2" => "alpha", 366 | b"3" => "sparc", 367 | b"6" => "m68k", 368 | b"noarch" => "all", 369 | b"ppc" => "powerpc", 370 | b"x86_64" | b"em64t" => "amd64", 371 | b"armv4l" => "arm", 372 | b"armv7l" => "armel", 373 | b"parisc" => "hppa", 374 | b"ppc64le" => "ppc64el", 375 | 376 | // Treat 486, 586, etc, and Pentium, as 386. 377 | o if o.eq_ignore_ascii_case(b"pentium") => "i386", 378 | &[b'i' | b'I', b'0'..=b'9', b'8', b'6'] => "i386", 379 | 380 | _ => arch, 381 | } 382 | } 383 | } 384 | 385 | // rpm maintainer scripts are typically shell scripts, 386 | // but often lack the leading shebang line. 387 | // This can confuse dpkg, so add the shebang if it looks like 388 | // there is no shebang magic already in place. 389 | // 390 | // Additionally, it's not uncommon for rpm maintainer scripts to 391 | // contain bashisms, which can be triggered when they are run on 392 | // systems where /bin/sh is not bash. To work around this, 393 | // the shebang line of the scripts is changed to use bash. 394 | // 395 | // Also if the rpm is relocatable, the script could refer to 396 | // RPM_INSTALL_PREFIX, which is to set by rpm at runtime. 397 | // Deal with this by adding code to the script to set RPM_INSTALL_PREFIX. 398 | fn sanitize_script(prefixes: &Option, s: Option) -> String { 399 | let prefix_code = prefixes 400 | .as_ref() 401 | .map(|p| { 402 | format!( 403 | "\nRPM_INSTALL_PREFIX={}\nexport RPM_INSTALL_PREFIX", 404 | p.display() 405 | ) 406 | }) 407 | .unwrap_or_default(); 408 | 409 | if let Some(t) = &s { 410 | if let Some(t) = t.strip_prefix("#!") { 411 | let t = t.trim_start(); 412 | if t.starts_with('/') { 413 | let mut t = t.replacen("/bin/sh", "#!/bin/bash", 1); 414 | if let Some(nl) = t.find('\n') { 415 | t.insert_str(nl, &prefix_code); 416 | } 417 | return t; 418 | } 419 | } 420 | } 421 | format!("#!/bin/bash\n{prefix_code}{}", s.unwrap_or_default()) 422 | } 423 | -------------------------------------------------------------------------------- /src/rpm/target.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::Write as _, 3 | fs::File, 4 | io::Write, 5 | path::{Path, PathBuf}, 6 | }; 7 | 8 | use base64::Engine; 9 | use eyre::{bail, Result}; 10 | use subprocess::{Exec, Redirection}; 11 | 12 | use crate::{util::ExecExt, PackageInfo, Script, TargetPackage}; 13 | 14 | #[derive(Debug)] 15 | pub struct RpmTarget { 16 | pub(crate) info: PackageInfo, 17 | unpacked_dir: PathBuf, 18 | spec: PathBuf, 19 | } 20 | impl RpmTarget { 21 | pub fn new(mut info: PackageInfo, unpacked_dir: PathBuf) -> Result { 22 | Self::sanitize_info(&mut info); 23 | 24 | let mut file_list = String::new(); 25 | for filename in &info.files { 26 | // DIFFERENCE WITH THE PERL VERSION: 27 | // `snailquote` doesn't escape the same characters as Perl, but that difference 28 | // is negligible at best - feel free to implement Perl-style escaping if you want to. 29 | // The list of escape sequences is in `perlop`. 30 | 31 | // Unquote any escaped characters in filenames - needed for non ascii characters. 32 | // (eg. iso_8859-1 latin set) 33 | let unquoted = snailquote::unescape(&filename.to_string_lossy())?; 34 | 35 | if unquoted.ends_with('/') { 36 | file_list.push_str("%dir "); 37 | } else if info 38 | .conffiles 39 | .iter() 40 | .any(|f| f.as_os_str() == unquoted.as_str()) 41 | { 42 | // it's a conffile 43 | file_list.push_str("%config "); 44 | } 45 | // Note all filenames are quoted in case they contain spaces. 46 | writeln!(file_list, r#""{unquoted}""#)?; 47 | } 48 | 49 | let PackageInfo { 50 | name, 51 | version, 52 | release, 53 | dependencies: depends, 54 | summary, 55 | copyright, 56 | distribution, 57 | group, 58 | use_scripts, 59 | scripts, 60 | description, 61 | original_format, 62 | .. 63 | } = &info; 64 | 65 | let spec = PathBuf::from(format!( 66 | "{}/{name}-{version}-{release}.spec", 67 | unpacked_dir.display() 68 | )); 69 | let mut spec_file = File::create(&spec)?; 70 | 71 | let mut build_root = std::env::current_dir()?; 72 | build_root.push(&unpacked_dir); 73 | 74 | #[rustfmt::skip] 75 | write!( 76 | spec_file, 77 | r#"Buildroot: {build_root} 78 | Name: {name} 79 | Version: {version} 80 | Release: {release} 81 | "#, 82 | build_root = build_root.display(), 83 | )?; 84 | 85 | if let [first, rest @ ..] = &depends[..] { 86 | write!(spec_file, "Requires: {first}",)?; 87 | for dep in rest { 88 | write!(spec_file, ", {dep}")?; 89 | } 90 | writeln!(spec_file)?; 91 | } 92 | 93 | #[rustfmt::skip] 94 | write!( 95 | spec_file, 96 | r#"Summary: {summary} 97 | License: {copyright} 98 | Distribution: {distribution} 99 | Group: Converted/{group} 100 | 101 | %define _rpmdir ../ 102 | %define _rpmfilename %%{{NAME}}-%%{{VERSION}}-%%{{RELEASE}}.%%{{ARCH}}.rpm 103 | %define _unpackaged_files_terminate_build 0 104 | 105 | "#, 106 | )?; 107 | 108 | if *use_scripts { 109 | for script in Script::ALL { 110 | let name = script.rpm_scriptlet_name(); 111 | let Some(script) = scripts.get(&script) else { 112 | continue; 113 | }; 114 | write!(spec_file, "{name}\n{script}\n\n")?; 115 | } 116 | } 117 | #[rustfmt::skip] 118 | write!( 119 | spec_file, 120 | r#"%description 121 | {description} 122 | 123 | (Converted from a {original_format} package by `xenomorph` version {xenomorph_version}.) 124 | 125 | %files 126 | {file_list}"#, 127 | xenomorph_version = env!("CARGO_PKG_VERSION") 128 | )?; 129 | 130 | Ok(Self { 131 | info, 132 | unpacked_dir, 133 | spec, 134 | }) 135 | } 136 | 137 | pub(crate) fn build_with(&mut self, cmd: &Path) -> Result { 138 | let rpmdir = Exec::cmd("rpm") 139 | .arg("--showrc") 140 | .log_and_output(None)? 141 | .stdout_str() 142 | .lines() 143 | .find_map(|l| { 144 | if let Some(l) = l.strip_prefix("rpmdir") { 145 | let path = l.trim_start().trim_start_matches(':').trim_start(); 146 | Some(PathBuf::from(path)) 147 | } else { 148 | None 149 | } 150 | }); 151 | 152 | let PackageInfo { 153 | name, 154 | version, 155 | release, 156 | arch, 157 | .. 158 | } = &self.info; 159 | 160 | let rpm = format!("{name}-{version}-{release}.{arch}.rpm"); 161 | 162 | let (rpm, arch_flag) = if let Some(rpmdir) = rpmdir { 163 | // Old versions of rpm toss it off in te middle of nowhere. 164 | let mut r = rpmdir.join(arch); 165 | r.push(&rpm); 166 | (r, "--buildarch") 167 | } else { 168 | // Presumably we're dealing with rpm 3.0 or above, which doesn't 169 | // output rpmdir in any format I'd care to try to parse. 170 | // Instead, rpm is now of a late enough version to notice the 171 | // %define's in the spec file, which will make the file end up 172 | // in the directory we started in. 173 | // Anyway, let's assume this is version 3 or above. 174 | 175 | // This is the new command line argument to set the arch rpms. 176 | // It appeared in rpm version 3. 177 | (PathBuf::from(rpm), "--target") 178 | }; 179 | 180 | let mut build_root = std::env::current_dir()?; 181 | build_root.push(&self.unpacked_dir); 182 | 183 | let mut cmd = Exec::cmd(cmd) 184 | .cwd(&self.unpacked_dir) 185 | .stderr(Redirection::Merge) 186 | .arg("--buildroot") 187 | .arg(build_root) 188 | .arg("-bb") 189 | .arg(arch_flag) 190 | .arg(arch); 191 | 192 | if let Ok(opt) = std::env::var("RPMBUILDOPT") { 193 | let opt: Vec<_> = opt.split(' ').collect(); 194 | cmd = cmd.args(&opt); 195 | } 196 | 197 | cmd = cmd.arg(format!("{name}-{version}-{release}.spec")); 198 | 199 | let cmdline = cmd.to_cmdline_lossy(); 200 | let out = cmd.log_and_output_without_checking(None)?; 201 | 202 | if !out.success() { 203 | bail!( 204 | "Package build failed. Here's the log of the command ({cmdline}):\n{}", 205 | out.stdout_str() 206 | ); 207 | } 208 | 209 | Ok(rpm) 210 | } 211 | 212 | fn sanitize_info(info: &mut PackageInfo) { 213 | // When retrieving scripts for building, we have to do some truly sick mangling. 214 | // Since debian/slackware scripts can be anything -- perl programs or binary files -- 215 | // and rpm is limited to only shell scripts, we need to encode the files and add a 216 | // scrap of shell script to make it unextract and run on the fly. 217 | 218 | for script in Script::ALL { 219 | let Some(script) = info.scripts.get_mut(&script) else { 220 | continue; 221 | }; 222 | 223 | if script.chars().all(char::is_whitespace) { 224 | continue; // it's blank. 225 | } 226 | 227 | if let Some(s) = script.strip_prefix("#!") { 228 | if s.trim_start().starts_with("/bin/sh") { 229 | continue; // looks like a shell script already 230 | } 231 | } 232 | // The original used uuencoding. That is cursed. We don't do that here 233 | let encoded = base64::engine::general_purpose::STANDARD.encode(&script); 234 | 235 | #[rustfmt::skip] 236 | let patched = format!( 237 | r#"#!/bin/sh 238 | set -e 239 | mkdir /tmp/xenomorph.$$ 240 | echo '{encoded}' | base64 -d > /tmp/xenomorph.$$/script 241 | chmod 755 /tmp/xenomorph.$$/script 242 | /tmp/xenomorph.$$/script "$@" 243 | rm -f /tmp/xenomorph.$$/script 244 | rmdir /tmp/xenomorph.$$ 245 | "# 246 | ); 247 | *script = patched; 248 | } 249 | 250 | info.version = info.version.replace('-', "_"); 251 | 252 | let arch = match info.arch.as_str() { 253 | "amd64" => Some("x86_64"), 254 | "powerpc" => Some("ppc"), // XXX is this the canonical name for powerpc on rpm systems? 255 | "hppa" => Some("parisc"), 256 | "all" => Some("noarch"), 257 | "ppc64el" => Some("ppc64le"), 258 | _ => None, 259 | }; 260 | if let Some(arch) = arch { 261 | info.arch = arch.to_owned(); 262 | } 263 | } 264 | } 265 | 266 | impl TargetPackage for RpmTarget { 267 | fn clean_tree(&mut self) -> Result<()> { 268 | let _ignore = std::fs::remove_file(&self.spec); 269 | Ok(()) 270 | } 271 | fn build(&mut self) -> Result { 272 | self.build_with(Path::new("rpmbuild")) 273 | } 274 | } 275 | -------------------------------------------------------------------------------- /src/tgz/mod.rs: -------------------------------------------------------------------------------- 1 | pub use source::TgzSource; 2 | pub use target::TgzTarget; 3 | 4 | use crate::util::{ExecExt, Verbosity}; 5 | use eyre::{bail, Context, Result}; 6 | use std::path::Path; 7 | use subprocess::Exec; 8 | 9 | pub mod source; 10 | pub mod target; 11 | 12 | /// Install a tgz with installpkg. Pass in the filename of the tgz to install. 13 | /// 14 | /// installpkg (a slackware program) is used because I'm not sanguine about 15 | /// just untarring a tgz file — it might trash a system. 16 | pub fn install(tgz: &Path) -> Result<()> { 17 | if Path::new("/sbin/installpkg").exists() { 18 | Exec::cmd("/sbin/installpkg") 19 | .arg(tgz) 20 | .log_and_spawn(Verbosity::VeryVerbose) 21 | .wrap_err("Unable to install") 22 | } else { 23 | bail!("Sorry, I cannot install the generated .tgz file because /sbin/installpkg is not present. You can use tar to install it yourself.") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/tgz/source.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | fmt::Debug, 4 | fs::File, 5 | io::{Read, Seek}, 6 | path::{Path, PathBuf}, 7 | }; 8 | 9 | use eyre::Result; 10 | use subprocess::Exec; 11 | 12 | use crate::{ 13 | util::{make_unpack_work_dir, ExecExt}, 14 | Format, PackageInfo, Script, SourcePackage, 15 | }; 16 | 17 | pub struct TgzSource { 18 | info: PackageInfo, 19 | tar: tar::Archive, 20 | } 21 | impl TgzSource { 22 | #[must_use] 23 | pub fn check_file(file: &Path) -> bool { 24 | let Some(f) = file.file_name() else { return false; }; 25 | let f = f.to_string_lossy(); 26 | 27 | let Some((rest, ext)) = f.rsplit_once('.') else { return false; }; 28 | let ext = ext.to_ascii_lowercase(); 29 | 30 | match ext.as_str() { 31 | "tgz" | "taz" => true, 32 | "gz" | "z" | "bz" | "bz2" => { 33 | if let Some((_, ext2)) = rest.rsplit_once('.') { 34 | ext2.eq_ignore_ascii_case("tar") 35 | } else { 36 | false 37 | } 38 | } 39 | _ => false, 40 | } 41 | } 42 | pub fn new(file: PathBuf) -> Result { 43 | let mut basename = if let Some(file_name) = file.file_name() { 44 | PathBuf::from(file_name) 45 | } else { 46 | file.clone() 47 | }; 48 | basename.set_extension(""); 49 | let basename = basename.to_string_lossy(); 50 | 51 | let (name, version) = basename.rsplit_once('-').unwrap_or((&basename, "1")); 52 | let (name, version) = (name.to_owned(), version.to_owned()); 53 | 54 | let binary_info = Exec::cmd("ls") 55 | .arg("-l") 56 | .arg(&file) 57 | .log_and_output(None)? 58 | .stdout_str(); 59 | 60 | let mut conffiles = vec![]; 61 | let mut files = vec![]; 62 | let mut scripts = HashMap::new(); 63 | 64 | let mut tar = tar::Archive::new(File::open(&file)?); 65 | for entry in tar.entries()? { 66 | let mut entry = entry?; 67 | let header = entry.header(); 68 | let mut path = PathBuf::from("/"); 69 | path.push(header.path()?); 70 | 71 | // Assume any regular file (non-directory) in /etc/ is a conffile. 72 | if path.starts_with("/etc/") && header.mode()? & 0o1000 == 0 { 73 | // If entry is just a regular file and not a directory 74 | 75 | conffiles.push(path.clone()); 76 | } else if path.starts_with("/install/") { 77 | // It might be a script! 78 | 79 | let Some(name) = path.file_name() else { continue; }; 80 | let name = name.to_string_lossy(); 81 | let Some(script) = Script::from_tgz_script_name(&name) else { continue; }; 82 | 83 | let mut content = String::new(); 84 | entry.read_to_string(&mut content)?; 85 | scripts.insert(script, content); 86 | } else { 87 | // Regular old file 88 | files.push(path); 89 | } 90 | } 91 | 92 | let info = PackageInfo { 93 | file, 94 | name, 95 | version, 96 | release: "1".into(), 97 | arch: "all".into(), 98 | group: "unknown".into(), 99 | summary: "Converted tgz package".into(), 100 | description: "Converted tgz package".into(), 101 | copyright: "unknown".into(), 102 | original_format: Format::Tgz, 103 | distribution: "Slackware/tarball".into(), 104 | binary_info, 105 | conffiles, 106 | files, 107 | scripts, 108 | ..Default::default() 109 | }; 110 | 111 | // Rewind tar to 112 | let mut tar = tar.into_inner(); 113 | tar.rewind()?; 114 | let tar = tar::Archive::new(tar); 115 | 116 | Ok(Self { info, tar }) 117 | } 118 | } 119 | impl SourcePackage for TgzSource { 120 | fn info(&self) -> &PackageInfo { 121 | &self.info 122 | } 123 | fn info_mut(&mut self) -> &mut PackageInfo { 124 | &mut self.info 125 | } 126 | fn into_info(self) -> PackageInfo { 127 | self.info 128 | } 129 | fn unpack(&mut self) -> Result { 130 | let work_dir = make_unpack_work_dir(&self.info)?; 131 | 132 | self.tar.unpack(&work_dir)?; 133 | 134 | // Delete the install directory that has slackware info in it. 135 | std::fs::remove_dir_all(work_dir.join("install"))?; 136 | 137 | Ok(work_dir) 138 | } 139 | } 140 | impl Debug for TgzSource { 141 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 142 | f.debug_struct("TgzSource") 143 | .field("info", &self.info) 144 | .finish() 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/tgz/target.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, path::PathBuf}; 2 | 3 | use eyre::Result; 4 | 5 | use crate::{ 6 | util::{chmod, mkdir}, 7 | PackageInfo, TargetPackage, 8 | }; 9 | 10 | #[derive(Debug)] 11 | pub struct TgzTarget { 12 | info: PackageInfo, 13 | unpacked_dir: PathBuf, 14 | } 15 | impl TgzTarget { 16 | pub fn new(info: PackageInfo, unpacked_dir: PathBuf) -> Result { 17 | if info.use_scripts { 18 | let mut out = unpacked_dir.join("install"); 19 | let mut created_install_folder = false; 20 | 21 | for (script, data) in &info.scripts { 22 | if data.chars().all(char::is_whitespace) { 23 | continue; 24 | } 25 | 26 | if !created_install_folder { 27 | mkdir(&out)?; 28 | chmod(&out, 0o755)?; 29 | created_install_folder = true; 30 | } 31 | out.push(script.tgz_script_name()); 32 | 33 | std::fs::write(&out, data)?; 34 | chmod(&out, 0o755)?; 35 | } 36 | } 37 | 38 | Ok(Self { info, unpacked_dir }) 39 | } 40 | } 41 | impl TargetPackage for TgzTarget { 42 | fn build(&mut self) -> Result { 43 | let path = format!("{}-{}.tgz", self.info.name, self.info.version); 44 | let path = PathBuf::from(path); 45 | 46 | let mut tgz = tar::Builder::new(File::create(&path)?); 47 | tgz.append_dir_all(".", &self.unpacked_dir)?; 48 | 49 | Ok(path) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use bpaf::{construct, long, Parser}; 4 | use enumflags2::BitFlags; 5 | use eyre::{bail, Context, Result}; 6 | use subprocess::{CaptureData, Exec, NullFile, Pipeline, Redirection}; 7 | 8 | use crate::{Format, PackageInfo}; 9 | 10 | use std::{ 11 | os::unix::prelude::PermissionsExt, 12 | path::{Path, PathBuf}, 13 | sync::OnceLock, 14 | }; 15 | 16 | #[allow(clippy::struct_excessive_bools)] 17 | #[derive(bpaf::Bpaf, Debug)] 18 | pub struct Args { 19 | #[bpaf(external)] 20 | pub formats: BitFlags, 21 | 22 | #[bpaf(external, group_help("deb-specific options:"))] 23 | pub deb_args: DebArgs, 24 | 25 | #[bpaf(external, group_help("tgz-specific options:"))] 26 | pub tgz_args: TgzArgs, 27 | 28 | /// Install generated package. 29 | #[bpaf(short, long, group_help(""))] // have to forcibly break the group for some reason 30 | pub install: bool, 31 | 32 | /// Generate build tree, but do not build package. 33 | #[bpaf(short, long)] 34 | pub generate: bool, 35 | 36 | /// Include scripts in package. 37 | #[bpaf(short('c'), long)] 38 | pub scripts: bool, 39 | 40 | /// Set architecture of the generated package. 41 | #[bpaf(argument("arch"))] 42 | pub target: Option, 43 | 44 | /// Display each command xenomorph runs. 45 | #[bpaf(external)] 46 | pub verbosity: Verbosity, 47 | 48 | /// Do not change version of generated package. 49 | #[bpaf(short, long)] 50 | pub keep_version: bool, 51 | 52 | /// Increment package version by this number. 53 | #[bpaf(argument("number"), fallback(1))] 54 | pub bump: u32, 55 | 56 | /// Package file or files to convert. 57 | #[bpaf(positional("FILES"), some("You must specify a file to convert."))] 58 | pub files: Vec, 59 | } 60 | #[derive(Debug, bpaf::Bpaf)] 61 | pub struct DebArgs { 62 | /// Specify patch file to use instead of automatically looking for patch 63 | /// in /var/lib/xenomorph. 64 | #[bpaf( 65 | argument("patch"), 66 | guard(patch_file_exists, "Specified patch file cannot be found") 67 | )] 68 | pub patch: Option, 69 | /// Do not use patches. 70 | pub nopatch: bool, 71 | /// Use even old version os patches. 72 | pub anypatch: bool, 73 | /// Like --generate, but do not create .orig directory. 74 | #[bpaf(short, long)] 75 | pub single: bool, 76 | /// Munge/fix permissions and owners. 77 | pub fixperms: bool, 78 | /// Test generated packages with lintian. 79 | pub test: bool, 80 | } 81 | 82 | #[derive(Debug, bpaf::Bpaf)] 83 | pub struct TgzArgs { 84 | /// Specify package description. 85 | #[bpaf(argument("desc"))] 86 | pub description: Option, 87 | 88 | #[bpaf(argument("version"))] 89 | /// Specify package version. 90 | pub version: Option, 91 | } 92 | 93 | fn formats() -> impl Parser> { 94 | let to_deb = long("to-deb") 95 | .short('d') 96 | .help("Generate a Debian deb package (default).") 97 | .flag(BitFlags::from(Format::Deb), BitFlags::empty()); 98 | let to_rpm = long("to-rpm") 99 | .short('r') 100 | .help("Generate a Red Hat rpm package.") 101 | .flag(BitFlags::from(Format::Rpm), BitFlags::empty()); 102 | let to_lsb = long("to-lsb") 103 | .short('l') 104 | .help("Generate a LSB package.") 105 | .flag(BitFlags::from(Format::Lsb), BitFlags::empty()); 106 | let to_tgz = long("to-tgz") 107 | .short('t') 108 | .help("Generate a Slackware tgz package.") 109 | .flag(BitFlags::from(Format::Tgz), BitFlags::empty()); 110 | let to_pkg = long("to-pkg") 111 | .short('p') 112 | .help("Generate a Solaris pkg package.") 113 | .flag(BitFlags::from(Format::Pkg), BitFlags::empty()); 114 | 115 | construct!(to_deb, to_rpm, to_lsb, to_tgz, to_pkg,).map(|(d, r, l, t, p)| { 116 | let mut formats = d | r | l | t | p; 117 | if formats.is_empty() { 118 | // Default to deb 119 | formats |= Format::Deb; 120 | } 121 | formats 122 | }) 123 | } 124 | 125 | fn patch_file_exists(s: &Option) -> bool { 126 | s.as_ref().map_or(true, |s| s.exists()) 127 | } 128 | 129 | fn verbosity() -> impl Parser { 130 | let verbose = long("verbose") 131 | .short('v') 132 | .help("Display each command `xenomorph` runs.") 133 | .switch(); 134 | let very_verbose = long("veryverbose") 135 | .help("Be verbose, and also display output of run commands.") 136 | .switch(); 137 | 138 | construct!(verbose, very_verbose).map(|(v, vv)| { 139 | if vv { 140 | Verbosity::VeryVerbose 141 | } else if v { 142 | Verbosity::Verbose 143 | } else { 144 | Verbosity::Normal 145 | } 146 | }) 147 | } 148 | 149 | #[derive(Debug, Clone, Copy, PartialEq)] 150 | pub enum Verbosity { 151 | Normal, 152 | Verbose, 153 | VeryVerbose, 154 | } 155 | impl Verbosity { 156 | pub fn set(self) { 157 | VERBOSITY.set(self).unwrap(); 158 | } 159 | pub fn get() -> Verbosity { 160 | *VERBOSITY.get().unwrap() 161 | } 162 | } 163 | static VERBOSITY: OnceLock = OnceLock::new(); 164 | 165 | pub(crate) trait ExecExt { 166 | type Output; 167 | 168 | fn log_and_spawn(self, verbosity: impl Into>) -> Result<()>; 169 | 170 | #[must_use = "Use `log_and_spawn` if you just want to spawn a command and forget about it"] 171 | fn log_and_output(self, verbosity: impl Into>) -> Result; 172 | 173 | #[must_use = "Use `log_and_spawn` if you just want to spawn a command and forget about it"] 174 | fn log_and_output_without_checking( 175 | self, 176 | verbosity: impl Into>, 177 | ) -> Result; 178 | } 179 | impl ExecExt for Exec { 180 | type Output = CaptureData; 181 | 182 | fn log_and_spawn(mut self, verbosity: impl Into>) -> Result<()> { 183 | let verbosity = verbosity.into().unwrap_or_else(Verbosity::get); 184 | let cmdline = self.to_cmdline_lossy(); 185 | if verbosity != Verbosity::Normal { 186 | println!("\t{cmdline}"); 187 | } 188 | if verbosity != Verbosity::VeryVerbose { 189 | self = self.stdout(NullFile); 190 | } 191 | let capture = self.capture()?; 192 | if !capture.success() { 193 | bail!( 194 | "Error executing command - stderr:\n{}", 195 | capture.stderr_str() 196 | ) 197 | } 198 | Ok(()) 199 | } 200 | 201 | fn log_and_output(self, verbosity: impl Into>) -> Result { 202 | let out = self.log_and_output_without_checking(verbosity)?; 203 | if !out.success() { 204 | bail!("Error executing command - stderr:\n{}", out.stderr_str()) 205 | } 206 | Ok(out) 207 | } 208 | fn log_and_output_without_checking( 209 | mut self, 210 | verbosity: impl Into>, 211 | ) -> Result { 212 | let verbosity = verbosity.into().unwrap_or_else(Verbosity::get); 213 | self = self.stdout(Redirection::Pipe); 214 | 215 | let cmdline = self.to_cmdline_lossy(); 216 | if verbosity != Verbosity::Normal { 217 | println!("\t{cmdline}"); 218 | } 219 | let output = self.capture()?; 220 | 221 | if verbosity == Verbosity::VeryVerbose { 222 | let stdout = String::from_utf8_lossy(&output.stdout); 223 | println!("{stdout}"); 224 | } 225 | Ok(output) 226 | } 227 | } 228 | 229 | impl ExecExt for Pipeline { 230 | type Output = CaptureData; 231 | 232 | fn log_and_spawn(mut self, verbosity: impl Into>) -> Result<()> { 233 | let verbosity = verbosity.into().unwrap_or_else(Verbosity::get); 234 | if verbosity != Verbosity::Normal { 235 | println!("\t{self:?}"); 236 | } 237 | if verbosity != Verbosity::VeryVerbose { 238 | self = self.stdout(NullFile); 239 | } 240 | let capture = self.capture()?; 241 | if !capture.success() { 242 | bail!( 243 | "Error executing command - stderr:\n{}", 244 | capture.stderr_str() 245 | ) 246 | } 247 | Ok(()) 248 | } 249 | 250 | fn log_and_output(self, verbosity: impl Into>) -> Result { 251 | let out = self.log_and_output_without_checking(verbosity)?; 252 | if !out.success() { 253 | bail!("Error executing command - stderr:\n{}", out.stderr_str()) 254 | } 255 | Ok(out) 256 | } 257 | fn log_and_output_without_checking( 258 | self, 259 | verbosity: impl Into>, 260 | ) -> Result { 261 | let verbosity = verbosity.into().unwrap_or_else(Verbosity::get); 262 | if verbosity != Verbosity::Normal { 263 | println!("\t{self:?}"); 264 | } 265 | let output = self.capture()?; 266 | 267 | if verbosity == Verbosity::VeryVerbose { 268 | let stdout = String::from_utf8_lossy(&output.stdout); 269 | println!("{stdout}"); 270 | } 271 | Ok(output) 272 | } 273 | } 274 | 275 | #[cfg(unix)] 276 | pub(crate) fn mkdir>(path: P) -> std::io::Result<()> { 277 | fn _mkdir(path: &Path) -> std::io::Result<()> { 278 | if let Some(Verbosity::Verbose) = VERBOSITY.get() { 279 | println!("\tmkdir {}", path.display()); 280 | } 281 | 282 | std::fs::create_dir(path) 283 | } 284 | _mkdir(path.as_ref()) 285 | } 286 | 287 | #[cfg(unix)] 288 | pub(crate) fn chmod>(path: P, mode: u32) -> std::io::Result<()> { 289 | fn _chmod(path: &Path, mode: u32) -> std::io::Result<()> { 290 | if let Some(Verbosity::Verbose) = VERBOSITY.get() { 291 | println!("\tchmod {mode:o} {}", path.display()); 292 | } 293 | 294 | let mut perms = std::fs::metadata(path)?.permissions(); 295 | perms.set_mode(mode); 296 | std::fs::set_permissions(path, perms)?; 297 | Ok(()) 298 | } 299 | _chmod(path.as_ref(), mode) 300 | } 301 | 302 | #[cfg(not(unix))] 303 | pub(crate) fn chmod(_path: &Path, _mode: u32) -> std::io::Result<()> { 304 | // do nothing :p 305 | } 306 | 307 | pub(crate) fn make_unpack_work_dir(info: &PackageInfo) -> Result { 308 | let work_dir = format!("{}-{}", info.name, info.version); 309 | mkdir(&work_dir).wrap_err_with(|| format!("unable to mkdir {work_dir}"))?; 310 | 311 | // If the parent directory is suid/guid, mkdir will make the root 312 | // directory of the package inherit those bits. That is a bad thing, 313 | // so explicitly force perms to 755. 314 | 315 | chmod(&work_dir, 0o755)?; 316 | Ok(PathBuf::from(work_dir)) 317 | } 318 | 319 | pub(crate) fn fetch_email_address() -> String { 320 | // TODO: how can this possibly work on windows? 321 | // Also TODO: just ask the user for their email address. ffs. 322 | // I don't have EMAIL set, and nor do i have `/etc/mailname`, 323 | // so now I'm stuck with leah@procrastinator, which of course, is not a real email address. 324 | 325 | if let Ok(email) = std::env::var("EMAIL") { 326 | email 327 | } else { 328 | let mailname = std::fs::read_to_string("/etc/mailname") 329 | .or_else(|_| whoami::fallible::hostname()) 330 | .unwrap_or("".to_owned()); 331 | 332 | let username = whoami::username(); 333 | format!("{username}@{mailname}") 334 | } 335 | } 336 | --------------------------------------------------------------------------------