Skip to content

Commit 2443fda

Browse files
committed
Initial support for USB/external installation
This seems to mostly work as far as Apple's tooling is concerned (with some bugs), but of course m1n1 can't chainload from USB, so it is only useful as a demo so far. Hence, it is only available in expert mode and will only let you install the m1n1/tethered boot option. Signed-off-by: Hector Martin <[email protected]>
1 parent 7cf5c45 commit 2443fda

6 files changed

Lines changed: 184 additions & 27 deletions

File tree

data/installer_data.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"default_os_name": "m1n1 proxy",
7878
"expert": true,
7979
"boot_object": "m1n1.bin",
80+
"external_boot": true,
8081
"partitions": []
8182
}
8283
]

src/diskutil.py

Lines changed: 67 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# SPDX-License-Identifier: MIT
22
import plistlib, subprocess, sys, logging
33
from dataclasses import dataclass
4+
from util import *
45

56
@dataclass
67
class Partition:
@@ -20,7 +21,7 @@ class DiskUtil:
2021
FREE_THRESHOLD = 16 * 1024 * 1024
2122
def __init__(self):
2223
self.verbose = "-v" in sys.argv
23-
24+
2425
def action(self, *args, verbose=False):
2526
if verbose == 2:
2627
capture = False
@@ -100,6 +101,29 @@ def find_system_disk(self):
100101
continue
101102
raise Exception("Could not find system disk")
102103

104+
def find_external_disks(self):
105+
logging.info(f"DiskUtil.find_external_disks()")
106+
disks = []
107+
for name, dsk in self.disks.items():
108+
try:
109+
if dsk["VirtualOrPhysical"] == "Virtual":
110+
continue
111+
if dsk["Internal"]:
112+
continue
113+
if dsk["BusProtocol"] != "USB":
114+
continue
115+
if not dsk["Writable"]:
116+
continue
117+
if not dsk["WholeDisk"]:
118+
continue
119+
if "usb-drd" not in dsk["DeviceTreePath"]:
120+
continue
121+
disks.append(dsk)
122+
except (KeyError, IndexError):
123+
continue
124+
125+
return disks
126+
103127
def get_partition_info(self, dev, refresh_apfs=False):
104128
logging.info(f"DiskUtil.get_partition_info({dev=!r}, {refresh_apfs=!r})")
105129
partinfo = self.get("info", "-plist", dev)
@@ -131,13 +155,21 @@ def get_partition_info(self, dev, refresh_apfs=False):
131155
logging.debug(f"Partition {dev}: {part}")
132156
return part
133157

158+
def get_disk_usable_range(self, dskname):
159+
# GPT overhead aligned to 4K
160+
dsk = self.disk_parts[dskname]
161+
start = 40 * 512
162+
end = align_down(dsk["Size"] - 34 * 512, 4096)
163+
return start, end
164+
134165
def get_partitions(self, dskname):
135166
logging.info(f"DiskUtil.get_partitions({dskname!r})")
136167
dsk = self.disk_parts[dskname]
137168
parts = []
138-
total_size = dsk["Size"]
139-
p = 0
140-
for dskpart in dsk["Partitions"]:
169+
170+
p, total_size = self.get_disk_usable_range(dskname)
171+
172+
for dskpart in dsk.get("Partitions", []):
141173
parts.append(self.get_partition_info(dskpart["DeviceIdentifier"]))
142174
parts.sort(key=lambda i: i.offset)
143175

@@ -182,8 +214,39 @@ def addVolume(self, container, name, **kwargs):
182214
else:
183215
raise
184216

217+
def partitionDisk(self, disk, fs, label, size):
218+
logging.info(f"DiskUtil.wipe_disk({disk}, {fs}, {label}, {size}")
219+
size = str(size)
220+
assert fs.lower() == "apfs"
221+
222+
# diskutil likes to "helpfully" create an EFI partition for us...
223+
self.action("partitionDisk", disk, "1", "GPT", "free", "free", "0", verbose=True)
224+
225+
self.get_list()
226+
parts = self.get_partitions(disk)
227+
assert len(parts) == 2 # EFI and free
228+
part = parts[0]
229+
230+
# So re-format it as APFS...
231+
self.action("eraseVolume", fs, label, part.name)
232+
# And then grow it to the right size
233+
self.action("apfs", "resizeContainer", part.name, size)
234+
# Yes, this is silly.
235+
236+
part = self.get_partition_info(part.name, refresh_apfs=(fs == "apfs"))
237+
logging.info(f"New partition: {part!r}")
238+
return part
239+
185240
def addPartition(self, after, fs, label, size):
241+
logging.info(f"DiskUtil.addPartition({after}, {fs}, {label}, {size})")
186242
size = str(size)
243+
244+
# diskutil can't create partitions on an empty disk...
245+
if (after in self.disk_parts
246+
and not self.disk_parts[after]["Partitions"]
247+
and fs.lower() == "apfs"):
248+
return self.partitionDisk(after, fs, label, size)
249+
187250
self.action("addPartition", after, fs, label, size, verbose=True)
188251

189252
disk = after.rsplit("s", 1)[0]

src/main.py

Lines changed: 100 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ def __init__(self):
8585
self.osi = None
8686
self.m1n1 = "boot/m1n1.bin"
8787
self.m1n1_ver = m1n1.get_version(self.m1n1)
88+
self.sys_disk = None
89+
self.cur_disk = None
8890

8991
def input(self):
9092
self.flush_input()
@@ -225,6 +227,33 @@ def action_install_into_container(self, avail_parts):
225227

226228
self.do_install()
227229

230+
def action_wipe(self):
231+
p_warning("This will wipe all data on the currently selected disk.")
232+
p_warning("Are you sure you want to continue?")
233+
if not self.yesno("Wipe my disk"):
234+
return True
235+
236+
print()
237+
238+
template = self.choose_os()
239+
240+
self.osins = osinstall.OSInstaller(self.dutil, self.data, template)
241+
self.osins.load_package()
242+
243+
min_size = STUB_SIZE + self.osins.min_size
244+
print()
245+
p_message(f"Minimum required space for this OS: {ssize(min_size)}")
246+
247+
start, end = self.dutil.get_disk_usable_range(self.cur_disk)
248+
os_size = self.get_os_size_and_info(end - start, min_size, template)
249+
250+
p_progress(f"Partitioning the whole disk ({self.cur_disk})")
251+
self.part = self.dutil.partitionDisk(self.cur_disk, "apfs", self.osins.name, STUB_SIZE)
252+
253+
p_progress(f"Creating new stub macOS named {self.osins.name}")
254+
logging.info(f"Creating stub macOS: {self.osins.name}")
255+
self.do_install(os_size)
256+
228257
def action_install_into_free(self, avail_free):
229258
template = self.choose_os()
230259

@@ -256,32 +285,41 @@ def action_install_into_free(self, avail_free):
256285
print()
257286
p_message(f"Available free space: {ssize(free_part.size)}")
258287

288+
os_size = self.get_os_size_and_info(free_part.size, min_size, template)
289+
290+
p_progress(f"Creating new stub macOS named {self.osins.name}")
291+
logging.info(f"Creating stub macOS: {self.osins.name}")
292+
self.part = self.dutil.addPartition(free_part.name, "apfs", self.osins.name, STUB_SIZE)
293+
294+
self.do_install(os_size)
295+
296+
def get_os_size_and_info(self, free_size, min_size, template):
259297
os_size = None
260298
if self.osins.expandable:
261299
print()
262300
p_question("How much space should be allocated to the new OS?")
263301
p_message(" You can enter a size such as '1GB', a fraction such as '50%',")
264302
p_message(" the word 'min' for the smallest allowable size, or")
265303
p_message(" the word 'max' to use all available space.")
266-
min_perc = 100 * min_size / free_part.size
304+
min_perc = 100 * min_size / free_size
267305
while True:
268306
os_size = self.get_size("New OS size", default="max",
269-
min=min_size, max=free_part.size,
270-
total=free_part.size)
307+
min=min_size, max=free_size,
308+
total=free_size)
271309
if os_size is None:
272310
continue
273311
os_size = align_down(os_size, PART_ALIGN)
274312
if os_size < min_size:
275313
p_error(f"Size is too small, please enter a value > {ssize(min_size)} ({min_perc:.2f}%)")
276314
continue
277-
if os_size > free_part.size:
278-
p_error(f"Size is too large, please enter a value < {ssize(free_part.size)}")
315+
if os_size > free_size:
316+
p_error(f"Size is too large, please enter a value < {ssize(free_size)}")
279317
continue
280318
break
281319

282320
print()
283321
p_message(f"The new OS will be allocated {ssize(os_size)} of space,")
284-
p_message(f"leaving {ssize(free_part.size - os_size)} of free space.")
322+
p_message(f"leaving {ssize(free_size - os_size)} of free space.")
285323
os_size -= STUB_SIZE
286324

287325
print()
@@ -296,12 +334,7 @@ def action_install_into_free(self, avail_free):
296334
logging.info(f"Chosen IPSW version: {ipsw.version}")
297335
self.ins = stub.StubInstaller(self.sysinfo, self.dutil, self.osinfo)
298336
self.ins.load_ipsw(ipsw)
299-
300-
p_progress(f"Creating new stub macOS named {label}")
301-
logging.info(f"Creating stub macOS: {label}")
302-
self.part = self.dutil.addPartition(free_part.name, "apfs", label, STUB_SIZE)
303-
304-
self.do_install(os_size)
337+
return os_size
305338

306339
def action_resume_or_upgrade(self, oses, upgrade):
307340
choices = {str(i): f"{p.desc}\n {str(o)}" for i, (p, o) in enumerate(oses)}
@@ -422,6 +455,8 @@ def choose_os(self):
422455
os_list = self.data["os_list"]
423456
if not self.expert:
424457
os_list = [i for i in os_list if not i.get("expert", False)]
458+
if self.cur_disk != self.sys_disk:
459+
os_list = [i for i in os_list if i.get("external_boot", False)]
425460
p_question("Choose an OS to install:")
426461
idx = self.choice("OS", [i["name"] for i in os_list])
427462
os = os_list[idx]
@@ -736,6 +771,22 @@ def action_resize(self, resizable):
736771

737772
return True
738773

774+
def action_select_disk(self):
775+
choices = {"1": "Internal storage"}
776+
777+
for i, disk in enumerate(self.external_disks):
778+
choices[str(i + 2)] = f"{disk['IORegistryEntryName']} ({ssize(disk['Size'])})"
779+
780+
print()
781+
p_question("Choose a disk:")
782+
idx = int(self.choice("Disk", choices))
783+
if idx == 1:
784+
self.cur_disk = self.sys_disk
785+
else:
786+
self.cur_disk = self.external_disks[idx - 2]["DeviceIdentifier"]
787+
788+
return True
789+
739790
def main(self):
740791
print()
741792
p_message("Welcome to the Asahi Linux installer!")
@@ -803,13 +854,24 @@ def main_loop(self):
803854
p_progress("Collecting partition information...")
804855
self.dutil = diskutil.DiskUtil()
805856
self.dutil.get_info()
806-
self.sysdsk = self.dutil.find_system_disk()
807-
p_info(f" System disk: {col()}{self.sysdsk}")
808-
self.parts = self.dutil.get_partitions(self.sysdsk)
857+
if self.sys_disk is None:
858+
self.cur_disk = self.sys_disk = self.dutil.find_system_disk()
859+
860+
p_info(f" System disk: {col()}{self.sys_disk}")
861+
862+
if self.expert:
863+
self.external_disks = self.dutil.find_external_disks()
864+
else:
865+
self.external_disks = None
866+
867+
if self.external_disks:
868+
p_info(f" Found {len(self.external_disks)} external disk(s)")
869+
870+
self.parts = self.dutil.get_partitions(self.cur_disk)
809871
print()
810872

811873
p_progress("Collecting OS information...")
812-
self.osinfo = osenum.OSEnum(self.sysinfo, self.dutil, self.sysdsk)
874+
self.osinfo = osenum.OSEnum(self.sysinfo, self.dutil, self.cur_disk)
813875
self.osinfo.collect(self.parts)
814876

815877
parts_free = []
@@ -842,9 +904,14 @@ def main_loop(self):
842904
p.desc = f"{p.type} ({ssize(p.size)})"
843905

844906
print()
845-
p_message(f"Partitions in system disk ({self.sysdsk}):")
907+
if self.cur_disk == self.sys_disk:
908+
t = "system"
909+
else:
910+
t = "external"
911+
p_message(f"Partitions in {t} disk ({self.cur_disk}):")
846912

847-
self.cur_os = None
913+
if self.cur_disk == self.sys_disk:
914+
self.cur_os = None
848915
self.is_sfr_recovery = self.sysinfo.boot_vgid in (osenum.UUID_SROS, osenum.UUID_FROS)
849916
default_os = None
850917

@@ -853,6 +920,8 @@ def main_loop(self):
853920
u = col(RED) + "?" + col()
854921
d = col(BRIGHT) + "*" + col()
855922

923+
is_gpt = self.dutil.disks[self.cur_disk]["Content"] == "GUID_partition_scheme"
924+
856925
for i, p in enumerate(self.parts):
857926
if p.desc is None:
858927
continue
@@ -898,14 +967,20 @@ def main_loop(self):
898967
if oses_incomplete:
899968
actions["p"] = "Repair an incomplete installation"
900969
default = default or "p"
901-
if parts_free:
970+
if parts_free and is_gpt:
902971
actions["f"] = "Install an OS into free space"
903972
default = default or "f"
904-
if parts_empty_apfs and False: # This feature is confusing, disable it for now
973+
if parts_empty_apfs and is_gpt and False: # This feature is confusing, disable it for now
905974
actions["a"] = "Install an OS into an existing APFS container"
906-
if parts_resizable:
975+
if parts_resizable and is_gpt:
907976
actions["r"] = "Resize an existing partition to make space for a new OS"
908977
default = default or "r"
978+
if self.cur_disk != self.sys_disk:
979+
actions["w"] = "Wipe and install into the whole disk"
980+
# Never make this default!
981+
if self.external_disks:
982+
actions["d"] = "Select another disk for installation"
983+
default = default or "d"
909984
if oses_upgradable:
910985
actions["m"] = "Upgrade m1n1 on an existing OS"
911986
default = default or "m"
@@ -931,6 +1006,10 @@ def main_loop(self):
9311006
return self.action_repair_or_upgrade(oses_upgradable, upgrade=True)
9321007
elif act == "p":
9331008
return self.action_repair_or_upgrade(oses_incomplete, upgrade=False)
1009+
elif act == "d":
1010+
return self.action_select_disk()
1011+
elif act == "w":
1012+
return self.action_wipe()
9341013
elif act == "q":
9351014
return False
9361015

src/osenum.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ class OSInfo:
2323
preboot: object = None
2424
recovery: object = None
2525
rec_vgid: str = None
26+
preboot_vgid: str = None
2627
bp: object = None
2728
paired: bool = False
2829
admin_users: object = None
@@ -147,6 +148,7 @@ def collect_os(self, part, volumes, vgid):
147148
logging.info(f" Failed to mount Data (FileVault?)")
148149

149150
rec_vgid = volumes["Recovery"]["APFSVolumeUUID"]
151+
preboot_vgid = volumes["Preboot"]["APFSVolumeUUID"]
150152

151153
stub = not os.path.exists(os.path.join(mounts["System"], "Library"))
152154

@@ -161,7 +163,8 @@ def collect_os(self, part, volumes, vgid):
161163
data=mounts["Data"],
162164
preboot=mounts["Preboot"],
163165
recovery=mounts["Recovery"],
164-
rec_vgid=rec_vgid)
166+
rec_vgid=rec_vgid,
167+
preboot_vgid=preboot_vgid)
165168

166169
for name in ("SystemVersion.plist", "SystemVersion-disabled.plist"):
167170
try:

src/step2/step2.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
set -e
55

66
VGID="##VGID##"
7+
PREBOOT="##PREBOOT##"
78

89
self="$0"
910
cd "${self%%step2.sh}"
@@ -101,6 +102,14 @@ done
101102
echo
102103
echo
103104

105+
if [ -e "/System/Volumes/iSCPreboot/$VGID/boot" ]; then
106+
# This is an external volume, and kmutil has a problem with trying to pick
107+
# up the AdminUserRecoveryInfo.plist from the wrong place. Work around that.
108+
diskutil mount "$PREBOOT"
109+
preboot="$(diskutil info "$PREBOOT" | grep "Mount Point" | sed 's, *Mount Point: *,,')"
110+
cp -R "$preboot/$VGID/var" "/System/Volumes/iSCPreboot/$VGID/"
111+
fi
112+
104113
while ! kmutil configure-boot -c boot.bin --raw --entry-point 2048 --lowest-virtual-address 0 -v "$system_dir"; do
105114
echo
106115
echo "kmutil failed. Did you mistype your password?"

0 commit comments

Comments
 (0)