A simple, zero-configuration script to quickly boot FreeBSD ISO images using QEMU

use deno cliffy for parsing command line args

+131 -47
+2
deno.json
··· 3 3 "dev": "deno run --watch main.ts" 4 4 }, 5 5 "imports": { 6 + "@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.8", 7 + "@cliffy/flags": "jsr:@cliffy/flags@^1.0.0-rc.8", 6 8 "@std/assert": "jsr:@std/assert@1", 7 9 "chalk": "npm:chalk@^5.6.2" 8 10 }
+40
deno.lock
··· 1 1 { 2 2 "version": "5", 3 3 "specifiers": { 4 + "jsr:@cliffy/command@^1.0.0-rc.8": "1.0.0-rc.8", 5 + "jsr:@cliffy/flags@1.0.0-rc.8": "1.0.0-rc.8", 6 + "jsr:@cliffy/flags@^1.0.0-rc.8": "1.0.0-rc.8", 7 + "jsr:@cliffy/internal@1.0.0-rc.8": "1.0.0-rc.8", 8 + "jsr:@cliffy/table@1.0.0-rc.8": "1.0.0-rc.8", 4 9 "jsr:@std/assert@1": "1.0.15", 10 + "jsr:@std/fmt@~1.0.2": "1.0.8", 5 11 "jsr:@std/internal@^1.0.12": "1.0.12", 12 + "jsr:@std/text@~1.0.7": "1.0.15", 6 13 "npm:chalk@^5.6.2": "5.6.2" 7 14 }, 8 15 "jsr": { 16 + "@cliffy/command@1.0.0-rc.8": { 17 + "integrity": "758147790797c74a707e5294cc7285df665422a13d2a483437092ffce40b5557", 18 + "dependencies": [ 19 + "jsr:@cliffy/flags@1.0.0-rc.8", 20 + "jsr:@cliffy/internal", 21 + "jsr:@cliffy/table", 22 + "jsr:@std/fmt", 23 + "jsr:@std/text" 24 + ] 25 + }, 26 + "@cliffy/flags@1.0.0-rc.8": { 27 + "integrity": "0f1043ce6ef037ba1cb5fe6b1bcecb25dc2f29371a1c17f278ab0f45e4b6f46c", 28 + "dependencies": [ 29 + "jsr:@std/text" 30 + ] 31 + }, 32 + "@cliffy/internal@1.0.0-rc.8": { 33 + "integrity": "34cdf2fad9b084b5aed493b138d573f52d4e988767215f7460daf0b918ff43d8" 34 + }, 35 + "@cliffy/table@1.0.0-rc.8": { 36 + "integrity": "8bbcdc2ba5e0061b4b13810a24e6f5c6ab19c09f0cce9eb691ccd76c7c6c9db5", 37 + "dependencies": [ 38 + "jsr:@std/fmt" 39 + ] 40 + }, 9 41 "@std/assert@1.0.15": { 10 42 "integrity": "d64018e951dbdfab9777335ecdb000c0b4e3df036984083be219ce5941e4703b", 11 43 "dependencies": [ 12 44 "jsr:@std/internal" 13 45 ] 46 + }, 47 + "@std/fmt@1.0.8": { 48 + "integrity": "71e1fc498787e4434d213647a6e43e794af4fd393ef8f52062246e06f7e372b7" 14 49 }, 15 50 "@std/internal@1.0.12": { 16 51 "integrity": "972a634fd5bc34b242024402972cd5143eac68d8dffaca5eaa4dba30ce17b027" 52 + }, 53 + "@std/text@1.0.15": { 54 + "integrity": "91f5cc1e12779a3d95f1be34e763f9c28a75a078b7360e6fcaef0d8d9b1e3e7f" 17 55 } 18 56 }, 19 57 "npm": { ··· 23 61 }, 24 62 "workspace": { 25 63 "dependencies": [ 64 + "jsr:@cliffy/command@^1.0.0-rc.8", 65 + "jsr:@cliffy/flags@^1.0.0-rc.8", 26 66 "jsr:@std/assert@1", 27 67 "npm:chalk@^5.6.2" 28 68 ]
+89 -47
main.ts
··· 1 1 #!/usr/bin/env -S deno run --allow-run --allow-read --allow-env 2 2 3 + import { Command } from "@cliffy/command"; 3 4 import chalk from "chalk"; 4 5 5 6 const DEFAULT_VERSION = "14.3-RELEASE"; 7 + 8 + interface Options { 9 + output?: string; 10 + cpu: string; 11 + memory: string; 12 + } 6 13 7 14 async function downloadIso(url: string, outputPath?: string): Promise<string> { 8 15 const filename = url.split("/").pop()!; ··· 34 41 return outputPath; 35 42 } 36 43 37 - if (import.meta.main) { 38 - if (Deno.args.includes("--help") || Deno.args.includes("-h")) { 39 - console.error( 40 - chalk.greenBright( 41 - "Usage: freebsd-up [path-to-iso | version | url]", 42 - ), 43 - ); 44 - Deno.exit(1); 45 - } 46 - 47 - if (Deno.args.length === 0) { 48 - console.log( 49 - chalk.blueBright( 50 - `No ISO path provided, defaulting to ${chalk.cyan("FreeBSD")} ${ 51 - chalk.cyan(DEFAULT_VERSION) 52 - }...`, 53 - ), 54 - ); 55 - const url = `https://download.freebsd.org/ftp/releases/ISO-IMAGES/${ 56 - DEFAULT_VERSION.split("-")[0] 57 - }/FreeBSD-${DEFAULT_VERSION}-amd64-disc1.iso`; 58 - Deno.args.push(url); 59 - } else { 60 - const versionRegex = /^\d{1,2}\.\d{1,2}-(RELEASE|BETA\d*|RC\d*)$/; 61 - const arg = Deno.args[0]; 62 - if (versionRegex.test(arg)) { 63 - console.log( 64 - chalk.blueBright( 65 - `Detected version ${chalk.cyan(arg)}, constructing download URL...`, 66 - ), 67 - ); 68 - const url = `https://download.freebsd.org/ftp/releases/ISO-IMAGES/${ 69 - arg.split("-")[0] 70 - }/FreeBSD-${arg}-amd64-disc1.iso`; 71 - Deno.args[0] = url; 72 - } 73 - } 74 - 75 - let isoPath = Deno.args[0]; 76 - 77 - if ( 78 - Deno.args[0].startsWith("https://") || Deno.args[0].startsWith("http://") 79 - ) { 80 - isoPath = await downloadIso(Deno.args[0]); 81 - } 44 + function constructDownloadUrl(version: string): string { 45 + return `https://download.freebsd.org/ftp/releases/ISO-IMAGES/${ 46 + version.split("-")[0] 47 + }/FreeBSD-${version}-amd64-disc1.iso`; 48 + } 82 49 50 + async function runQemu(isoPath: string, options: Options): Promise<void> { 83 51 const cmd = new Deno.Command("qemu-system-x86_64", { 84 52 args: [ 85 53 "-enable-kvm", 86 54 "-cpu", 87 - "host", 55 + options.cpu, 88 56 "-m", 89 - "2G", 57 + options.memory, 90 58 "-smp", 91 59 "2", 92 60 "-cdrom", ··· 116 84 Deno.exit(status.code); 117 85 } 118 86 } 87 + 88 + function handleInput(input?: string): string { 89 + if (!input) { 90 + console.log( 91 + chalk.blueBright( 92 + `No ISO path provided, defaulting to ${chalk.cyan("FreeBSD")} ${ 93 + chalk.cyan(DEFAULT_VERSION) 94 + }...`, 95 + ), 96 + ); 97 + return constructDownloadUrl(DEFAULT_VERSION); 98 + } 99 + 100 + const versionRegex = /^\d{1,2}\.\d{1,2}-(RELEASE|BETA\d*|RC\d*)$/; 101 + 102 + if (versionRegex.test(input)) { 103 + console.log( 104 + chalk.blueBright( 105 + `Detected version ${chalk.cyan(input)}, constructing download URL...`, 106 + ), 107 + ); 108 + return constructDownloadUrl(input); 109 + } 110 + 111 + return input; 112 + } 113 + 114 + if (import.meta.main) { 115 + await new Command() 116 + .name("freebsd-up") 117 + .version("0.1.0") 118 + .description("Start a FreeBSD virtual machine using QEMU") 119 + .arguments("[path-to-iso-or-version:string]") 120 + .option("-o, --output <path:string>", "Output path for downloaded ISO") 121 + .option("-c, --cpu <type:string>", "Type of CPU to emulate", { 122 + default: "host", 123 + }) 124 + .option("-m, --memory <size:string>", "Amount of memory for the VM", { 125 + default: "2G", 126 + }) 127 + .example( 128 + "Default usage", 129 + "freebsd-up", 130 + ) 131 + .example( 132 + "Specific version", 133 + "freebsd-up 14.3-RELEASE", 134 + ) 135 + .example( 136 + "Local ISO file", 137 + "freebsd-up /path/to/freebsd.iso", 138 + ) 139 + .example( 140 + "Download URL", 141 + "freebsd-up https://download.freebsd.org/ftp/releases/ISO-IMAGES/14.3/FreeBSD-14.3-RELEASE-amd64-disc1.iso", 142 + ) 143 + .action(async (options: Options, input?: string) => { 144 + const resolvedInput = handleInput(input); 145 + let isoPath = resolvedInput; 146 + 147 + if ( 148 + resolvedInput.startsWith("https://") || 149 + resolvedInput.startsWith("http://") 150 + ) { 151 + isoPath = await downloadIso(resolvedInput, options.output); 152 + } 153 + 154 + await runQemu(isoPath, { 155 + cpu: options.cpu, 156 + memory: options.memory, 157 + }); 158 + }) 159 + .parse(Deno.args); 160 + }