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

Configure Feed

Select the types of activity you want to include in your feed.

Refactor main.ts and utils.ts to modularize functions and improve code organization; add lodash dependency for utility functions.

+201 -154
+2 -1
deno.json
··· 6 6 "@cliffy/command": "jsr:@cliffy/command@^1.0.0-rc.8", 7 7 "@cliffy/flags": "jsr:@cliffy/flags@^1.0.0-rc.8", 8 8 "@std/assert": "jsr:@std/assert@1", 9 - "chalk": "npm:chalk@^5.6.2" 9 + "chalk": "npm:chalk@^5.6.2", 10 + "lodash": "npm:lodash@^4.17.21" 10 11 } 11 12 }
+7 -2
deno.lock
··· 10 10 "jsr:@std/fmt@~1.0.2": "1.0.8", 11 11 "jsr:@std/internal@^1.0.12": "1.0.12", 12 12 "jsr:@std/text@~1.0.7": "1.0.15", 13 - "npm:chalk@^5.6.2": "5.6.2" 13 + "npm:chalk@^5.6.2": "5.6.2", 14 + "npm:lodash@^4.17.21": "4.17.21" 14 15 }, 15 16 "jsr": { 16 17 "@cliffy/command@1.0.0-rc.8": { ··· 57 58 "npm": { 58 59 "chalk@5.6.2": { 59 60 "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==" 61 + }, 62 + "lodash@4.17.21": { 63 + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" 60 64 } 61 65 }, 62 66 "workspace": { ··· 64 68 "jsr:@cliffy/command@^1.0.0-rc.8", 65 69 "jsr:@cliffy/flags@^1.0.0-rc.8", 66 70 "jsr:@std/assert@1", 67 - "npm:chalk@^5.6.2" 71 + "npm:chalk@^5.6.2", 72 + "npm:lodash@^4.17.21" 68 73 ] 69 74 } 70 75 }
+13 -151
main.ts
··· 1 1 #!/usr/bin/env -S deno run --allow-run --allow-read --allow-env 2 2 3 3 import { Command } from "@cliffy/command"; 4 - import chalk from "chalk"; 5 - 6 - const DEFAULT_VERSION = "14.3-RELEASE"; 7 - 8 - interface Options { 9 - output?: string; 10 - cpu: string; 11 - cpus: number; 12 - memory: string; 13 - drive?: string; 14 - diskFormat: string; 15 - size?: string; 16 - } 17 - 18 - async function downloadIso(url: string, outputPath?: string): Promise<string> { 19 - const filename = url.split("/").pop()!; 20 - outputPath = outputPath ?? filename; 21 - 22 - if (await Deno.stat(outputPath).catch(() => false)) { 23 - console.log( 24 - chalk.yellowBright( 25 - `File ${outputPath} already exists, skipping download.`, 26 - ), 27 - ); 28 - return outputPath; 29 - } 30 - 31 - const cmd = new Deno.Command("curl", { 32 - args: ["-L", "-o", outputPath, url], 33 - stdin: "inherit", 34 - stdout: "inherit", 35 - stderr: "inherit", 36 - }); 37 - 38 - const status = await cmd.spawn().status; 39 - if (!status.success) { 40 - console.error(chalk.redBright("Failed to download ISO image.")); 41 - Deno.exit(status.code); 42 - } 43 - 44 - console.log(chalk.greenBright(`Downloaded ISO to ${outputPath}`)); 45 - return outputPath; 46 - } 47 - 48 - function constructDownloadUrl(version: string): string { 49 - return `https://download.freebsd.org/ftp/releases/ISO-IMAGES/${ 50 - version.split("-")[0] 51 - }/FreeBSD-${version}-amd64-disc1.iso`; 52 - } 53 - 54 - async function runQemu(isoPath: string, options: Options): Promise<void> { 55 - const cmd = new Deno.Command("qemu-system-x86_64", { 56 - args: [ 57 - "-enable-kvm", 58 - "-cpu", 59 - options.cpu, 60 - "-m", 61 - options.memory, 62 - "-smp", 63 - options.cpus.toString(), 64 - "-cdrom", 65 - isoPath, 66 - "-netdev", 67 - "user,id=net0,hostfwd=tcp::2222-:22", 68 - "-device", 69 - "e1000,netdev=net0", 70 - "-nographic", 71 - "-monitor", 72 - "none", 73 - "-chardev", 74 - "stdio,id=con0,signal=off", 75 - "-serial", 76 - "chardev:con0", 77 - ...(options.drive 78 - ? [ 79 - "-drive", 80 - `file=${options.drive},format=${options.diskFormat},if=virtio`, 81 - ] 82 - : []), 83 - ], 84 - stdin: "inherit", 85 - stdout: "inherit", 86 - stderr: "inherit", 87 - }); 88 - 89 - const status = await cmd.spawn().status; 90 - 91 - if (!status.success) { 92 - Deno.exit(status.code); 93 - } 94 - } 95 - 96 - function handleInput(input?: string): string { 97 - if (!input) { 98 - console.log( 99 - chalk.blueBright( 100 - `No ISO path provided, defaulting to ${chalk.cyan("FreeBSD")} ${ 101 - chalk.cyan(DEFAULT_VERSION) 102 - }...`, 103 - ), 104 - ); 105 - return constructDownloadUrl(DEFAULT_VERSION); 106 - } 107 - 108 - const versionRegex = /^\d{1,2}\.\d{1,2}-(RELEASE|BETA\d*|RC\d*)$/; 109 - 110 - if (versionRegex.test(input)) { 111 - console.log( 112 - chalk.blueBright( 113 - `Detected version ${chalk.cyan(input)}, constructing download URL...`, 114 - ), 115 - ); 116 - return constructDownloadUrl(input); 117 - } 118 - 119 - return input; 120 - } 121 - 122 - async function createDriveImageIfNeeded( 123 - { 124 - drive: path, 125 - diskFormat: format, 126 - size, 127 - }: Options, 128 - ): Promise<void> { 129 - if (await Deno.stat(path!).catch(() => false)) { 130 - console.log( 131 - chalk.yellowBright( 132 - `Drive image ${path} already exists, skipping creation.`, 133 - ), 134 - ); 135 - return; 136 - } 137 - 138 - const cmd = new Deno.Command("qemu-img", { 139 - args: ["create", "-f", format, path!, size!], 140 - stdin: "inherit", 141 - stdout: "inherit", 142 - stderr: "inherit", 143 - }); 144 - 145 - const status = await cmd.spawn().status; 146 - if (!status.success) { 147 - console.error(chalk.redBright("Failed to create drive image.")); 148 - Deno.exit(status.code); 149 - } 150 - 151 - console.log(chalk.greenBright(`Created drive image at ${path}`)); 152 - } 4 + import { 5 + createDriveImageIfNeeded, 6 + downloadIso, 7 + handleInput, 8 + Options, 9 + runQemu, 10 + } from "./utils.ts"; 153 11 154 12 if (import.meta.main) { 155 13 await new Command() ··· 202 60 ) 203 61 .action(async (options: Options, input?: string) => { 204 62 const resolvedInput = handleInput(input); 205 - let isoPath = resolvedInput; 63 + let isoPath: string | null = resolvedInput; 206 64 207 65 if ( 208 66 resolvedInput.startsWith("https://") || 209 67 resolvedInput.startsWith("http://") 210 68 ) { 211 - isoPath = await downloadIso(resolvedInput, options.output); 69 + isoPath = await downloadIso(resolvedInput, options); 212 70 } 213 71 214 72 if (options.drive) { 215 73 await createDriveImageIfNeeded(options); 74 + } 75 + 76 + if (!input && options.drive) { 77 + isoPath = null; 216 78 } 217 79 218 80 await runQemu(isoPath, {
+179
utils.ts
··· 1 + import chalk from "chalk"; 2 + import _ from "lodash"; 3 + 4 + const DEFAULT_VERSION = "14.3-RELEASE"; 5 + 6 + export interface Options { 7 + output?: string; 8 + cpu: string; 9 + cpus: number; 10 + memory: string; 11 + drive?: string; 12 + diskFormat: string; 13 + size?: string; 14 + } 15 + 16 + async function du(path: string): Promise<number> { 17 + const cmd = new Deno.Command("du", { 18 + args: [path], 19 + stdout: "piped", 20 + stderr: "inherit", 21 + }); 22 + 23 + const { stdout } = await cmd.spawn().output(); 24 + const output = new TextDecoder().decode(stdout).trim(); 25 + const size = parseInt(output.split("\t")[0], 10); 26 + return size; 27 + } 28 + export async function downloadIso( 29 + url: string, 30 + options: Options, 31 + ): Promise<string | null> { 32 + const filename = url.split("/").pop()!; 33 + const outputPath = options.output ?? filename; 34 + 35 + if (options.drive && await Deno.stat(options.drive).catch(() => false)) { 36 + const driveSize = await du(options.drive); 37 + if (driveSize > 10) { 38 + console.log( 39 + chalk.yellowBright( 40 + `Drive image ${options.drive} is not empty (size: ${driveSize} KB), skipping ISO download to avoid overwriting existing data.`, 41 + ), 42 + ); 43 + return null; 44 + } 45 + } 46 + 47 + if (await Deno.stat(outputPath).catch(() => false)) { 48 + console.log( 49 + chalk.yellowBright( 50 + `File ${outputPath} already exists, skipping download.`, 51 + ), 52 + ); 53 + return outputPath; 54 + } 55 + 56 + const cmd = new Deno.Command("curl", { 57 + args: ["-L", "-o", outputPath, url], 58 + stdin: "inherit", 59 + stdout: "inherit", 60 + stderr: "inherit", 61 + }); 62 + 63 + const status = await cmd.spawn().status; 64 + if (!status.success) { 65 + console.error(chalk.redBright("Failed to download ISO image.")); 66 + Deno.exit(status.code); 67 + } 68 + 69 + console.log(chalk.greenBright(`Downloaded ISO to ${outputPath}`)); 70 + return outputPath; 71 + } 72 + 73 + export function constructDownloadUrl(version: string): string { 74 + return `https://download.freebsd.org/ftp/releases/ISO-IMAGES/${ 75 + version.split("-")[0] 76 + }/FreeBSD-${version}-amd64-disc1.iso`; 77 + } 78 + 79 + export async function runQemu( 80 + isoPath: string | null, 81 + options: Options, 82 + ): Promise<void> { 83 + const cmd = new Deno.Command("qemu-system-x86_64", { 84 + args: [ 85 + "-enable-kvm", 86 + "-cpu", 87 + options.cpu, 88 + "-m", 89 + options.memory, 90 + "-smp", 91 + options.cpus.toString(), 92 + ..._.compact([isoPath && "-cdrom", isoPath]), 93 + "-netdev", 94 + "user,id=net0,hostfwd=tcp::2222-:22", 95 + "-device", 96 + "e1000,netdev=net0", 97 + "-nographic", 98 + "-monitor", 99 + "none", 100 + "-chardev", 101 + "stdio,id=con0,signal=off", 102 + "-serial", 103 + "chardev:con0", 104 + ..._.compact( 105 + options.drive && [ 106 + "-drive", 107 + `file=${options.drive},format=${options.diskFormat},if=virtio`, 108 + ], 109 + ), 110 + ], 111 + stdin: "inherit", 112 + stdout: "inherit", 113 + stderr: "inherit", 114 + }); 115 + 116 + const status = await cmd.spawn().status; 117 + 118 + if (!status.success) { 119 + Deno.exit(status.code); 120 + } 121 + } 122 + 123 + export function handleInput(input?: string): string { 124 + if (!input) { 125 + console.log( 126 + chalk.blueBright( 127 + `No ISO path provided, defaulting to ${chalk.cyan("FreeBSD")} ${ 128 + chalk.cyan(DEFAULT_VERSION) 129 + }...`, 130 + ), 131 + ); 132 + return constructDownloadUrl(DEFAULT_VERSION); 133 + } 134 + 135 + const versionRegex = /^\d{1,2}\.\d{1,2}-(RELEASE|BETA\d*|RC\d*)$/; 136 + 137 + if (versionRegex.test(input)) { 138 + console.log( 139 + chalk.blueBright( 140 + `Detected version ${chalk.cyan(input)}, constructing download URL...`, 141 + ), 142 + ); 143 + return constructDownloadUrl(input); 144 + } 145 + 146 + return input; 147 + } 148 + 149 + export async function createDriveImageIfNeeded( 150 + { 151 + drive: path, 152 + diskFormat: format, 153 + size, 154 + }: Options, 155 + ): Promise<void> { 156 + if (await Deno.stat(path!).catch(() => false)) { 157 + console.log( 158 + chalk.yellowBright( 159 + `Drive image ${path} already exists, skipping creation.`, 160 + ), 161 + ); 162 + return; 163 + } 164 + 165 + const cmd = new Deno.Command("qemu-img", { 166 + args: ["create", "-f", format, path!, size!], 167 + stdin: "inherit", 168 + stdout: "inherit", 169 + stderr: "inherit", 170 + }); 171 + 172 + const status = await cmd.spawn().status; 173 + if (!status.success) { 174 + console.error(chalk.redBright("Failed to create drive image.")); 175 + Deno.exit(status.code); 176 + } 177 + 178 + console.log(chalk.greenBright(`Created drive image at ${path}`)); 179 + }