├── README.md └── pkgbasify.lua /README.md: -------------------------------------------------------------------------------- 1 | # pkgbasify 2 | 3 | Automatically convert a FreeBSD system to use [pkgbase]. 4 | 5 | This project is sponsored by the [FreeBSD Foundation](https://freebsdfoundation.org/). 6 | 7 | ## Disclaimer 8 | 9 | Both the pkgbasify tool and pkgbase itself are experimental. 10 | Running pkgbasify may result in irreversible data loss and/or a system that fails to boot. 11 | It is highly recommended to make backups before running this tool. 12 | 13 | That said, I am not aware of any bugs in pkgbasify and have done my best to make it as robust as possible. 14 | I currently believe pkgbasify to be as reliable as manual conversion if not better. 15 | 16 | If you find a bug in pkgbasify please open an issue! 17 | 18 | ## Usage 19 | 20 | Ensure you have at least 5 GiB of free disk space. 21 | Conversion can likely succeed with less, but pkg is [not yet](https://github.com/freebsd/pkg/issues/75) 22 | able to detect and handle insufficient space gracefully. 23 | It can be difficult to recover if the system runs out of space during conversion. 24 | 25 | Download the script, give it permission to execute, run it as root: 26 | 27 | 1. `fetch https://github.com/FreeBSDFoundation/pkgbasify/raw/refs/heads/main/pkgbasify.lua` 28 | 2. `chmod +x ./pkgbasify.lua` 29 | 3. `./pkgbasify.lua` 30 | 31 | If conversion succeeds: 32 | 33 | 4. Verify that expected users and groups are present in `/etc/master.passwd` and `/etc/group`, and that `/etc/ssh/sshd_config` is as expected. 34 | These should be handled automatically by pkgbasify, but since the consequences are high it is recommended to double check. 35 | 5. Restart the system. 36 | 37 | If there is an error during installation of the pkgbase packages, the system may be left in a partially-converted state. 38 | In this case, the user should fix whatever issue caused the error and run `./pkgbasify.lua --force` to try and complete the conversion. 39 | 40 | See also [Common Problems and Solutions](#common-problems-and-solutions). 41 | 42 | ## Behavior 43 | 44 | pkgbasify performs the following steps: 45 | 46 | 1. Make a copy of the [etcupdate(8)] current database (`/var/db/etcupdate/current`). 47 | This makes it possible for pkgbasify to merge config files after converting the system. 48 | 2. Select a repository based on the output of [freebsd-version(1)] and create `/usr/local/etc/pkg/repos/FreeBSD-base.conf`. 49 | 3. Select packages that correspond to the currently installed base system components. 50 | - For example: if the lib32 component is not already installed, 51 | pkgbasify will skip installation of lib32 packages. 52 | - pkgbasify never installs the `FreeBSD-src` package even if `/usr/src` is present and non-empty. 53 | This prevents unwanted overwriting of potentially modified source files and/or a VCS repository. 54 | 4. Prompt the user to create a "pre-pkgbasify" boot environment using [bectl(8)] if possible. 55 | 5. Install the selected packages with [pkg(8)], 56 | overwriting base system files and creating `.pkgsave` files as per standard [pkg(8)] behavior. 57 | 6. Run a three-way-merge between the `.pkgsave` files (ours), 58 | the new files installed by pkg (theirs), 59 | and the old files in the copy of the etcupdate database. 60 | - If there are merge conflicts, an error is logged and manual intervention may be required. 61 | - `.pkgsave` files without a corresponding entry in the old etcupdate database are skipped. 62 | 7. If [sshd(8)] is running, restart the service. 63 | 8. Run [pwd_mkdb(8)] and [cap_mkdb(1)]. 64 | 9. Remove `/boot/kernel/linker.hints`. 65 | 66 | [bectl(8)]: https://man.freebsd.org/cgi/man.cgi?query=bectl&sektion=8&manpath=freebsd-release 67 | [pkgbase]: https://wiki.freebsd.org/PkgBase 68 | [etcupdate(8)]: https://man.freebsd.org/cgi/man.cgi?query=etcupdate&sektion=8&manpath=freebsd-release 69 | [freebsd-version(1)]: https://man.freebsd.org/cgi/man.cgi?query=freebsd-version&sektion=1&manpath=freebsd-release 70 | [pkg(8)]: https://man.freebsd.org/cgi/man.cgi?query=pkg&sektion=8&manpath=freebsd-ports 71 | [sshd(8)]: https://man.freebsd.org/cgi/man.cgi?query=sshd&sektion=8&manpath=freebsd-release 72 | [pwd_mkdb(8)]: https://man.freebsd.org/cgi/man.cgi?query=pwd_mkdb&sektion=8&manpath=freebsd-release 73 | [cap_mkdb(1)]: https://man.freebsd.org/cgi/man.cgi?query=cap_mkdb&sektion=1&manpath=freebsd-release 74 | 75 | ## Common Problems and Solutions 76 | 77 | ### "Fail to create hardlink" 78 | 79 | ``` 80 | [1/66] Installing FreeBSD-runtime-15.snap20250604185611... 81 | [1/66] Extracting FreeBSD-runtime-15.snap20250604185611: 33% 82 | pkg: Fail to create hardlink: /.pkgtemp..profile.6vmf7kjyXtm8 <-> /root/.pkgtemp..profile.h5D7P2AMln3A:Cross-device link 83 | [1/66] Extracting FreeBSD-runtime-15.snap20250604185611: 100% 84 | Error: exit 85 | ``` 86 | 87 | This may be caused by a mountpoint over the top of a file or directory that `pkg` is trying to update. 88 | `pkg` expects that the `TMPDIR` and the destination are on the same filesystem. 89 | Unmount whatever is on top, and run `./pkgbasify.lua --force` to finish conversion. 90 | In this case, `/root` had been put on its own zfs dataset. 91 | 92 | ### "Fail to set time on /var/empty:Read-only file system" 93 | 94 | ``` 95 | [1/66] Installing FreeBSD-runtime-15.snap20250604185611... 96 | [1/66] Extracting FreeBSD-runtime-15.snap20250604185611: 100% 97 | pkg: Fail to set time on /var/empty:Read-only file system 98 | Error: exit 99 | ``` 100 | 101 | This may be caused by having a zfs filesystem `zroot/var/empty` with the property `readonly=on`. 102 | Set `readonly=off` and run `./pkgbasify.lua --force` to finish conversion. 103 | 104 | ### "Fail to rename ..." while extracting FreeBSD-src 105 | 106 | ``` 107 | [323/371] Installing FreeBSD-src-14.1p8... 108 | [323/371] Extracting FreeBSD-src-14.1p8: 100% 109 | pkg: Fail to rename /usr/src/contrib/llvm-project/libcxx/include/.pkgtemp.__string.olNBRRqZQLwR -> /usr/src/contrib/llvm-project/libcxx/include/__string:Not a directory 110 | Error: exit 111 | ``` 112 | 113 | This can be caused by a dirty `/usr/src` directory. 114 | The recommended fix is to remove `/usr/src` and run `./pkgbasify.lua --force` to finish conversion and let pkg fully reinstall `/usr/src`. 115 | -------------------------------------------------------------------------------- /pkgbasify.lua: -------------------------------------------------------------------------------- 1 | #!/usr/libexec/flua 2 | 3 | -- SPDX-License-Identifier: BSD-2-Clause 4 | -- 5 | -- Copyright(c) 2025 The FreeBSD Foundation. 6 | -- 7 | -- This software was developed by Isaac Freund 8 | -- under sponsorship from the FreeBSD Foundation. 9 | 10 | -- See also the pkgbase wiki page: https://wiki.freebsd.org/PkgBase 11 | 12 | local options = { 13 | create_repo_conf = true, 14 | repo_name = "FreeBSD-base", 15 | rootdir = "/", 16 | jail = nil, 17 | } 18 | 19 | local function repo_conf_dir() 20 | return options.rootdir .. "/usr/local/etc/pkg/repos/" 21 | end 22 | local function repo_conf_file() 23 | return repo_conf_dir() .. options.repo_name .. ".conf" 24 | end 25 | 26 | -- Run a command using the OS shell and capture the stdout 27 | -- Strips exactly one trailing newline if present, does not strip any other whitespace. 28 | -- Asserts that the command exits cleanly 29 | local function capture(command) 30 | local p = io.popen(command) 31 | local output = p:read("*a") 32 | assert(p:close()) 33 | -- Strip exactly one trailing newline from the output, if there is one 34 | return output:match("(.-)\n$") or output 35 | end 36 | 37 | local function prompt_yn(question) 38 | while true do 39 | io.write(question .. " (y/n) ") 40 | local input = io.read() 41 | if input == "y" or input == "Y" then 42 | return true 43 | elseif input == "n" or input == "N" then 44 | return false 45 | end 46 | end 47 | end 48 | 49 | local function append_list(list, other) 50 | for _, item in ipairs(other) do 51 | table.insert(list, item) 52 | end 53 | end 54 | 55 | local function err(msg) 56 | io.stderr:write("Error: " .. msg .. "\n") 57 | end 58 | 59 | local function fatal(msg) 60 | err(msg) 61 | os.exit(1) 62 | end 63 | 64 | local function freebsd_version() 65 | local raw 66 | if options.jail then 67 | raw = capture("freebsd-version -j " .. options.jail) 68 | else 69 | raw = capture("freebsd-version") 70 | end 71 | -- e.g. 15.0-CURRENT, 14.2-STABLE, 14.1-RxLEASE, 14.1-RELEASE-p6, 72 | local major, minor, branch = assert(raw:match("(%d+)%.(%d+)%-(%u+)")) 73 | return major, minor, branch, raw 74 | end 75 | 76 | -- Returns the URL for the pkgbase repository that matches the version 77 | -- reported by freebsd-version(1) 78 | local function base_repo_url() 79 | local major, minor, branch, raw = freebsd_version() 80 | if math.tointeger(major) < 14 then 81 | fatal("Unsupported FreeBSD version: " .. raw) 82 | end 83 | if branch == "RELEASE" or branch:match("^BETA") or branch:match("^RC") then 84 | return "pkg+https://pkg.FreeBSD.org/${ABI}/base_release_" .. minor 85 | elseif branch == "CURRENT" or 86 | branch == "STABLE" or 87 | branch == "PRERELEASE" or 88 | branch:match("^ALPHA") 89 | then 90 | return "pkg+https://pkg.FreeBSD.org/${ABI}/base_latest" 91 | else 92 | fatal("Unsupported FreeBSD version: " .. raw) 93 | end 94 | end 95 | 96 | local function create_base_repo_conf(path) 97 | assert(os.execute("mkdir -p " .. path:match(".*/"))) 98 | local f = assert(io.open(path, "w")) 99 | if math.tointeger(freebsd_version()) >= 15 then 100 | assert(f:write(string.format([[ 101 | %s: { 102 | enabled: yes 103 | } 104 | ]], options.repo_name))) 105 | else 106 | assert(f:write(string.format([[ 107 | %s: { 108 | url: "%s", 109 | mirror_type: "srv", 110 | signature_type: "fingerprints", 111 | fingerprints: "/usr/share/keys/pkg", 112 | enabled: yes 113 | } 114 | ]], options.repo_name, base_repo_url()))) 115 | end 116 | end 117 | 118 | -- Set to true if the pkg install or any later step errors. We will always 119 | -- attempt to execute every step after pkg install even if it fails, but we 120 | -- should exit with an error code if there was a failure along the way. 121 | local err_post_install = false 122 | local function check_err(ok, err_msg) 123 | if not ok then 124 | err(err_msg) 125 | err_post_install = true 126 | end 127 | end 128 | 129 | local function merge_pkgsaves(workdir) 130 | local old_dir = workdir .. "/current" 131 | for old in capture("find " .. old_dir .. " -type f"):gmatch("[^\n]+") do 132 | local path = old:sub(#old_dir + 1) 133 | assert(path:sub(1,1) == "/") 134 | local theirs = options.rootdir .. path 135 | local ours = theirs .. ".pkgsave" 136 | if os.execute("test -e " .. ours) then 137 | local merged = workdir .. "/merged/" .. path 138 | check_err(os.execute("mkdir -p " .. merged:match(".*/"))) 139 | -- Using cat and a redirection rather than, for example, mv preserves 140 | -- file attributes of theirs (mode, ownership, etc). This is critical 141 | -- when merging executable scripts in /etc/rc.d/ for example. 142 | if os.execute("diff3 -m " .. ours .. " " .. old .. " " .. theirs .. " > " .. merged) and 143 | os.execute("cat " .. merged .. " > " .. theirs) 144 | then 145 | print("Merged " .. theirs) 146 | else 147 | print("Failed to merge " .. theirs .. ", manual intervention may be necessary") 148 | end 149 | end 150 | end 151 | end 152 | 153 | local function execute_conversion(workdir, package_list) 154 | if options.create_repo_conf then 155 | if os.execute("test -e " .. repo_conf_file()) then 156 | print("Overwriting " .. repo_conf_file()) 157 | else 158 | print("Creating " .. repo_conf_file()) 159 | end 160 | create_base_repo_conf(repo_conf_file()) 161 | end 162 | 163 | if capture("pkg config BACKUP_LIBRARIES") ~= "yes" then 164 | print("Adding BACKUP_LIBRARIES=yes to /usr/local/etc/pkg.conf") 165 | local f = assert(io.open("/usr/local/etc/pkg.conf", "a")) 166 | assert(f:write("BACKUP_LIBRARIES=yes\n")) 167 | end 168 | 169 | local pkg = "pkg --rootdir " .. options.rootdir .. 170 | " -o REPOS_DIR=" .. options.rootdir .. "/etc/pkg/," .. repo_conf_dir() .. " " 171 | 172 | local packages = table.concat(package_list, " ") 173 | -- Fetch the packages separately so that we can retry if there is a temporary 174 | -- network issue or similar. 175 | while not os.execute(pkg .. " install --fetch-only -y -r " 176 | .. options.repo_name .. " " .. packages) 177 | do 178 | if not prompt_yn("Fetching packages failed, try again?") then 179 | print("Canceled") 180 | os.exit(1) 181 | end 182 | end 183 | 184 | -- pkg install is not necessarily fully atomic, even if it fails some subset 185 | -- of the packages may have been installed. Therefore, we must attempt all 186 | -- followup work even if install fails. 187 | check_err(os.execute(pkg .. " install --no-repo-update -y -r " .. 188 | options.repo_name .. " " .. packages)) 189 | 190 | merge_pkgsaves(workdir) 191 | 192 | if options.rootdir == "/" then 193 | if os.execute("service sshd status > /dev/null 2>&1") then 194 | print("Restarting sshd") 195 | check_err(os.execute("service sshd restart")) 196 | end 197 | end 198 | 199 | check_err(os.execute("pwd_mkdb -d " .. options.rootdir .. 200 | "/etc -p " .. options.rootdir .. "/etc/master.passwd")) 201 | check_err(os.execute("cap_mkdb " .. options.rootdir .. "/etc/login.conf")) 202 | 203 | -- Ensure linker.hints is regenerated at next boot. 204 | check_err(os.execute("rm -f " .. options.rootdir .. "/boot/kernel/linker.hints")) 205 | 206 | if err_post_install then 207 | print([[ 208 | An error occurred during conversion leaving the system in a partially 209 | converted state. 210 | 211 | Please determine and resolve the root cause of the error. 212 | 213 | When you believe the error will not happen again, run pkgbasify with 214 | the --force argument to try and complete the conversion. 215 | ]]) 216 | os.exit(1) 217 | else 218 | local prefix = options.rootdir 219 | if prefix == "/" then 220 | prefix = "" 221 | end 222 | print(string.format([[ 223 | Conversion finished. 224 | 225 | Please verify that the contents of the following critical files are as expected: 226 | %s/etc/master.passwd 227 | %s/etc/group 228 | %s/etc/ssh/sshd_config 229 | 230 | After verifying those files, restart the system. 231 | ]], prefix, prefix, prefix)) 232 | os.exit(0) 233 | end 234 | end 235 | 236 | -- Returns the osversion as an integer 237 | local function rquery_osversion(pkg) 238 | -- It feels like pkg should provide a less ugly way to do this. 239 | -- TODO is FreeBSD-runtime the correct pkg to check against? 240 | local tags = capture(pkg .. "rquery -r " .. options.repo_name .. 241 | " %At FreeBSD-runtime"):gmatch("[^\n]+") 242 | local values = capture(pkg .. "rquery -r " .. options.repo_name .. 243 | " %Av FreeBSD-runtime"):gmatch("[^\n]+") 244 | while true do 245 | local tag = tags() 246 | local value = values() 247 | if not tag or not value then 248 | break 249 | end 250 | if tag == "FreeBSD_version" then 251 | return math.tointeger(value) 252 | end 253 | end 254 | fatal("Missing FreeBSD_version annotation for FreeBSD-runtime package") 255 | end 256 | 257 | local function confirm_version_compatibility(pkg) 258 | local osversion_local = math.tointeger(capture(pkg .. " config osversion")) 259 | local osversion_remote = rquery_osversion(pkg) 260 | if osversion_remote < osversion_local then 261 | -- This may be overly restrictive, having to wait for remote repositories to 262 | -- update before the system can be pkgbasified is poor UX. 263 | print(string.format("System has newer __FreeBSD_version than remote pkgbase packages (%d vs %d).", 264 | osversion_local, osversion_remote)) 265 | return prompt_yn(string.format("Continue anyway and downgrade the system to %d?", osversion_remote)) 266 | elseif osversion_remote > osversion_local then 267 | print(string.format("System has older __FreeBSD_version than remote pkgbase packages (%d vs %d).", 268 | osversion_local, osversion_remote)) 269 | print("It is recommended to update your system before running pkgbasify.") 270 | return prompt_yn("Ignore the osversion and continue anyway?") 271 | end 272 | assert(osversion_local == osversion_remote) 273 | return true 274 | end 275 | 276 | local function create_boot_environment() 277 | -- Don't create a boot environment if running in a jail 278 | if capture("sysctl -n security.jail.jailed") == "1" then 279 | return 280 | end 281 | 282 | if not os.execute("bectl check") then 283 | return 284 | end 285 | 286 | if prompt_yn("Create a boot environment before conversion?") then 287 | local timestamp = capture("date +'%Y-%m-%d_%H%M%S'") 288 | if not os.execute("bectl create -r pre-pkgbasify_" .. timestamp) then 289 | fatal("Failed to create boot environment") 290 | end 291 | end 292 | end 293 | 294 | -- Returns true if the path is a non-empty directory. 295 | -- Returns false if the path is empty, not a directory, or does not exist. 296 | local function non_empty_dir(path) 297 | local p = io.popen("find " .. path .. " -maxdepth 0 -type d -not -empty 2>/dev/null") 298 | local output = p:read("*a"):gsub("%s+", "") -- remove whitespace 299 | local success = p:close() 300 | return output ~= "" and success 301 | end 302 | 303 | -- Returns a list of pkgbase packages matching the files present on the system 304 | local function select_packages(pkg) 305 | local kernel = {} 306 | local kernel_dbg = {} 307 | local base = {} 308 | local base_dbg = {} 309 | local lib32 = {} 310 | local lib32_dbg = {} 311 | local src = {} 312 | local tests = {} 313 | 314 | local rquery = capture(pkg .. "rquery -r " .. options.repo_name .. " %n") 315 | for package in rquery:gmatch("[^\n]+") do 316 | if package == "FreeBSD-src" or package:match("FreeBSD%-src%-.*") then 317 | table.insert(src, package) 318 | elseif package == "FreeBSD-tests" or package:match("FreeBSD%-tests%-.*") then 319 | table.insert(tests, package) 320 | elseif package:match("FreeBSD%-kernel%-.*") then 321 | -- Kernels other than FreeBSD-kernel-generic are ignored 322 | if package == "FreeBSD-kernel-generic" then 323 | table.insert(kernel, package) 324 | elseif package == "FreeBSD-kernel-generic-dbg" then 325 | table.insert(kernel_dbg, package) 326 | end 327 | elseif package:match(".*%-dbg%-lib32") then 328 | table.insert(lib32_dbg, package) 329 | elseif package:match(".*%-lib32") then 330 | table.insert(lib32, package) 331 | elseif package:match(".*%-dbg") then 332 | table.insert(base_dbg, package) 333 | else 334 | table.insert(base, package) 335 | end 336 | end 337 | -- No asserts on lib32(-dbg) since they aren't present for all targets 338 | assert(#kernel == 1) 339 | assert(#kernel_dbg == 1) 340 | assert(#base > 0) 341 | assert(#base_dbg > 0) 342 | assert(#tests > 0) 343 | -- FreeBSD-src was not yet available for FreeBSD 14.0 344 | assert(#src >= 0) 345 | 346 | local selected = {} 347 | append_list(selected, kernel) 348 | append_list(selected, base) 349 | 350 | if non_empty_dir(options.rootdir .. "/usr/lib/debug/boot/kernel") then 351 | append_list(selected, kernel_dbg) 352 | end 353 | if os.execute("test -e " .. options.rootdir .. "/usr/lib/debug/lib/libc.so.7.debug") then 354 | append_list(selected, base_dbg) 355 | end 356 | -- Checking if /usr/lib32 is non-empty is not sufficient, as base.txz 357 | -- includes several empty /usr/lib32 subdirectories. 358 | if os.execute("test -e " .. options.rootdir .. "/usr/lib32/libc.so.7") then 359 | append_list(selected, lib32) 360 | end 361 | if os.execute("test -e " .. options.rootdir .. "/usr/lib/debug/usr/lib32/libc.so.7.debug") then 362 | append_list(selected, lib32_dbg) 363 | end 364 | if non_empty_dir(options.rootdir .. "/usr/tests") then 365 | append_list(selected, tests) 366 | end 367 | 368 | return selected 369 | end 370 | 371 | local function setup_conversion(workdir) 372 | -- We must make a copy of the etcupdate db before running pkg install as 373 | -- the etcupdate db matching the pre-pkgbasify system state will be overwritten. 374 | assert(os.execute("cp -a " .. options.rootdir .. "/var/db/etcupdate/current " .. 375 | workdir .. "/current")) 376 | 377 | -- Use a temporary pkg db until we are sure we will carry through with the 378 | -- conversion to avoid polluting the standard one. 379 | -- Let pkg handle actually creating the pkgdb directory so that it sets the 380 | -- permissions it expects and does not error out due to a "too lax" umask. 381 | local tmp_db = workdir .. "/pkgdb/" 382 | 383 | -- Use a temporary repo configuration file for the setup phase so that there 384 | -- is nothing to clean up on failure. 385 | local tmp_repos = workdir .. "/pkgrepos/" 386 | create_base_repo_conf(tmp_repos .. options.repo_name .. ".conf") 387 | 388 | local pkg = "pkg -o PKG_DBDIR=" .. tmp_db .. " -o REPOS_DIR=" .. options.rootdir .. "/etc/pkg," .. tmp_repos .. " " 389 | 390 | assert(os.execute(pkg .. "-o IGNORE_OSVERSION=yes update")) 391 | 392 | if not confirm_version_compatibility(pkg) then 393 | print("Canceled") 394 | os.exit(1) 395 | end 396 | 397 | if options.create_repo_conf then 398 | -- The repo_conf_file is created/overwritten in execute_conversion() 399 | if os.execute("test -e " .. repo_conf_file()) then 400 | if not prompt_yn("Overwrite " .. repo_conf_file() .. "?") then 401 | print("Canceled") 402 | os.exit(1) 403 | end 404 | end 405 | end 406 | 407 | return select_packages(pkg) 408 | end 409 | 410 | local function bootstrap_pkg() 411 | -- Some versions of pkg do not handle `bootstrap -y` gracefully. 412 | -- This has been fixed in https://github.com/freebsd/pkg/pull/2426 but 413 | -- but we still need to check before running the bootstrap in case the pkg 414 | -- version has the broken behavior. 415 | if os.execute("pkg -N > /dev/null 2>&1") then 416 | return true 417 | else 418 | return os.execute("pkg bootstrap -y") 419 | end 420 | end 421 | 422 | local function confirm_risk() 423 | print("Running this tool will irreversibly modify your system to use pkgbase.") 424 | print("This tool and pkgbase are experimental and may result in a broken system.") 425 | print("It is highly recommended to backup your system before proceeding.") 426 | return prompt_yn("Do you accept this risk and wish to continue?") 427 | end 428 | 429 | local function check_etc_symlinks() 430 | local etc 431 | if options.rootdir == "/" then 432 | etc = "/etc" 433 | else 434 | etc = options.rootdir .. "/etc" 435 | end 436 | local known_symlinks = { 437 | [etc .. "/aliases"] = true, 438 | [etc .. "/localtime"] = true, 439 | [etc .. "/motd"] = true, 440 | [etc .. "/os-release"] = true, 441 | [etc .. "/rmt"] = true, 442 | [etc .. "/termcap"] = true, 443 | [etc .. "/unbound"] = true, 444 | } 445 | local found = capture("find " .. etc .. " -type l ! -path '" .. etc .. 446 | "/ssl/*' ! -path '" .. etc .. "/mail/certs/*' 2>/dev/null || true") 447 | 448 | local unexpected = {} 449 | for link in found:gmatch("[^\n]+") do 450 | if not known_symlinks[link] then 451 | table.insert(unexpected, link) 452 | end 453 | end 454 | 455 | if #unexpected == 0 then 456 | return true 457 | end 458 | 459 | print("\nFound unexpected symlinks in " .. etc) 460 | for _, link in ipairs(unexpected) do 461 | print(" " .. link) 462 | end 463 | print([[ 464 | These symlinks will be overwritten by pkg(8) if they conflict with files in 465 | base system packages. Please ensure that your system configuration will not be 466 | broken if these symlinks are overwritten.]]) 467 | return prompt_yn("Continue and overwrite symlinks in " .. etc .. "?") 468 | end 469 | 470 | local function check_no_readonly_var_empty() 471 | if not os.execute("test -e /var/empty/.zfs") then 472 | return true -- Not a zfs filesystem 473 | end 474 | return capture("zfs get -H -o value readonly /var/empty") == "off" 475 | end 476 | 477 | local function check_disk_space() 478 | -- KiB available on the root filesystem 479 | local avail = tonumber(capture( 480 | "df -k " .. options.rootdir .. " | awk '{x=$4}END{print x}'")) 481 | if avail >= (5 * 1024 * 1024) then 482 | return true 483 | else 484 | print([[ 485 | Less than 5GiB space available on the root filesystem. 486 | It is recommended to have at lest 5GiB available before conversion as pkg does 487 | not detect and handle insufficient space gracefully during installation. 488 | ]]) 489 | return prompt_yn("Continue despite possibly insufficient disk space?") 490 | end 491 | end 492 | 493 | local usage = [[ 494 | Usage: pkgbasify.lua [options] 495 | 496 | -h, --help Print this usage message and exit 497 | --force Attempt conversion even if /usr/bin/uname 498 | is owned by a package. 499 | --repo-name Name of the pkgbase repository (Default: FreeBSD-base) 500 | --no-create-repo-conf Don't create a repository configuration, 501 | requires the user to configure a pkgbase repository 502 | --rootdir Operate on the given directory rather than / 503 | --jail Operate on the jail with the given jid or name, 504 | matching the version of the jail's userland. 505 | ]] 506 | 507 | local function parse_options() 508 | local i = 1 509 | while i <= #arg do 510 | if arg[i] == "-h" or arg[i] == "--help" then 511 | io.stdout:write(usage) 512 | os.exit(0) 513 | elseif arg[i] == "--force" then 514 | options.force = true 515 | elseif arg[i] == "--no-create-repo-conf" then 516 | options.create_repo_conf = false 517 | elseif arg[i] == "--repo-name" then 518 | i = i + 1 519 | if i > #arg then 520 | fatal("--repo-name requires an argument") 521 | end 522 | options.repo_name = arg[i] 523 | elseif arg[i] == "--rootdir" then 524 | i = i + 1 525 | if i > #arg then 526 | fatal("--rootdir requires an argument") 527 | end 528 | options.rootdir = arg[i] 529 | elseif arg[i] == "--jail" then 530 | i = i + 1 531 | if i > #arg then 532 | fatal("--jail requires an argument") 533 | end 534 | options.jail = arg[i] 535 | options.rootdir = capture("jls -j " .. options.jail .. " -h path"):match(".+\n(.*)") 536 | else 537 | io.stderr:write("Error: unknown option " .. arg[i] .. "\n") 538 | io.stderr:write(usage) 539 | os.exit(1) 540 | end 541 | i = i + 1 542 | end 543 | end 544 | 545 | local function main() 546 | parse_options() 547 | 548 | if capture("id -u") ~= "0" then 549 | fatal("This tool must be run as the root user.") 550 | end 551 | -- It is possible to have a pkgbase system without pkg bootstrapped, for 552 | -- example if bsdinstall was used to install a pkgbase system. Therefore 553 | -- we must bootstrap pkg to be able to check if the system is already 554 | -- using pkgbase. 555 | if not bootstrap_pkg() then 556 | fatal("Failed to bootstrap pkg.") 557 | end 558 | 559 | if not options.force and 560 | os.execute("pkg --rootdir " .. options.rootdir .. " which /usr/bin/uname > /dev/null 2>&1") 561 | then 562 | fatal([[ 563 | The system is already using pkgbase. 564 | Pass --force to run pkgbasify anyway, for example to fix a partial conversion.]]) 565 | end 566 | if not check_disk_space() then 567 | print("Canceled") 568 | os.exit(1) 569 | end 570 | if options.rootdir == "/" and not check_no_readonly_var_empty() then 571 | print([[ 572 | /var/empty is a readonly zfs filesystem. 573 | This will cause conversion to fail as pkg will be unable to set the time of 574 | /var/empty. Set readonly=off and run pkgbasify again. 575 | ]]) 576 | os.exit(1) 577 | end 578 | if not confirm_risk() then 579 | print("Canceled") 580 | os.exit(1) 581 | end 582 | if not check_etc_symlinks() then 583 | print("Canceled") 584 | os.exit(1) 585 | end 586 | 587 | local workdir = capture("mktemp -d -t pkgbasify") 588 | 589 | local package_list = setup_conversion(workdir) 590 | 591 | if options.rootdir == "/" then 592 | create_boot_environment() 593 | end 594 | 595 | -- This is the point of no return, execute_conversion() will start mutating 596 | -- global system state. 597 | -- Before this point, any error should leave the system to exactly the state 598 | -- it was in before running pkgbasify. 599 | -- After this point, no error should be fatal and pkgbasify should attempt 600 | -- to finish conversion regardless of what happens. 601 | execute_conversion(workdir, package_list) 602 | end 603 | 604 | main() 605 | --------------------------------------------------------------------------------