pax_global_header00006660000000000000000000000064145121715160014515gustar00rootroot0000000000000052 comment=41edadfc868137360ae2d2e519be8d8cea9cba14 asahi-installer-0.6.12/000077500000000000000000000000001451217151600146635ustar00rootroot00000000000000asahi-installer-0.6.12/.gitignore000066400000000000000000000001141451217151600166470ustar00rootroot00000000000000__pycache__ dl package installer.tar.gz latest *.ipsw *.egg-info build dist asahi-installer-0.6.12/.gitmodules000066400000000000000000000002451451217151600170410ustar00rootroot00000000000000[submodule "artwork"] path = artwork url = https://github.com/AsahiLinux/artwork.git [submodule "m1n1"] path = m1n1 url = https://github.com/AsahiLinux/m1n1.git asahi-installer-0.6.12/LICENSE000066400000000000000000000020601451217151600156660ustar00rootroot00000000000000Copyright (c) 2021 The Asahi Linux contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. asahi-installer-0.6.12/README.md000066400000000000000000000005301451217151600161400ustar00rootroot00000000000000# Asahi Linux installer TODO: document ## License Copyright The Asahi Linux Contributors The Asahi Linux installer is distributed under the MIT license. See LICENSE for the license text. This installer vendors [python-asn1](https://github.com/andrivet/python-asn1), which is distributed under the same license. ## Building `./build.sh` asahi-installer-0.6.12/artwork/000077500000000000000000000000001451217151600163545ustar00rootroot00000000000000asahi-installer-0.6.12/asahi_firmware/000077500000000000000000000000001451217151600176445ustar00rootroot00000000000000asahi-installer-0.6.12/asahi_firmware/__init__.py000066400000000000000000000000001451217151600217430ustar00rootroot00000000000000asahi-installer-0.6.12/asahi_firmware/asmedia.py000066400000000000000000000010751451217151600216240ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import logging, struct from .core import FWFile log = logging.getLogger("asahi_firmware.asmedia") MAGIC = b"2214A_RCFG" def extract_asmedia(kernel): try: off = kernel.index(MAGIC) except ValueError: raise Exception("Could not find ASMedia firmware") size = struct.unpack(" None """Constructor.""" self.m_stack = None def start(self): # type: () -> None """This method instructs the encoder to start encoding a new ASN.1 output. This method may be called at any time to reset the encoder, and resets the current output (if any). """ self.m_stack = [[]] def enter(self, nr, cls=None): # type: (int, int) -> None """This method starts the construction of a constructed type. Args: nr (int): The desired ASN.1 type. Use ``Numbers`` enumeration. cls (int): This optional parameter specifies the class of the constructed type. The default class to use is the universal class. Use ``Classes`` enumeration. Returns: None Raises: `Error` """ if self.m_stack is None: raise Error('Encoder not initialized. Call start() first.') if cls is None: cls = Classes.Universal self._emit_tag(nr, Types.Constructed, cls) self.m_stack.append([]) def leave(self): # type: () -> None """This method completes the construction of a constructed type and writes the encoded representation to the output buffer. """ if self.m_stack is None: raise Error('Encoder not initialized. Call start() first.') if len(self.m_stack) == 1: raise Error('Tag stack is empty.') value = b''.join(self.m_stack[-1]) del self.m_stack[-1] self._emit_length(len(value)) self._emit(value) def write(self, value, nr=None, typ=None, cls=None): # type: (object, int, int, int) -> None """This method encodes one ASN.1 tag and writes it to the output buffer. Note: Normally, ``value`` will be the only parameter to this method. In this case Python-ASN1 will autodetect the correct ASN.1 type from the type of ``value``, and will output the encoded value based on this type. Args: value (any): The value of the ASN.1 tag to write. Python-ASN1 will try to autodetect the correct ASN.1 type from the type of ``value``. nr (int): If the desired ASN.1 type cannot be autodetected or is autodetected wrongly, the ``nr`` parameter can be provided to specify the ASN.1 type to be used. Use ``Numbers`` enumeration. typ (int): This optional parameter can be used to write constructed types to the output by setting it to indicate the constructed encoding type. In this case, ``value`` must already be valid ASN.1 encoded data as plain Python bytes. This is not normally how constructed types should be encoded though, see `Encoder.enter()` and `Encoder.leave()` for the recommended way of doing this. Use ``Types`` enumeration. cls (int): This parameter can be used to override the class of the ``value``. The default class is the universal class. Use ``Classes`` enumeration. Returns: None Raises: `Error` """ if self.m_stack is None: raise Error('Encoder not initialized. Call start() first.') if typ is None: typ = Types.Primitive if cls is None: cls = Classes.Universal if cls != Classes.Universal and nr is None: raise Error('Please specify a tag number (nr) when using classes Application, Context or Private') if nr is None: if isinstance(value, bool): nr = Numbers.Boolean elif isinstance(value, int): nr = Numbers.Integer elif isinstance(value, str): nr = Numbers.PrintableString elif isinstance(value, bytes): nr = Numbers.OctetString elif value is None: nr = Numbers.Null value = self._encode_value(cls, nr, value) self._emit_tag(nr, typ, cls) self._emit_length(len(value)) self._emit(value) def output(self): # type: () -> bytes """This method returns the encoded ASN.1 data as plain Python ``bytes``. This method can be called multiple times, also during encoding. In the latter case the data that has been encoded so far is returned. Note: It is an error to call this method if the encoder is still constructing a constructed type, i.e. if `Encoder.enter()` has been called more times that `Encoder.leave()`. Returns: bytes: The DER encoded ASN.1 data. Raises: `Error` """ if self.m_stack is None: raise Error('Encoder not initialized. Call start() first.') if len(self.m_stack) != 1: raise Error('Stack is not empty.') output = b''.join(self.m_stack[0]) return output def _emit_tag(self, nr, typ, cls): # type: (int, int, int) -> None """Emit a tag.""" if nr < 31: self._emit_tag_short(nr, typ, cls) else: self._emit_tag_long(nr, typ, cls) def _emit_tag_short(self, nr, typ, cls): # type: (int, int, int) -> None """Emit a short (< 31 bytes) tag.""" assert nr < 31 self._emit(bytes([nr | typ | cls])) def _emit_tag_long(self, nr, typ, cls): # type: (int, int, int) -> None """Emit a long (>= 31 bytes) tag.""" head = bytes([typ | cls | 0x1f]) self._emit(head) values = [(nr & 0x7f)] nr >>= 7 while nr: values.append((nr & 0x7f) | 0x80) nr >>= 7 values.reverse() for val in values: self._emit(bytes([val])) def _emit_length(self, length): # type: (int) -> None """Emit length octets.""" if length < 128: self._emit_length_short(length) else: self._emit_length_long(length) def _emit_length_short(self, length): # type: (int) -> None """Emit the short length form (< 128 octets).""" assert length < 128 self._emit(bytes([length])) def _emit_length_long(self, length): # type: (int) -> None """Emit the long length form (>= 128 octets).""" values = [] while length: values.append(length & 0xff) length >>= 8 values.reverse() # really for correctness as this should not happen anytime soon assert len(values) < 127 head = bytes([0x80 | len(values)]) self._emit(head) for val in values: self._emit(bytes([val])) def _emit(self, s): # type: (bytes) -> None """Emit raw bytes.""" assert isinstance(s, bytes) self.m_stack[-1].append(s) def _encode_value(self, cls, nr, value): # type: (int, int, any) -> bytes """Encode a value.""" if cls != Classes.Universal: return value if nr in (Numbers.Integer, Numbers.Enumerated): return self._encode_integer(value) if nr in (Numbers.OctetString, Numbers.PrintableString, Numbers.UTF8String, Numbers.IA5String, Numbers.UnicodeString, Numbers.UTCTime): return self._encode_octet_string(value) if nr == Numbers.BitString: return self._encode_bit_string(value) if nr == Numbers.Boolean: return self._encode_boolean(value) if nr == Numbers.Null: return self._encode_null() if nr == Numbers.ObjectIdentifier: return self._encode_object_identifier(value) return value @staticmethod def _encode_boolean(value): # type: (bool) -> bytes """Encode a boolean.""" return value and bytes(b'\xff') or bytes(b'\x00') @staticmethod def _encode_integer(value): # type: (int) -> bytes """Encode an integer.""" if value < 0: value = -value negative = True limit = 0x80 else: negative = False limit = 0x7f values = [] while value > limit: values.append(value & 0xff) value >>= 8 values.append(value & 0xff) if negative: # create two's complement for i in range(len(values)): # Invert bits values[i] = 0xff - values[i] for i in range(len(values)): # Add 1 values[i] += 1 if values[i] <= 0xff: break assert i != len(values) - 1 values[i] = 0x00 if negative and values[len(values) - 1] == 0x7f: # Two's complement corner case values.append(0xff) values.reverse() return bytes(values) @staticmethod def _encode_octet_string(value): # type: (object) -> bytes """Encode an octetstring.""" # Use the primitive encoding assert isinstance(value, str) or isinstance(value, bytes) if isinstance(value, str): return value.encode('utf-8') else: return value @staticmethod def _encode_bit_string(value): # type: (object) -> bytes """Encode a bitstring. Assumes no unused bytes.""" # Use the primitive encoding assert isinstance(value, bytes) return b'\x00' + value @staticmethod def _encode_null(): # type: () -> bytes """Encode a Null value.""" return bytes(b'') _re_oid = re.compile(r'^[0-9]+(\.[0-9]+)+$') def _encode_object_identifier(self, oid): # type: (str) -> bytes """Encode an object identifier.""" if not self._re_oid.match(oid): raise Error('Illegal object identifier') cmps = list(map(int, oid.split('.'))) if cmps[0] > 39 or cmps[1] > 39: raise Error('Illegal object identifier') cmps = [40 * cmps[0] + cmps[1]] + cmps[2:] cmps.reverse() result = [] for cmp_data in cmps: result.append(cmp_data & 0x7f) while cmp_data > 0x7f: cmp_data >>= 7 result.append(0x80 | (cmp_data & 0x7f)) result.reverse() return bytes(result) class Decoder(object): """ASN.1 decoder. Understands BER (and DER which is a subset).""" def __init__(self): # type: () -> None """Constructor.""" self.m_stack = None self.m_tag = None def start(self, data): # type: (bytes) -> None """This method instructs the decoder to start decoding the ASN.1 input ``data``, which must be a passed in as plain Python bytes. This method may be called at any time to start a new decoding job. If this method is called while currently decoding another input, that decoding context is discarded. Note: It is not necessary to specify the encoding because the decoder assumes the input is in BER or DER format. Args: data (bytes): ASN.1 input, in BER or DER format, to be decoded. Returns: None Raises: `Error` """ if not isinstance(data, bytes): raise Error('Expecting bytes instance.') self.m_stack = [[0, bytes(data)]] self.m_tag = None def peek(self): # type: () -> Tag """This method returns the current ASN.1 tag (i.e. the tag that a subsequent `Decoder.read()` call would return) without updating the decoding offset. In case no more data is available from the input, this method returns ``None`` to signal end-of-file. This method is useful if you don't know whether the next tag will be a primitive or a constructed tag. Depending on the return value of `peek`, you would decide to either issue a `Decoder.read()` in case of a primitive type, or an `Decoder.enter()` in case of a constructed type. Note: Because this method does not advance the current offset in the input, calling it multiple times in a row will return the same value for all calls. Returns: `Tag`: The current ASN.1 tag. Raises: `Error` """ if self.m_stack is None: raise Error('No input selected. Call start() first.') if self._end_of_input(): return None if self.m_tag is None: self.m_tag = self._read_tag() return self.m_tag def read(self, tagnr=None): # type: (Number) -> (Tag, any) """This method decodes one ASN.1 tag from the input and returns it as a ``(tag, value)`` tuple. ``tag`` is a 3-tuple ``(nr, typ, cls)``, while ``value`` is a Python object representing the ASN.1 value. The offset in the input is increased so that the next `Decoder.read()` call will return the next tag. In case no more data is available from the input, this method returns ``None`` to signal end-of-file. Returns: `Tag`, value: The current ASN.1 tag and its value. Raises: `Error` """ if self.m_stack is None: raise Error('No input selected. Call start() first.') if self._end_of_input(): return None tag = self.peek() length = self._read_length() if tagnr is None: tagnr = tag.nr value = self._read_value(tag.cls, tagnr, length) self.m_tag = None return tag, value def eof(self): # type: () -> bool """Return True if we are at the end of input. Returns: bool: True if all input has been decoded, and False otherwise. """ return self._end_of_input() def enter(self): # type: () -> None """This method enters the constructed type that is at the current decoding offset. Note: It is an error to call `Decoder.enter()` if the to be decoded ASN.1 tag is not of a constructed type. Returns: None """ if self.m_stack is None: raise Error('No input selected. Call start() first.') tag = self.peek() if tag.typ != Types.Constructed: raise Error('Cannot enter a non-constructed tag.') length = self._read_length() bytes_data = self._read_bytes(length) self.m_stack.append([0, bytes_data]) self.m_tag = None def leave(self): # type: () -> None """This method leaves the last constructed type that was `Decoder.enter()`-ed. Note: It is an error to call `Decoder.leave()` if the current ASN.1 tag is not of a constructed type. Returns: None """ if self.m_stack is None: raise Error('No input selected. Call start() first.') if len(self.m_stack) == 1: raise Error('Tag stack is empty.') del self.m_stack[-1] self.m_tag = None def _read_tag(self): # type: () -> Tag """Read a tag from the input.""" byte = self._read_byte() cls = byte & 0xc0 typ = byte & 0x20 nr = byte & 0x1f if nr == 0x1f: # Long form of tag encoding nr = 0 while True: byte = self._read_byte() nr = (nr << 7) | (byte & 0x7f) if not byte & 0x80: break return Tag(nr=nr, typ=typ, cls=cls) def _read_length(self): # type: () -> int """Read a length from the input.""" byte = self._read_byte() if byte & 0x80: count = byte & 0x7f if count == 0x7f: raise Error('ASN1 syntax error') bytes_data = self._read_bytes(count) length = 0 for byte in bytes_data: length = (length << 8) | int(byte) try: length = int(length) except OverflowError: pass else: length = byte return length def _read_value(self, cls, nr, length): # type: (int, int, int) -> any """Read a value from the input.""" bytes_data = self._read_bytes(length) if cls != Classes.Universal: value = bytes_data elif nr == Numbers.Boolean: value = self._decode_boolean(bytes_data) elif nr in (Numbers.Integer, Numbers.Enumerated): value = self._decode_integer(bytes_data) elif nr == Numbers.OctetString: value = self._decode_octet_string(bytes_data) elif nr == Numbers.Null: value = self._decode_null(bytes_data) elif nr == Numbers.ObjectIdentifier: value = self._decode_object_identifier(bytes_data) elif nr in (Numbers.PrintableString, Numbers.IA5String, Numbers.UTF8String, Numbers.UTCTime): value = self._decode_printable_string(bytes_data) elif nr == Numbers.BitString: value = self._decode_bitstring(bytes_data) else: value = bytes_data return value def _read_byte(self): # type: () -> int """Return the next input byte, or raise an error on end-of-input.""" index, input_data = self.m_stack[-1] try: byte = input_data[index] except IndexError: raise Error('Premature end of input.') self.m_stack[-1][0] += 1 return byte def _read_bytes(self, count): # type: (int) -> bytes """Return the next ``count`` bytes of input. Raise error on end-of-input.""" index, input_data = self.m_stack[-1] bytes_data = input_data[index:index + count] if len(bytes_data) != count: raise Error('Premature end of input.') self.m_stack[-1][0] += count return bytes_data def _end_of_input(self): # type: () -> bool """Return True if we are at the end of input.""" index, input_data = self.m_stack[-1] assert not index > len(input_data) return index == len(input_data) @staticmethod def _decode_boolean(bytes_data): # type: (bytes) -> bool """Decode a boolean value.""" if len(bytes_data) != 1: raise Error('ASN1 syntax error') if bytes_data[0] == 0: return False return True @staticmethod def _decode_integer(bytes_data): # type: (bytes) -> int """Decode an integer value.""" values = [int(b) for b in bytes_data] # check if the integer is normalized if len(values) > 1 and (values[0] == 0xff and values[1] & 0x80 or values[0] == 0x00 and not (values[1] & 0x80)): raise Error('ASN1 syntax error') negative = values[0] & 0x80 if negative: # make positive by taking two's complement for i in range(len(values)): values[i] = 0xff - values[i] for i in range(len(values) - 1, -1, -1): values[i] += 1 if values[i] <= 0xff: break assert i > 0 values[i] = 0x00 value = 0 for val in values: value = (value << 8) | val if negative: value = -value try: value = int(value) except OverflowError: pass return value @staticmethod def _decode_octet_string(bytes_data): # type: (bytes) -> bytes """Decode an octet string.""" return bytes_data @staticmethod def _decode_null(bytes_data): # type: (bytes) -> any """Decode a Null value.""" if len(bytes_data) != 0: raise Error('ASN1 syntax error') return None @staticmethod def _decode_object_identifier(bytes_data): # type: (bytes) -> str """Decode an object identifier.""" result = [] value = 0 for i in range(len(bytes_data)): byte = int(bytes_data[i]) if value == 0 and byte == 0x80: raise Error('ASN1 syntax error') value = (value << 7) | (byte & 0x7f) if not byte & 0x80: result.append(value) value = 0 if len(result) == 0 or result[0] > 1599: raise Error('ASN1 syntax error') result = [result[0] // 40, result[0] % 40] + result[1:] result = list(map(str, result)) return str('.'.join(result)) @staticmethod def _decode_printable_string(bytes_data): # type: (bytes) -> str """Decode a printable string.""" return bytes_data.decode('utf-8') @staticmethod def _decode_bitstring(bytes_data): # type: (bytes) -> str """Decode a bitstring.""" if len(bytes_data) == 0: raise Error('ASN1 syntax error') num_unused_bits = bytes_data[0] if not (0 <= num_unused_bits <= 7): raise Error('ASN1 syntax error') if num_unused_bits == 0: return bytes_data[1:] # Shift off unused bits remaining = bytearray(bytes_data[1:]) bitmask = (1 << num_unused_bits) - 1 removed_bits = 0 for i in range(len(remaining)): byte = int(remaining[i]) remaining[i] = (byte >> num_unused_bits) | (removed_bits << num_unused_bits) removed_bits = byte & bitmask return bytes(remaining) asahi-installer-0.6.12/asahi_firmware/bluetooth.py000066400000000000000000000102011451217151600222150ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import logging, os, os.path, re, sys from collections import namedtuple, defaultdict from .core import FWFile log = logging.getLogger("asahi_firmware.bluetooth") BluetoothChip = namedtuple( "BluetoothChip", ("chip", "stepping", "board_type", "vendor") ) # 13.5 is missing PTBs for these. 4388B0 probably never shipped to end users here. INCOMPLETE_CHIPS = set([ ('4388', 'b0', 'apple,tokara'), ('4388', 'b0', 'apple,amami'), ]) class BluetoothFWCollection(object): VENDORMAP = { "MUR": "m", "USI": "u", "GEN": None, } STRIP_SUFFIXES = [ "ES2" ] def __init__(self, source_path): self.fwfiles = defaultdict(lambda: [None, None]) self.load(source_path) def load(self, source_path): for fname in os.listdir(source_path): root, ext = os.path.splitext(fname) # index for bin and ptb inside self.fwfiles if ext == ".bin": idx = 0 elif ext == ".ptb": idx = 1 else: # skip firmware for older (UART) chips continue # skip T2 _DEV firmware if "_DEV" in root: continue chip = self.parse_fname(root) if chip is None: continue if self.fwfiles[chip][idx] is not None: log.warning(f"duplicate entry for {chip}: {self.fwfiles[chip][idx].name} and now {fname}") continue path = os.path.join(source_path, fname) with open(path, "rb") as f: data = f.read() self.fwfiles[chip][idx] = FWFile(fname, data) def parse_fname(self, fname): fname = fname.split("_") match = re.fullmatch("bcm(43[0-9]{2})([a-z][0-9])", fname[0].lower()) if not match: log.warning(f"Unexpected firmware file: {fname}") return None chip, stepping = match.groups() # board type is either preceded by PCIE_macOS or by PCIE try: pcie_offset = fname.index("PCIE") except: log.warning(f"Can't find board type in {fname}") return None bt_offset = pcie_offset + 1 if fname[bt_offset] == "macOS": bt_offset += 1 # 4388 has this extra prefix, meh if fname[bt_offset] == "Willamette": bt_offset += 1 board_type = fname[bt_offset] for i in self.STRIP_SUFFIXES: board_type = board_type.rstrip(i) board_type = "apple," + board_type.lower() # make sure we can identify exactly one vendor otp_values = set() for vendor, otp_value in self.VENDORMAP.items(): if vendor in fname: otp_values.add(otp_value) if len(otp_values) != 1: log.warning(f"Unable to determine vendor ({otp_values}) in {fname}") return None vendor = otp_values.pop() return BluetoothChip( chip=chip, stepping=stepping, board_type=board_type, vendor=vendor ) def files(self): for chip, (bin, ptb) in self.fwfiles.items(): fname_base = f"brcm/brcmbt{chip.chip}{chip.stepping}-{chip.board_type}" if chip.vendor is not None: fname_base += f"-{chip.vendor}" if bin is None: log.warning(f"no bin for {chip}") continue else: yield fname_base + ".bin", bin if ptb is None: if (chip.chip, chip.stepping, chip.board_type) not in INCOMPLETE_CHIPS: log.warning(f"no ptb for {chip}") continue else: yield fname_base + ".ptb", ptb if __name__ == "__main__": col = BluetoothFWCollection(sys.argv[1]) if len(sys.argv) > 2: from . import FWPackage pkg = FWPackage(sys.argv[2]) pkg.add_files(sorted(col.files())) pkg.close() for i in pkg.manifest: print(i) else: for name, fwfile in col.files(): print(name, f"{fwfile.name} ({len(fwfile.data)} bytes)") asahi-installer-0.6.12/asahi_firmware/core.py000066400000000000000000000053011451217151600211450ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import tarfile, io, logging, os.path from hashlib import sha256 from . import cpio UBOOT_FILES = set([ "asmedia/asm2214a-apple.bin" ]) class FWFile(object): def __init__(self, name, data): self.name = name self.data = data self.sha = sha256(data).hexdigest() def __repr__(self): return f"FWFile({self.name!r}, <{self.sha[:16]}>)" def __eq__(self, other): if other is None: return False return self.sha == other.sha def __hash__(self): return hash(self.sha) class FWPackage(object): def __init__(self, path): self.closed = False self.path = path self.tar_path = os.path.join(path, "firmware.tar") self.cpio_path = os.path.join(path, "firmware.cpio") self.tarfile = tarfile.open(self.tar_path, mode="w") self.cpiofile = cpio.CPIO(self.cpio_path) self.hashes = {} self.manifest = [] def close(self): if self.closed: return self.closed = True ti = tarfile.TarInfo("vendorfw/.vendorfw.manifest") ti.type = tarfile.REGTYPE fd = io.BytesIO() for i in self.manifest: fd.write(i.encode("ascii") + b"\n") ti.size = fd.tell() fd.seek(0) self.cpiofile.addfile(ti, fd) self.tarfile.close() self.cpiofile.close() with open(os.path.join(self.path, "manifest.txt"), "w") as fd: for i in self.manifest: fd.write(i + "\n") def add_file(self, name, data): ti = tarfile.TarInfo(name) fd = None if data.sha in self.hashes: ti.type = tarfile.LNKTYPE ti.linkname = self.hashes[data.sha] self.manifest.append(f"LINK {name} {ti.linkname}") else: ti.type = tarfile.REGTYPE ti.size = len(data.data) fd = io.BytesIO(data.data) self.hashes[data.sha] = name self.manifest.append(f"FILE {name} SHA256 {data.sha}") logging.info(f"+ {self.manifest[-1]}") self.tarfile.addfile(ti, fd) if fd is not None: fd.seek(0) ti.name = os.path.join("vendorfw", ti.name) if ti.linkname: ti.linkname = os.path.join("vendorfw", ti.linkname) self.cpiofile.addfile(ti, fd) if name in UBOOT_FILES: path = os.path.join(self.path, "u-boot", name) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "wb") as fd: fd.write(data.data) def add_files(self, it): for name, data in it: self.add_file(name, data) def __del__(self): self.close() asahi-installer-0.6.12/asahi_firmware/cpio.py000066400000000000000000000045671451217151600211640ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import os.path, tarfile class CPIO: DIR = 0o040755 FILE = 0o100644 def __init__(self, filename): self.fd = open(filename, "wb") self.closed = False self.dirs = set() self.inode = 1 self.nlink = {} self.nlinkoff = {} self.inodemap = {} def cpio_hdr(self, name, mode, size, target=None): if target is not None: inode = self.inodemap[target] self.nlink[inode] += 1 p = self.fd.tell() for i in self.nlinkoff[inode]: self.fd.seek(i) self.fd.write(b"%08x" % self.nlink[inode]) self.fd.seek(p) else: inode = self.inode self.inode += 1 self.nlink[inode] = 1 self.nlinkoff[inode] = [] self.inodemap[name] = inode p = self.fd.tell() if p & 3: self.fd.write(bytes(4 - (p & 3))) self.fd.write(b"070701") self.nlinkoff[inode].append(self.fd.tell() + 8 * 4) hdr = [ inode, mode, 0, # uid 0, # gid self.nlink[inode], 0, # mtime size, 0, # maj 0, # min 0, # rmaj 0, # rmin len(name) + 1, 0, # chksum ] self.fd.write(b"".join(b"%08x" % i for i in hdr)) self.fd.write(name.encode("ascii") + b"\x00") p = self.fd.tell() if p & 3: self.fd.write(bytes(4 - (p & 3))) def addfile(self, ti, fd): path = "" for i in ti.name.split("/")[:-1]: if not i: continue path = os.path.join(path, i) if path not in self.dirs: self.cpio_hdr(path, self.DIR, 0) self.dirs.add(path) if ti.type == tarfile.LNKTYPE: self.cpio_hdr(ti.name, self.FILE, 0, ti.linkname) elif ti.type == tarfile.REGTYPE: self.cpio_hdr(ti.name, self.FILE, ti.size) self.fd.write(fd.read()) else: raise Exception(f"Unsupported file type {ti.type}") def close(self): if self.closed: return self.closed = True self.cpio_hdr("TRAILER!!!", self.FILE, 0); self.fd.close() def __del__(self): self.close() asahi-installer-0.6.12/asahi_firmware/img4.py000066400000000000000000000044661451217151600210700ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import sys from . import asn1 from ctypes import * __all__ = ["img4p_extract_compressed", "img4p_extract"] def decode_lzfse_liblzfse(cdata, raw_size): lzfse = CDLL("liblzfse.so") dest = create_string_buffer(raw_size) decoded = lzfse.lzfse_decode_buffer(dest, raw_size, cdata, len(cdata), None) assert decoded == raw_size return dest.raw def decode_lzfse_darwin(cdata, raw_size): compression = CDLL("libcompression.dylib") dest = create_string_buffer(raw_size) COMPRESSION_LZFSE = 0x801 decoded = compression.compression_decode_buffer(dest, raw_size, cdata, len(cdata), None, COMPRESSION_LZFSE) assert decoded == raw_size return dest.raw if sys.platform == 'darwin': decode_lzfse = decode_lzfse_darwin else: decode_lzfse = decode_lzfse_liblzfse def decode_header(decoder): tag = decoder.peek() assert tag.nr == asn1.Numbers.Sequence assert tag.typ == asn1.Types.Constructed decoder.enter() tag, value = decoder.read() assert tag == asn1.Tag(asn1.Numbers.IA5String, asn1.Types.Primitive, 0) assert value == "IM4P" tag, name = decoder.read() assert tag == asn1.Tag(asn1.Numbers.IA5String, asn1.Types.Primitive, 0) tag, unk = decoder.read() assert tag == asn1.Tag(asn1.Numbers.IA5String, asn1.Types.Primitive, 0) tag, data = decoder.read() assert tag == asn1.Tag(asn1.Numbers.OctetString, asn1.Types.Primitive, 0) return name, data def img4p_extract(data): decoder = asn1.Decoder() decoder.start(data) name, cdata = decode_header(decoder) tag = decoder.peek() if tag is None: return name, cdata assert tag.nr == asn1.Numbers.Sequence assert tag.typ == asn1.Types.Constructed decoder.enter() tag, ctype = decoder.read() assert tag == asn1.Tag(asn1.Numbers.Integer, asn1.Types.Primitive, 0) assert ctype == 1 tag, raw_size = decoder.read() assert tag == asn1.Tag(asn1.Numbers.Integer, asn1.Types.Primitive, 0) return name, decode_lzfse(cdata, raw_size) if __name__ == "__main__": import sys data = open(sys.argv[1], "rb").read() name, raw = img4p_extract(data) with open(sys.argv[2], "wb") as fd: fd.write(raw) asahi-installer-0.6.12/asahi_firmware/isp.py000066400000000000000000000124261451217151600210160ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import struct, os, logging from collections import namedtuple from .core import FWFile SetFile = namedtuple('SetFile', ['sensor', 'magic', 'name', 'size']) ISP_PREFIX = "apple/isp_" ISP_SETFILE_ALIGNMENT = 0x1000 ISP_SETFILES = [ SetFile(0x248, 0x18200103, "1820_01XX", 0x442c), SetFile(0x248, 0x18220201, "1822_02XX", 0x442c), # SetFile(0x343, 0x52210211, "5221_02XX", 0x4870), # SetFile(0x354, 0x92510208, "9251_02XX", 0xa5ec), # SetFile(0x356, 0x48200107, "4820_01XX", 0x9324), # SetFile(0x356, 0x48200206, "4820_02XX", 0x9324), SetFile(0x364, 0x87200103, "8720_01XX", 0x36ac), SetFile(0x364, 0x87230101, "8723_01XX", 0x361c), # SetFile(0x372, 0x38200108, "3820_01XX", 0xfdb0), # SetFile(0x372, 0x38200205, "3820_02XX", 0xfdb0), # SetFile(0x372, 0x38201104, "3820_11XX", 0xfdb0), # SetFile(0x372, 0x38201204, "3820_12XX", 0xfdb0), # SetFile(0x405, 0x97200102, "9720_01XX", 0x92c8), # SetFile(0x405, 0x97210102, "9721_01XX", 0x9818), # SetFile(0x405, 0x97230101, "9723_01XX", 0x92c8), # SetFile(0x414, 0x25200102, "2520_01XX", 0xa444), # SetFile(0x503, 0x78200109, "7820_01XX", 0xb268), # SetFile(0x503, 0x78200206, "7820_02XX", 0xb268), # SetFile(0x505, 0x39210102, "3921_01XX", 0x89b0), # SetFile(0x514, 0x28200108, "2820_01XX", 0xa198), # SetFile(0x514, 0x28200205, "2820_02XX", 0xa198), # SetFile(0x514, 0x28200305, "2820_03XX", 0xa198), # SetFile(0x514, 0x28200405, "2820_04XX", 0xa198), SetFile(0x558, 0x19210106, "1921_01XX", 0xad40), SetFile(0x558, 0x19220201, "1922_02XX", 0xad40), # SetFile(0x603, 0x79200109, "7920_01XX", 0xad2c), # SetFile(0x603, 0x79200205, "7920_02XX", 0xad2c), # SetFile(0x603, 0x79210104, "7921_01XX", 0xad90), # SetFile(0x613, 0x49200108, "4920_01XX", 0x9324), # SetFile(0x613, 0x49200204, "4920_02XX", 0x9324), # SetFile(0x614, 0x29210107, "2921_01XX", 0xed6c), # SetFile(0x614, 0x29210202, "2921_02XX", 0xed6c), # SetFile(0x614, 0x29220201, "2922_02XX", 0xed6c), # SetFile(0x633, 0x36220111, "3622_01XX", 0x100d4), # SetFile(0x703, 0x77210106, "7721_01XX", 0x936c), # SetFile(0x703, 0x77220106, "7722_01XX", 0xac20), # SetFile(0x713, 0x47210107, "4721_01XX", 0x936c), # SetFile(0x713, 0x47220109, "4722_01XX", 0x9218), # SetFile(0x714, 0x20220107, "2022_01XX", 0xa198), # SetFile(0x772, 0x37210106, "3721_01XX", 0xfdf8), # SetFile(0x772, 0x37211106, "3721_11XX", 0xfe14), # SetFile(0x772, 0x37220104, "3722_01XX", 0xfca4), # SetFile(0x772, 0x37230106, "3723_01XX", 0xfca4), # SetFile(0x814, 0x21230101, "2123_01XX", 0xed54), # SetFile(0x853, 0x76220112, "7622_01XX", 0x247f8), # SetFile(0x913, 0x75230107, "7523_01XX", 0x247f8), # SetFile(0xd56, 0x62210102, "6221_01XX", 0x1b80), # SetFile(0xd56, 0x62220102, "6222_01XX", 0x1b80), ] ISP_SETFILE_MAP = {s.magic: s for s in ISP_SETFILES} ISP_SETFILE_COUNT = len(ISP_SETFILES) assert len(ISP_SETFILE_MAP) == ISP_SETFILE_COUNT log = logging.getLogger("asahi_firmware.isp") def round_up(x, y): return ((x + (y - 1)) & (-y)) def isp_setfile_header_check(hdr): return ( (hdr[2] == 0x0) and (hdr[3] == 0x0) and (hdr[4] & 0xff000000 == hdr[4]) and (hdr[4]) and (hdr[5] & 0xffff0000 == hdr[5]) and (hdr[5]) and (hdr[6] == 0x0) and (hdr[7] == 0x3c00000) ) class ISPFWCollection(object): def __init__(self, source_path): self.fwfiles = [] self.load(source_path) def extract_isp(self, data): files = [] found = 0 for offset in range(0, len(data), ISP_SETFILE_ALIGNMENT): hdrdata = data[offset:offset + 8*4] if len(hdrdata) < 8*4: break # search for valid header and magic constant at 4K boundary header = struct.unpack(">8L", hdrdata) if not isp_setfile_header_check(header): continue setfile = ISP_SETFILE_MAP.get(header[0], None) if not setfile: continue size = round_up(setfile.size, 64) # align to be safe dat = data[offset:offset + size] sensor_name = f"{setfile.sensor:x}_{setfile.name}" log.info(f"isp-extract: {found + 1}/{ISP_SETFILE_COUNT}: Found sensor {sensor_name} data at offset {offset:#x}") yield FWFile(f"apple/isp_{setfile.name}.dat", dat) found += 1 if found != ISP_SETFILE_COUNT: log.warn(f"isp-extract: Found {found}/{ISP_SETFILE_COUNT} calibration files.") else: log.info(f"isp-extract: Found all {found}/{ISP_SETFILE_COUNT} sensor calibration files!") def load(self, source_path): if os.path.isdir(source_path): bin_path = os.path.join(source_path, "appleh13camerad") else: bin_path = source_path if not os.path.exists(bin_path): log.warn("appleh13camerad not found, cannot extract ISP camera calibration firmwares. Webcam output will be low quality.") return log.info(f"Extracting firmware from camera daemon at {bin_path}") with open(bin_path, "rb") as fd: data = fd.read() for fwf in self.extract_isp(data): self.fwfiles.append((fwf.name, fwf)) def files(self): return self.fwfiles asahi-installer-0.6.12/asahi_firmware/kernel.py000066400000000000000000000020401451217151600214720ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import struct, os, logging from .img4 import img4p_extract from .core import FWFile from .asmedia import extract_asmedia log = logging.getLogger("asahi_firmware.kernel") class KernelFWCollection(object): def __init__(self, source_path): self.fwfiles = [] self.load(source_path) def load(self, source_path): if os.path.isdir(source_path): for fname in os.listdir(source_path): if fname.startswith("kernelcache"): kern_path = os.path.join(source_path, fname) break else: raise Exception("Could not find kernelcache") else: kern_path = source_path log.info(f"Extracting firmware from kernel at {kern_path}") with open(kern_path, "rb") as fd: im4p = fd.read() name, kernel = img4p_extract(im4p) for fwf in extract_asmedia(kernel): self.fwfiles.append((fwf.name, fwf)) def files(self): return self.fwfiles asahi-installer-0.6.12/asahi_firmware/multitouch.py000066400000000000000000000174321451217151600224220ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import xml.etree.ElementTree as ET import plistlib, base64, struct, os, logging from .img4 import img4p_extract from .core import FWFile log = logging.getLogger("asahi_firmware.multitouch") def load_plist_xml(d): root = ET.fromstring(d.decode("ascii")) idmap = {} def unmunge(el, idmap): if "ID" in el.attrib: idmap[el.attrib["ID"]] = el if "IDREF" in el.attrib: return idmap[el.attrib["IDREF"]] else: el2 = ET.Element(el.tag) el2.text = el.text el2.tag = el.tag el2.attrib = el.attrib for child in el: el2.append(unmunge(child, idmap)) return el2 pl = ET.Element("plist") pl.append(unmunge(root, idmap)) return plistlib.loads(ET.tostring(pl)) LOAD_COMMAND_INIT_PAYLOAD = 0 LOAD_COMMAND_SEND_BLOB = 1 LOAD_COMMAND_SEND_CALIBRATION = 2 def plist_to_bin_touchbar(plist): def serialize(plist): yield struct.pack("<4sI", b"Z2FW", 1) for i in plist: if i["Type"] == "Config": init = i["Config"]["SPI Config"]["Init Payload"] yield struct.pack("H", o) else: yield bytes([0x1a]) yield struct.pack(">I", o) elif isinstance(o, bytes): if len(o) <= 0xffff: yield (4, 3) yield struct.pack(">BH", 0x59, len(o)) else: raise Exception("Unsupported serializer case") yield b"?" + struct.pack(">I", len(o)) yield o else: raise Exception("Unsupported serializer case") yield b"?" + str(type(o)).encode("ascii") def add_padding(l): nonlocal iface_offset off = 0 for b in l: if b is None: assert iface_offset is None iface_offset = off b = b"\x00" if isinstance(b, tuple): align, i = b if (off + i) % align != 0: pad = align - ((off + i) % align) off += pad yield b"\xd3" * pad else: off += len(b) yield b blob = b"".join(add_padding(serialize(plist))) assert iface_offset is not None hdr = struct.pack("<4sIII", b"HIDF", 1, 32, len(blob)) hdr += struct.pack(" 3: first = next(iter(node.leaves.values())) if all(i == first for i in node.leaves.values()): node.this = first.this for i in node.leaves.values(): if not i.this or not node.this: break if i.this != node.this: break else: node.leaves = {} def _walk_files(self, node, ident): if node.this is not None: yield ident, node.this for k, subnode in node.leaves.items(): yield from self._walk_files(subnode, ident + [k]) def files(self): for ident, fwfile in self._walk_files(self.root, []): (ext, chip, rev), rest = ident[:3], ident[3:] rev = rev.lower() ext = self.EXTMAP[ext] if rest: rest = "," + "-".join(rest) else: rest = "" filename = f"brcm/brcmfmac{chip}{rev}-pcie.apple{rest}.{ext}" yield filename, fwfile def process_nvram(self, data): data = data.decode("ascii") keys = {} lines = [] for line in data.split("\n"): if not line: continue key, value = line.split("=", 1) keys[key] = value # Clean up spurious whitespace that Linux does not like lines.append(f"{key.strip()}={value}\n") return "".join(lines).encode("ascii") def print(self): self.root.print() if __name__ == "__main__": col = WiFiFWCollection(sys.argv[1]) if len(sys.argv) > 2: from .core import FWPackage pkg = FWPackage(sys.argv[2]) pkg.add_files(sorted(col.files())) pkg.close() for i in pkg.manifest: print(i) else: for name, fwfile in col.files(): if isinstance(fwfile, str): print(name, "->", fwfile) else: print(name, f"({len(fwfile.data)} bytes)") asahi-installer-0.6.12/build.sh000077500000000000000000000044401451217151600163230ustar00rootroot00000000000000#!/bin/sh # SPDX-License-Identifier: MIT set -e cd "$(dirname "$0")" PYTHON_VER=3.9.6 PYTHON_PKG=python-$PYTHON_VER-macos11.pkg PYTHON_URI="https://www.python.org/ftp/python/$PYTHON_VER/$PYTHON_PKG" M1N1="$PWD/m1n1" ARTWORK="$PWD/artwork" AFW="$PWD/asahi_firmware" SRC="$PWD/src" VENDOR="$PWD/vendor" DL="$PWD/dl" PACKAGE="$PWD/package" RELEASES="$PWD/releases" RELEASES_DEV="$PWD/releases-dev" rm -rf "$PACKAGE" mkdir -p "$DL" "$PACKAGE" "$RELEASES" "$RELEASES_DEV" mkdir -p "$PACKAGE/bin" echo "Determining version..." VER=$(git describe --always --dirty --tags) echo "Version: $VER" if [ -z "$VER" ]; then if [ -e version.tag ]; then VER="$(cat version.tag)" else echo "Could not determine version!" exit 1 fi fi echo "Downloading installer components..." cd "$DL" wget -Nc "$PYTHON_URI" echo "Building m1n1..." make -C "$M1N1" RELEASE=1 CHAINLOADING=1 -j4 echo "Copying files..." cp -r "$SRC"/* "$PACKAGE/" rm "$PACKAGE/asahi_firmware" cp -r "$AFW" "$PACKAGE/" cp "$ARTWORK/logos/icns/AsahiLinux_logomark.icns" "$PACKAGE/logo.icns" mkdir -p "$PACKAGE/boot" cp "$M1N1/build/m1n1.bin" "$PACKAGE/boot" echo "Extracting Python framework..." mkdir -p "$PACKAGE/Frameworks/Python.framework" 7z x -so "$DL/$PYTHON_PKG" Python_Framework.pkg/Payload | zcat | \ cpio -i -D "$PACKAGE/Frameworks/Python.framework" cd "$PACKAGE/Frameworks/Python.framework/Versions/Current" echo "Copying vendored libffi into Python framework..." cp -P "$VENDOR"/libffi/* lib/ echo "Slimming down Python..." rm -rf include share cd lib rm -rf -- tdb* tk* Tk* libtk* *tcl* cd python3.* rm -rf test ensurepip idlelib cd lib-dynload rm -f _test* _tkinter* echo "Copying certificates..." certs="$(python3 -c 'import certifi; print(certifi.where())')" cp "$certs" "$PACKAGE/Frameworks/Python.framework/Versions/Current/etc/openssl/cert.pem" echo "Packaging installer..." cd "$PACKAGE" echo "$VER" > version.tag if [ "$1" == "prod" ]; then PKGFILE="$RELEASES/installer-$VER.tar.gz" LATEST="$RELEASES/latest" elif [ "$1" == "dev" ]; then PKGFILE="$RELEASES_DEV/installer-$VER.tar.gz" LATEST="$RELEASES_DEV/latest" else PKGFILE="../installer.tar.gz" LATEST="../latest" fi tar czf "$PKGFILE" . echo "$VER" > "$LATEST" echo echo "Built package: $(basename "$PKGFILE")" asahi-installer-0.6.12/data/000077500000000000000000000000001451217151600155745ustar00rootroot00000000000000asahi-installer-0.6.12/data/installer_data.json000066400000000000000000000054261451217151600214640ustar00rootroot00000000000000{ "os_list": [ { "name": "Asahi Linux Desktop", "default_os_name": "Asahi Linux", "boot_object": "m1n1.bin", "next_object": "m1n1/boot.bin", "package": "asahi-plasma-20231012-1.zip", "supported_fw": ["12.3", "12.3.1", "12.4", "13.5"], "partitions": [ { "name": "EFI", "type": "EFI", "size": "500MB", "format": "fat", "volume_id": "0x2abf9f91", "copy_firmware": true, "copy_installer_data": true, "source": "esp" }, { "name": "Root", "type": "Linux", "size": "12GB", "expand": true, "image": "root.img" } ] }, { "name": "Asahi Linux Minimal (Arch Linux ARM)", "default_os_name": "Asahi Linux", "boot_object": "m1n1.bin", "next_object": "m1n1/boot.bin", "package": "asahi-base-20231012-1.zip", "supported_fw": ["12.3", "12.3.1", "12.4", "13.5"], "partitions": [ { "name": "EFI", "type": "EFI", "size": "500MB", "format": "fat", "volume_id": "0x2abf9f91", "copy_firmware": true, "copy_installer_data": true, "source": "esp" }, { "name": "Root", "type": "Linux", "size": "5GB", "expand": true, "image": "root.img" } ] }, { "name": "UEFI environment only (m1n1 + U-Boot + ESP)", "default_os_name": "UEFI boot", "boot_object": "m1n1.bin", "next_object": "m1n1/boot.bin", "package": "uefi-only-20231012-1.zip", "supported_fw": ["12.3", "12.3.1", "12.4", "13.5"], "partitions": [ { "name": "EFI", "type": "EFI", "size": "500MB", "format": "fat", "copy_firmware": true, "copy_installer_data": true, "source": "esp" } ] }, { "name": "Tethered boot (m1n1, for development)", "default_os_name": "m1n1 proxy", "expert": true, "boot_object": "m1n1.bin", "external_boot": true, "partitions": [] } ] } asahi-installer-0.6.12/m1n1/000077500000000000000000000000001451217151600154375ustar00rootroot00000000000000asahi-installer-0.6.12/scripts/000077500000000000000000000000001451217151600163525ustar00rootroot00000000000000asahi-installer-0.6.12/scripts/bootstrap-dev.sh000077500000000000000000000024141451217151600215030ustar00rootroot00000000000000#!/bin/sh # SPDX-License-Identifier: MIT set -e export LC_ALL=en_US.UTF-8 export LANG=en_US.UTF-8 export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH" export VERSION_FLAG=https://cdn.asahilinux.org/installer-dev/latest export INSTALLER_BASE=https://cdn.asahilinux.org/installer-dev export INSTALLER_DATA=https://github.com/AsahiLinux/asahi-installer/raw/main/data/installer_data.json export REPO_BASE=https://cdn.asahilinux.org export REPORT=https://stats.asahilinux.org/report export REPORT_TAG=alx-dev export EXPERT=1 #TMP="$(mktemp -d)" TMP=/tmp/asahi-install echo echo "Bootstrapping installer:" if [ -e "$TMP" ]; then mv "$TMP" "$TMP-$(date +%Y%m%d-%H%M%S)" fi mkdir -p "$TMP" cd "$TMP" echo " Checking version..." PKG_VER="$(curl --no-progress-meter -L "$VERSION_FLAG")" echo " Version: $PKG_VER" PKG="installer-$PKG_VER.tar.gz" echo " Downloading..." curl --no-progress-meter -L -o "$PKG" "$INSTALLER_BASE/$PKG" curl --no-progress-meter -L -O "$INSTALLER_DATA" echo " Extracting..." tar xf "$PKG" echo " Initializing..." echo if [ "$USER" != "root" ]; then echo "The installer needs to run as root." echo "Please enter your sudo password if prompted." exec caffeinate -dis sudo -E ./install.sh "$@" else exec caffeinate -dis ./install.sh "$@" fi asahi-installer-0.6.12/scripts/bootstrap-prod.sh000077500000000000000000000030641451217151600216730ustar00rootroot00000000000000#!/bin/sh # SPDX-License-Identifier: MIT set -e export LC_ALL=en_US.UTF-8 export LANG=en_US.UTF-8 export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH" export VERSION_FLAG=https://cdn.asahilinux.org/installer/latest export INSTALLER_BASE=https://cdn.asahilinux.org/installer export INSTALLER_DATA=https://github.com/AsahiLinux/asahi-installer/raw/prod/data/installer_data.json export INSTALLER_DATA_ALT=https://alx.sh/installer_data.json export REPO_BASE=https://cdn.asahilinux.org export REPORT=https://stats.asahilinux.org/report export REPORT_TAG=alx-prod #TMP="$(mktemp -d)" TMP=/tmp/asahi-install echo echo "Bootstrapping installer:" if [ -e "$TMP" ]; then mv "$TMP" "$TMP-$(date +%Y%m%d-%H%M%S)" fi mkdir -p "$TMP" cd "$TMP" echo " Checking version..." PKG_VER="$(curl --no-progress-meter -L "$VERSION_FLAG")" echo " Version: $PKG_VER" PKG="installer-$PKG_VER.tar.gz" echo " Downloading..." curl --no-progress-meter -L -o "$PKG" "$INSTALLER_BASE/$PKG" if ! curl --no-progress-meter -L -O "$INSTALLER_DATA"; then echo " Error downloading installer_data.json. GitHub might be blocked in your network." echo " Please consider using a VPN if you experience issues." echo " Trying workaround..." curl --no-progress-meter -L -O "$INSTALLER_DATA_ALT" fi echo " Extracting..." tar xf "$PKG" echo " Initializing..." echo if [ "$USER" != "root" ]; then echo "The installer needs to run as root." echo "Please enter your sudo password if prompted." exec caffeinate -dis sudo -E ./install.sh "$@" else exec caffeinate -dis ./install.sh "$@" fi asahi-installer-0.6.12/scripts/bootstrap.sh000077500000000000000000000017021451217151600207260ustar00rootroot00000000000000#!/bin/sh # SPDX-License-Identifier: MIT set -e export LC_ALL=en_US.UTF-8 export LANG=en_US.UTF-8 export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH" export INSTALLER_BASE=http://localhost:5000 export INSTALLER_DATA=http://localhost:5000/data/installer_data.json export REPO_BASE=https://cdn.asahilinux.org PKG=installer.tar.gz export EXPERT=1 #TMP="$(mktemp -d)" TMP=/tmp/asahi-install echo echo "Bootstrapping installer:" if [ -e "$TMP" ]; then mv "$TMP" "$TMP-$(date +%Y%m%d-%H%M%S)" fi mkdir -p "$TMP" cd "$TMP" echo " Downloading..." curl --no-progress-meter -L -O "$INSTALLER_BASE/$PKG" curl --no-progress-meter -L -O "$INSTALLER_DATA" echo " Extracting..." tar xf "$PKG" echo " Initializing..." echo if [ "$USER" != "root" ]; then echo "The installer needs to run as root." echo "Please enter your sudo password if prompted." exec caffeinate -dis sudo -E ./install.sh "$@" else exec caffeinate -dis ./install.sh "$@" fi asahi-installer-0.6.12/setup.py000066400000000000000000000006371451217151600164030ustar00rootroot00000000000000#!/usr/bin/env python from distutils.core import setup setup(name='asahi_firmware', version='0.1', description='Asahi Linux firmware tools', author='Hector Martin', author_email='marcan@marcan.st', url='https://github.com/AsahiLinux/asahi-installer/', packages=['asahi_firmware'], entry_points={"console_scripts": ["asahi-fwextract = asahi_firmware.update:main"]} ) asahi-installer-0.6.12/src/000077500000000000000000000000001451217151600154525ustar00rootroot00000000000000asahi-installer-0.6.12/src/asahi_firmware000077700000000000000000000000001451217151600234702../asahi_firmwareustar00rootroot00000000000000asahi-installer-0.6.12/src/diskutil.py000066400000000000000000000240151451217151600176560ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import plistlib, subprocess, sys, logging from dataclasses import dataclass from util import * @dataclass class Partition: name: str offset: int size: int free: bool type: str uuid: str = None desc: str = None label: str = None info: object = None container: object = None os: object = None class DiskUtil: FREE_THRESHOLD = 16 * 1024 * 1024 def __init__(self): self.verbose = "-v" in sys.argv def action(self, *args, verbose=False): if verbose == 2: capture = False elif verbose: capture = not self.verbose else: capture = True logging.debug(f"run: diskutil {args!r}") if capture: p = subprocess.run(["diskutil"] + list(args), check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) logging.debug(f"process output: {p.stdout}") else: subprocess.run(["diskutil"] + list(args), check=True) def get(self, *args): logging.debug(f"get: diskutil {args!r}") result = subprocess.run(["diskutil"] + list(args), stdout=subprocess.PIPE, check=True) return plistlib.loads(result.stdout) def get_list(self): logging.info(f"DiskUtil.get_list()") self.list = self.get("list", "-plist") self.disk_list = self.list["WholeDisks"] logging.debug(" Whole disks:") for i in self.disk_list: logging.debug(f" - {i!r}") self.disk_parts = {dsk["DeviceIdentifier"]: dsk for dsk in self.list["AllDisksAndPartitions"]} logging.debug(" All disks and partitions:") for k, v in self.disk_parts.items(): logging.debug(f" - {k}: {v!r}") def get_apfs_list(self, dev=None): logging.info(f"DiskUtil.get_apfs_list({dev=!r})") if dev: apfs = self.get("apfs", "list", dev, "-plist") else: apfs = self.get("apfs", "list", "-plist") for ctnr in apfs["Containers"]: vgs = self.get("apfs", "listVolumeGroups", ctnr["ContainerReference"], "-plist") logging.debug(f"container: {ctnr!r}") logging.debug(f" VGs: {vgs!r}") ctnr["VolumeGroups"] = vgs["Containers"][0]["VolumeGroups"] self.ctnr_by_ref[ctnr["ContainerReference"]] = ctnr self.ctnr_by_store[ctnr["DesignatedPhysicalStore"]] = ctnr def get_disk_info(self): logging.info(f"DiskUtil.get_disk_info()") self.disks = {} for i in self.disk_list: self.disks[i] = self.get("info", "-plist", i) logging.debug(f" {i}: {self.disks[i]}") def get_info(self): logging.info(f"DiskUtil.get_info()") self.get_list() self.ctnr_by_ref = {} self.ctnr_by_store = {} self.get_apfs_list() self.get_disk_info() def find_system_disk(self): logging.info(f"DiskUtil.find_system_disk()") for name, dsk in self.disks.items(): try: if dsk["VirtualOrPhysical"] == "Virtual": continue if not dsk["Internal"]: continue parts = self.disk_parts[name]["Partitions"] if parts[0]["Content"] == "Apple_APFS_ISC": logging.info(f"System disk: {name}") return name except (KeyError, IndexError): continue raise Exception("Could not find system disk") def find_external_disks(self): logging.info(f"DiskUtil.find_external_disks()") disks = [] for name, dsk in self.disks.items(): try: if dsk["VirtualOrPhysical"] == "Virtual": continue if dsk["Internal"]: continue if dsk["BusProtocol"] != "USB": continue if not dsk["Writable"]: continue if not dsk["WholeDisk"]: continue if "usb-drd" not in dsk["DeviceTreePath"]: continue disks.append(dsk) except (KeyError, IndexError): continue return disks def get_partition_info(self, dev, refresh_apfs=False): logging.info(f"DiskUtil.get_partition_info({dev=!r}, {refresh_apfs=!r})") partinfo = self.get("info", "-plist", dev) off = partinfo["PartitionMapPartitionOffset"] part = Partition(name=partinfo["DeviceIdentifier"], free=False, type=partinfo["Content"], offset=off, size=partinfo["Size"], uuid=partinfo["DiskUUID"], info=partinfo) if refresh_apfs: self.get_apfs_list(partinfo["APFSContainerReference"]) if part.name in self.ctnr_by_store: part.container = self.ctnr_by_store[part.name] for t in (["System"], ["Data"], []): for vol in part.container["Volumes"]: if vol["Roles"] == t: part.label = vol["Name"] break else: continue break logging.debug(f"Partition {dev}: {part}") return part def get_disk_size(self, dskname): dsk = self.disk_parts[dskname] return dsk["Size"] def get_disk_usable_range(self, dskname): # GPT overhead aligned to 4K dsk = self.disk_parts[dskname] start = 40 * 512 end = align_down(dsk["Size"] - 34 * 512, 4096) return start, end def get_partitions(self, dskname): logging.info(f"DiskUtil.get_partitions({dskname!r})") dsk = self.disk_parts[dskname] parts = [] p, total_size = self.get_disk_usable_range(dskname) for dskpart in dsk.get("Partitions", []): parts.append(self.get_partition_info(dskpart["DeviceIdentifier"])) parts.sort(key=lambda i: i.offset) prev_name = dskname parts2 = [] for part in parts: if (part.offset - p) > self.FREE_THRESHOLD: parts2.append(Partition(name=prev_name, free=True, type=None, offset=p, size=(part.offset - p))) parts2.append(part) prev_name = part.name p = part.offset + part.size if (total_size - p) > self.FREE_THRESHOLD: parts2.append(Partition(name=prev_name, free=True, type=None, offset=p, size=(total_size - p))) return parts2 def refresh_part(self, part): logging.info(f"DiskUtil.refresh_part({part.name=!r})") self.get_apfs_list(part.container["ContainerReference"]) part.container = self.ctnr_by_store[part.name] def mount(self, target): self.action("mount", target) info = self.get("info", "-plist", target) return info["MountPoint"] def remount_rw(self, target): logging.info(f"DiskUtil.remount_rw({target})") subprocess.run(["mount", "-u", "-w", target], check=True) def addVolume(self, container, name, **kwargs): args = [] for k, v in kwargs.items(): args.extend(["-" + k, v]) try: self.action("apfs", "addVolume", container, "apfs", name, *args, verbose=True) except subprocess.CalledProcessError as e: if e.output is not None and b"Mounting APFS Volume" in e.output: logging.warning(f"diskutil addVolume errored out spuriously, squelching: {e.output}") else: raise def partitionDisk(self, disk, fs, label, size): logging.info(f"DiskUtil.wipe_disk({disk}, {fs}, {label}, {size}") size = str(size) assert fs.lower() == "apfs" # diskutil likes to "helpfully" create an EFI partition for us... self.action("partitionDisk", disk, "1", "GPT", "free", "free", "0", verbose=True) self.get_list() parts = self.get_partitions(disk) assert len(parts) == 2 # EFI and free part = parts[0] # So re-format it as APFS... self.action("eraseVolume", fs, label, part.name) # And then grow it to the right size self.action("apfs", "resizeContainer", part.name, size) # Yes, this is silly. part = self.get_partition_info(part.name, refresh_apfs=(fs == "apfs")) logging.info(f"New partition: {part!r}") return part def addPartition(self, after, fs, label, size): logging.info(f"DiskUtil.addPartition({after}, {fs}, {label}, {size})") size = str(size) # diskutil can't create partitions on an empty disk... if (after in self.disk_parts and not self.disk_parts[after]["Partitions"] and fs.lower() == "apfs"): return self.partitionDisk(after, fs, label, size) self.action("addPartition", after, fs, label, size, verbose=True) disk = after.rsplit("s", 1)[0] self.get_list() parts = self.get_partitions(disk) for i, part in enumerate(parts): logging.info(f"Checking #{i} {part.name}...") if part.name == after: logging.info(f"Found previous partition {part.name}...") new_part = self.get_partition_info(parts[i + 1].name, refresh_apfs=(fs == "apfs")) logging.info(f"New partition: {new_part!r}") return new_part raise Exception("Could not find new partition") def changeVolumeRole(self, volume, role): self.action("apfs", "changeVolumeRole", volume, role, verbose=True) def rename(self, volume, name): self.action("rename", volume, name, verbose=True) def get_resize_limits(self, name): return self.get("apfs", "resizeContainer", name, "limits", "-plist") def resizeContainer(self, name, size): size = str(size) self.action("apfs", "resizeContainer", name, size, verbose=2) asahi-installer-0.6.12/src/install.sh000077500000000000000000000030631451217151600174610ustar00rootroot00000000000000#!/bin/sh # SPDX-License-Identifier: MIT set -e if [ "${0%/*}" != "$0" ]; then cd "${0%/*}" fi export LC_ALL=en_US.UTF-8 export LANG=en_US.UTF-8 export DYLD_LIBRARY_PATH=$PWD/Frameworks/Python.framework/Versions/Current/lib export DYLD_FRAMEWORK_PATH=$PWD/Frameworks python=Frameworks/Python.framework/Versions/3.9/bin/python3.9 export SSL_CERT_FILE=$PWD/Frameworks/Python.framework/Versions/Current/etc/openssl/cert.pem # Bootstrap does part of this, but install.sh can be run standalone # so do it again for good measure. export PATH="$PWD/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH" set +e macos_ver=$(/usr/libexec/PlistBuddy -c "Print :ProductVersion" \ /System/Library/CoreServices/SystemVersion.plist) res=$? set -e if [ "$res" -ne 0 ] || [ -z "$macos_ver" ]; then echo "Unable to determine macOS version. Please report a bug." exit 1 fi if [ "${macos_ver%%.*}" -lt 12 ]; then echo "This installer requires macOS 12.3 or later." exit 1 fi if ! arch -arm64 ls >/dev/null 2>/dev/null; then echo echo "Looks like this is an Intel Mac!" echo "Sorry, Asahi Linux only supports Apple Silicon machines." echo "May we interest you in https://t2linux.org/ instead?" exit 1 fi if [ $(arch) != "arm64" ]; then echo echo "You're running the installer in Intel mode under Rosetta!" echo "Don't worry, we can fix that for you. Switching to ARM64 mode..." # This loses env vars in some security states, so just re-launch ourselves exec arch -arm64 ./install.sh fi exec /dev/tty 2>/dev/tty exec $python main.py "$@" asahi-installer-0.6.12/src/m1n1.py000066400000000000000000000014061451217151600166010ustar00rootroot00000000000000# SPDX-License-Identifier: MIT def build(src, dest, vars): if isinstance(vars, (list, tuple)): vars = b"".join(i.encode("ascii") + b"\n" for i in vars) + b"\0\0\0\0" with open(src, "rb") as fd: m1n1_data = fd.read() with open(dest, "wb") as fd: fd.write(m1n1_data + vars) def extract_vars(src): with open(src, "rb") as fd: m1n1_data = fd.read() try: vars = m1n1_data.split(b"STACKBOT")[1].split(b"\0")[0].decode("ascii") except Exception: return None return [i for i in vars.split("\n") if i] def get_version(path): data = open(path, "rb").read() if b"##m1n1_ver##" in data: return data.split(b"##m1n1_ver##")[1].split(b"\0")[0].decode("ascii") else: return None asahi-installer-0.6.12/src/main.py000066400000000000000000001235671451217151600167660ustar00rootroot00000000000000#!/usr/bin/python3 # SPDX-License-Identifier: MIT import os, os.path, shlex, subprocess, sys, time, termios, json, getpass, reporting from dataclasses import dataclass import system, osenum, stub, diskutil, osinstall, asahi_firmware, m1n1 from util import * PART_ALIGN = psize("1MiB") STUB_SIZE = align_down(psize("2.5GB"), PART_ALIGN) # Minimum free space to leave when resizing, to allow for OS upgrades MIN_FREE_OS = psize("38GB") # Minimum free space to leave for non-OS containers MIN_FREE = psize("1GB") # 2.5GB stub + 5GB OS + 0.5GB EFI = 8GB, round up to 10GB MIN_INSTALL_FREE = psize("10GB") MIN_MACOS_VERSION = "12.3" MIN_MACOS_VERSION_EXPERT = "12.3" @dataclass class IPSW: version: str min_macos: str min_iboot: str min_sfr: str expert_only: bool url: str @dataclass class Device: min_ver: str expert_only: bool CHIP_MIN_VER = { 0x8103: "11.0", # T8103, M1 0x6000: "12.0", # T6000, M1 Pro 0x6001: "12.0", # T6001, M1 Max 0x6002: "12.3", # T6002, M1 Ultra 0x8112: "12.4", # T8112, M2 0x6020: "13.1", # T6020, M2 Pro 0x6021: "13.1", # T6021, M2 Max 0x6022: "13.4", # T6022, M2 Ultra } DEVICES = { "j274ap": Device("11.0", False), # Mac mini (M1, 2020) "j293ap": Device("11.0", False), # MacBook Pro (13-inch, M1, 2020) "j313ap": Device("11.0", False), # MacBook Air (M1, 2020) "j456ap": Device("11.3", False), # iMac (24-inch, M1, 2021) "j457ap": Device("11.3", False), # iMac (24-inch, M1, 2021) "j314cap": Device("12.0", False), # MacBook Pro (14-inch, M1 Max, 2021) "j314sap": Device("12.0", False), # MacBook Pro (14-inch, M1 Pro, 2021) "j316cap": Device("12.0", False), # MacBook Pro (16-inch, M1 Max, 2021) "j316sap": Device("12.0", False), # MacBook Pro (16-inch, M1 Pro, 2021) "j375cap": Device("12.3", False), # Mac Studio (M1 Max, 2022) "j375dap": Device("12.3", False), # Mac Studio (M1 Ultra, 2022) "j413ap": Device("12.4", False), # MacBook Air (M2, 2022) "j493ap": Device("12.4", False), # MacBook Pro (13-inch, M2, 2022) "j414cap": Device("13.2", False), # MacBook Pro (14-inch, M2 Max, 2023) "j414sap": Device("13.2", False), # MacBook Pro (14-inch, M2 Pro, 2023) "j416cap": Device("13.2", False), # MacBook Pro (16-inch, M2 Max, 2023) "j416sap": Device("13.2", False), # MacBook Pro (16-inch, M2 Pro, 2023) "j473ap": Device("13.2", True), # Mac mini (M2, 2023) "j474sap": Device("13.2", True), # Mac mini (M2 Pro, 2023) "j415ap": Device("13.4", False), # MacBook Air (15-inch, M2, 2023) "j475cap": Device("13.4", True), # Mac Studio (M2 Max, 2023) "j475dap": Device("13.4", True), # Mac Studio (M2 Ultra, 2023) "j180dap": Device("13.4", True), # Mac Pro (M2 Ultra, 2023) } IPSW_VERSIONS = [ # This is the special M2 version, it comes ahead so it isn't the default in expert mode IPSW("12.4", "12.1", "iBoot-7459.121.3", "21.6.81.2.0,0", False, "https://updates.cdn-apple.com/2022SpringFCS/fullrestores/012-17781/F045A95A-44B4-4BA9-8A8A-919ECCA2BB31/UniversalMac_12.4_21F2081_Restore.ipsw"), IPSW("12.3.1", "12.1", "iBoot-7459.101.3", "21.5.258.0.0,0", False, "https://updates.cdn-apple.com/2022SpringFCS/fullrestores/002-79219/851BEDF0-19DB-4040-B765-0F4089D1530D/UniversalMac_12.3.1_21E258_Restore.ipsw"), IPSW("12.3", "12.1", "iBoot-7459.101.2", "21.5.230.0.0,0", False, "https://updates.cdn-apple.com/2022SpringFCS/fullrestores/071-08757/74A4F2A1-C747-43F9-A22A-C0AD5FB4ECB6/UniversalMac_12.3_21E230_Restore.ipsw"), IPSW("13.5", "13.0", "iBoot-8422.141.2", "22.7.74.0.0,0", False, "https://updates.cdn-apple.com/2023SummerFCS/fullrestores/032-69606/D3E05CDF-E105-434C-A4A1-4E3DC7668DD0/UniversalMac_13.5_22G74_Restore.ipsw"), ] class InstallerMain: def __init__(self, version): self.version = version self.data = json.load(open("installer_data.json")) self.credentials_validated = False self.expert = False self.ipsw = None self.osins = None self.osi = None self.m1n1 = "boot/m1n1.bin" self.m1n1_ver = m1n1.get_version(self.m1n1) self.sys_disk = None self.cur_disk = None def input(self): self.flush_input() return input() def get_size(self, prompt, default=None, min=None, max=None, total=None): self.flush_input() if default is not None: prompt += f" ({default})" new_size = input_prompt(prompt + f": ").strip() try: if default is not None and not new_size: new_size = default if new_size.lower() == "min" and min is not None: val = min elif new_size.lower() == "max" and max is not None: val = max elif new_size.endswith("%") and total is not None: val = int(float(new_size[:-1]) * total / 100) elif new_size.endswith("B"): val = psize(new_size.upper()) else: val = psize(new_size.upper() + "B") except Exception as e: print(e) val = None if val is None: p_error(f"Invalid size '{new_size}'.") return val def choice(self, prompt, options, default=None): is_array = False if isinstance(options, list): is_array = True options = {str(i+1): v for i, v in enumerate(options)} if default is not None: default += 1 int_keys = all(isinstance(i, int) for i in options.keys()) for k, v in options.items(): p_choice(f" {col(BRIGHT)}{k}{col(NORMAL)}: {v}") if default: prompt += f" ({default})" while True: self.flush_input() res = input_prompt(prompt + ": ").strip() if res == "" and default is not None: res = str(default) if res not in options: p_warning(f"Enter one of the following: {', '.join(map(str, options.keys()))}") continue print() if is_array: return int(res) - 1 else: return res def yesno(self, prompt, default=False): if default: prompt += " (Y/n): " else: prompt += " (y/N): " while True: self.flush_input() res = input_prompt(prompt).strip() if not res: return default elif res.lower() in ("y", "yes", "1", "true"): return True elif res.lower() in ("n", "no", "0", "false"): return False p_warning(f"Please enter 'Y' or 'N'") def check_cur_os(self): if self.cur_os is None: p_error("Unable to determine primary OS.") p_error("This installer requires you to already have a macOS install with") p_error("at least one administrator user that is a machine owner.") p_error("Please run this installer from your main macOS instance or its") p_error("paired recovery, or ensure the default boot volume is set correctly.") sys.exit(1) p_progress(f"Using OS '{self.cur_os.label}' ({self.cur_os.sys_volume}) for machine authentication.") logging.info(f"Current OS: {self.cur_os.label} / {self.cur_os.sys_volume}") if not self.cur_os.admin_users: p_error("No admin users found in the primary OS. Cannot continue.") p_message("If this is a new or freshly reset machine, you will have to go through macOS") p_message("initial user set-up and create an admin user before using this installer.") sys.exit(1) def get_admin_credentials(self): if self.credentials_validated: return print() p_message("To continue the installation, you will need to enter your macOS") p_message("admin credentials.") print() if self.sysinfo.boot_mode == "macOS": self.admin_user = self.sysinfo.login_user else: if len(self.cur_os.admin_users) > 1: p_question("Choose an admin user for authentication:") idx = self.choice("User", self.cur_os.admin_users) else: idx = 0 self.admin_user = self.cur_os.admin_users[idx] self.admin_password = getpass.getpass(f'Password for {self.admin_user}: ') def action_install_into_container(self, avail_parts): template = self.choose_os() containers = {str(i): p.desc for i,p in enumerate(self.parts) if p in avail_parts} print() p_question("Choose a container to install into:") idx = self.choice("Target container", containers) self.part = self.parts[int(idx)] ipsw = self.choose_ipsw(template.get("supported_fw", None)) logging.info(f"Chosen IPSW version: {ipsw.version}") self.ins = stub.StubInstaller(self.sysinfo, self.dutil, self.osinfo) self.ins.load_ipsw(ipsw) self.osins = osinstall.OSInstaller(self.dutil, self.data, template) self.osins.load_package() self.do_install() def action_wipe(self): p_warning("This will wipe all data on the currently selected disk.") p_warning("Are you sure you want to continue?") if not self.yesno("Wipe my disk"): return True print() template = self.choose_os() self.osins = osinstall.OSInstaller(self.dutil, self.data, template) self.osins.load_package() min_size = STUB_SIZE + self.osins.min_size print() p_message(f"Minimum required space for this OS: {ssize(min_size)}") start, end = self.dutil.get_disk_usable_range(self.cur_disk) os_size = self.get_os_size_and_info(end - start, min_size, template) p_progress(f"Partitioning the whole disk ({self.cur_disk})") self.part = self.dutil.partitionDisk(self.cur_disk, "apfs", self.osins.name, STUB_SIZE) p_progress(f"Creating new stub macOS named {self.osins.name}") logging.info(f"Creating stub macOS: {self.osins.name}") self.do_install(os_size) def action_install_into_free(self, avail_free): template = self.choose_os() self.osins = osinstall.OSInstaller(self.dutil, self.data, template) self.osins.load_package() min_size = STUB_SIZE + self.osins.min_size print() p_message(f"Minimum required space for this OS: {ssize(min_size)}") frees = {str(i): p.desc for i,p in enumerate(self.parts) if p in avail_free and align_down(p.size, PART_ALIGN) >= min_size} if len(frees) < 1: p_error( "There is not enough free space to install this OS.") print() p_message("Press enter to go back to the main menu.") self.input() return True if len(frees) > 1: print() p_question("Choose a free area to install into:") idx = self.choice("Target area", frees) else: idx = list(frees.keys())[0] free_part = self.parts[int(idx)] print() p_message(f"Available free space: {ssize(free_part.size)}") os_size = self.get_os_size_and_info(free_part.size, min_size, template) p_progress(f"Creating new stub macOS named {self.osins.name}") logging.info(f"Creating stub macOS: {self.osins.name}") self.part = self.dutil.addPartition(free_part.name, "apfs", self.osins.name, STUB_SIZE) self.do_install(os_size) def get_os_size_and_info(self, free_size, min_size, template): os_size = None if self.osins.expandable: print() p_question("How much space should be allocated to the new OS?") p_message(" You can enter a size such as '1GB', a fraction such as '50%',") p_message(" the word 'min' for the smallest allowable size, or") p_message(" the word 'max' to use all available space.") min_perc = 100 * min_size / free_size while True: os_size = self.get_size("New OS size", default="max", min=min_size, max=free_size, total=free_size) if os_size is None: continue os_size = align_down(os_size, PART_ALIGN) if os_size < min_size: p_error(f"Size is too small, please enter a value > {ssize(min_size)} ({min_perc:.2f}%)") continue if os_size > free_size: p_error(f"Size is too large, please enter a value < {ssize(free_size)}") continue break print() p_message(f"The new OS will be allocated {ssize(os_size)} of space,") p_message(f"leaving {ssize(free_size - os_size)} of free space.") os_size -= STUB_SIZE print() self.flush_input() p_question("Enter a name for your OS") label = input_prompt(f"OS name ({self.osins.name}): ") or self.osins.name self.osins.name = label logging.info(f"New OS name: {label}") print() ipsw = self.choose_ipsw(template.get("supported_fw", None)) logging.info(f"Chosen IPSW version: {ipsw.version}") self.ins = stub.StubInstaller(self.sysinfo, self.dutil, self.osinfo) self.ins.load_ipsw(ipsw) return os_size def action_repair_or_upgrade(self, oses, upgrade): choices = {str(i): f"{p.desc}\n {str(o)}" for i, (p, o) in enumerate(oses)} if len(choices) > 1: print() if upgrade: p_question("Choose an existing install to upgrade:") else: p_question("Choose an incomplete install to repair:") idx = self.choice("Installed OS", choices) else: idx = list(choices.keys())[0] self.part, osi = oses[int(idx)] if upgrade: p_progress(f"Upgrading installation {self.part.name} ({self.part.label})") p_info(f" Old m1n1 stage 1 version: {osi.m1n1_ver}") p_info(f" New m1n1 stage 1 version: {self.m1n1_ver}") print() else: p_progress(f"Resuming installation into {self.part.name} ({self.part.label})") self.ins = stub.StubInstaller(self.sysinfo, self.dutil, self.osinfo) if not self.ins.check_existing_install(osi): op = "upgrade" if upgrade else "repair" p_error( "The existing installation is missing files.") p_message(f"This tool can only {op} installations that completed the first") p_message( "stage of the installation process. If it was interrupted, please") p_message( "delete the partitions manually and reinstall from scratch.") return False self.dutil.remount_rw(self.ins.osi.system) if upgrade: # Note: we get the vars out of the boot.bin in the system volume instead of the # actual installed fuOS. This is arguably the better option, since it allows # users to fix their install using this functionality if they messed up the boot # object. vars = m1n1.extract_vars(self.ins.boot_obj_path) if vars is None: p_error("Could not get variables from the installed m1n1") p_message(f"Path: {self.ins.boot_obj_path}") return False p_progress(f"Transferring m1n1 variables:") for v in vars: p_info(f" {v}") print() m1n1.build(self.m1n1, self.ins.boot_obj_path, vars) # Unhide the SystemVersion, if hidden self.ins.prepare_for_bless() # Go for step2 again self.step2() def do_install(self, total_size=None): p_progress(f"Installing stub macOS into {self.part.name} ({self.part.label})") self.ins.prepare_volume(self.part) self.ins.check_volume() self.ins.install_files(self.cur_os) self.osins.partition_disk(self.part.name, total_size) pkg = None if self.osins.needs_firmware: os.makedirs("vendorfw", exist_ok=True) pkg = asahi_firmware.core.FWPackage("vendorfw") self.ins.collect_firmware(pkg) pkg.close() self.osins.firmware_package = pkg self.osins.install(self.ins) for i in self.osins.idata_targets: self.ins.collect_installer_data(i) shutil.copy("installer.log", os.path.join(i, "installer.log")) self.step2(report=True) def choose_ipsw(self, supported_fw=None): sys_iboot = split_ver(self.sysinfo.sys_firmware) sys_macos = split_ver(self.sysinfo.macos_ver) sys_sfr = split_ver(self.sysinfo.sfr_full_ver) chip_min = split_ver(CHIP_MIN_VER.get(self.sysinfo.chip_id, "0")) device_min = split_ver(self.device.min_ver) minver = [ipsw for ipsw in IPSW_VERSIONS if split_ver(ipsw.version) >= max(chip_min, device_min) and (supported_fw is None or ipsw.version in supported_fw)] avail = [ipsw for ipsw in minver if split_ver(ipsw.min_iboot) <= sys_iboot and split_ver(ipsw.min_macos) <= sys_macos and split_ver(ipsw.min_sfr) <= sys_sfr and (not ipsw.expert_only or self.expert)] minver.sort(key=lambda ipsw: split_ver(ipsw.version)) if not avail: p_error("Your system firmware is too old.") p_error(f"Please upgrade to macOS {minver[0].version} or newer.") sys.exit(1) if self.expert: p_question("Choose the macOS version to use for boot firmware:") p_plain("(If unsure, just press enter)") p_warning("Picking a non-default option here is UNSUPPORTED and will BREAK YOUR SYSTEM.") p_warning("DO NOT FILE BUGS. YOU HAVE BEEN WARNED.") idx = self.choice("Version", [i.version for i in avail], len(avail)-1) else: idx = len(avail)-1 self.ipsw = ipsw = avail[idx] p_message(f"Using macOS {ipsw.version} for OS firmware") print() return ipsw def choose_os(self): os_list = self.data["os_list"] if not self.expert: os_list = [i for i in os_list if not i.get("expert", False)] if self.cur_disk != self.sys_disk: os_list = [i for i in os_list if i.get("external_boot", False)] p_question("Choose an OS to install:") idx = self.choice("OS", [i["name"] for i in os_list]) os = os_list[idx] logging.info(f"Chosen OS: {os['name']}") return os def set_reduced_security(self): while True: self.get_admin_credentials() print() p_progress("Preparing the new OS for booting in Reduced Security mode...") try: subprocess.run(["bputil", "-g", "-v", self.ins.osi.vgid, "-u", self.admin_user, "-p", self.admin_password], check=True) break except subprocess.CalledProcessError: p_error("Failed to run bputil. Press enter to try again.") self.input() self.credentials_validated = True print() def bless(self): while True: self.get_admin_credentials() print() p_progress("Setting the new OS as the default boot volume...") try: subprocess.run(["bless", "--setBoot", "--device", "/dev/" + self.ins.osi.sys_volume, "--user", self.admin_user, "--stdinpass"], input=self.admin_password.encode("utf-8"), check=True) break except subprocess.CalledProcessError: if self.admin_password.strip() != self.admin_password: p_warning("Failed to run bless.") p_warning("This is probably because your password starts or ends with a space,") p_warning("and that doesn't work due to a silly Apple bug.") p_warning("Let's try a different way. Sorry, you'll have to type it in again.") try: subprocess.run(["bless", "--setBoot", "--device", "/dev/" + self.ins.osi.sys_volume, "--user", self.admin_user], check=True) print() return except subprocess.CalledProcessError: pass p_error("Failed to run bless. Press enter to try again.") self.input() self.credentials_validated = True print() def step2(self, report=False): is_1tr = self.sysinfo.boot_mode == "one true recoveryOS" is_recovery = "recoveryOS" in self.sysinfo.boot_mode sys_ver = split_ver(self.sysinfo.macos_ver) if is_1tr and self.ins.osi.paired: subprocess.run([self.ins.step2_sh], check=True) self.bless() self.step2_completed(report) elif is_recovery: self.set_reduced_security() self.bless() self.step2_indirect(report) else: self.bless() self.step2_indirect(report) def flush_input(self): try: termios.tcflush(sys.stdin, termios.TCIFLUSH) except: pass def install_info(self, report): # Hide the new volume until step2 is done self.ins.prepare_for_step2() p_success( "Installation successful!") print() p_progress("Install information:") p_info( f" APFS VGID: {col()}{self.ins.osi.vgid}") if self.osins and self.osins.efi_part: p_info(f" EFI PARTUUID: {col()}{self.osins.efi_part.uuid.lower()}") print() if report: reporting.report(self) def step2_completed(self, report=False): self.install_info(report) print() time.sleep(2) p_prompt( "Press enter to reboot the system.") self.input() time.sleep(1) os.system("shutdown -r now") def step2_indirect(self, report=False): # Hide the new volume until step2 is done self.ins.prepare_for_step2() self.install_info(report) p_message( "To be able to boot your new OS, you will need to complete one more step.") p_warning( "Please read the following instructions carefully. Failure to do so") p_warning( "will leave your new installation in an unbootable state.") print() p_question( "Press enter to continue.") self.input() print() print() print() p_message( "When the system shuts down, follow these steps:") print() p_message( "1. Wait 15 seconds for the system to fully shut down.") p_message(f"2. Press and {col(BRIGHT, YELLOW)}hold{col()}{col(BRIGHT)} down the power button to power on the system.") p_warning( " * It is important that the system be fully powered off before this step,") p_warning( " and that you press and hold down the button once, not multiple times.") p_warning( " This is required to put the machine into the right mode.") p_message( "3. Release it once you see 'Loading startup options...' or a spinner.") p_message( "4. Wait for the volume list to appear.") p_message(f"5. Choose '{self.part.label}'.") p_message( "6. You will briefly see a 'macOS Recovery' dialog.") p_plain( " * If you are asked to 'Select a volume to recover',") p_plain( " then choose your normal macOS volume and click Next.") p_plain( " You may need to authenticate yourself with your macOS credentials.") p_message( "7. Once the 'Asahi Linux installer' screen appears, follow the prompts.") print() p_warning( "If you end up in a bootloop or get a message telling you that macOS needs to") p_warning( "be reinstalled, that means you didn't follow the steps above properly.") p_message( "Fully shut down your system without doing anything, and try again.") p_message( "If in trouble, hold down the power button to boot, select macOS, run") p_message( "this installer again, and choose the 'p' option to retry the process.") print() time.sleep(2) p_prompt( "Press enter to shut down the system.") self.input() time.sleep(1) os.system("shutdown -h now") def get_min_free_space(self, p): if p.os and any(os.version for os in p.os) and not self.expert: logging.info(" Has OS") return MIN_FREE_OS else: return MIN_FREE def can_resize(self, p): logging.info(f"Checking resizability of {p.name}") if p.type != "Apple_APFS": logging.info(f" Not APFS or system container") return False if p.container is None: logging.info(f" No container?") return False min_space = self.get_min_free_space(p) + psize("500MB") logging.info(f" Min space required: {min_space}") free = p.container["CapacityFree"] logging.info(f" Free space: {free}") if free <= min_space: logging.info(f" Cannot resize") return False else: logging.info(f" Can resize") return True def action_resize(self, resizable): choices = {str(i): p.desc for i,p in enumerate(self.parts) if p in resizable} print() if len(resizable) > 1 or self.expert: p_question("Choose an existing partition to resize:") idx = self.choice("Partition", choices) target = self.parts[int(idx)] else: target = resizable[0] limits = self.dutil.get_resize_limits(target.name) total = target.container["CapacityCeiling"] free = target.container["CapacityFree"] min_free = self.get_min_free_space(target) # Minimum size, ignoring APFS snapshots & co, but with a conservative buffer min_size_raw = align_up(total - free + min_free, PART_ALIGN) # Minimum size reported by diskutil, considering APFS snapshots & co but with a less conservative buffer min_size_safe = limits["MinimumSizePreferred"] min_size = max(min_size_raw, min_size_safe) overhead = min_size - min_size_raw avail = total - min_size min_perc = 100 * min_size / total assert free > min_free p_message( "We're going to resize this partition:") p_message(f" {target.desc}") p_info( f" Total size: {col()}{ssize(total)}") p_info( f" Free space: {col()}{ssize(free)}") p_info( f" Available space: {col()}{ssize(avail)}") p_info( f" Overhead: {col()}{ssize(overhead)}") p_info( f" Minimum new size: {col()}{ssize(min_size)} ({min_perc:.2f}%)") print() if overhead > 16_000_000_000: p_warning(" Warning: The selected partition has a large amount of overhead space.") p_warning(" This prevents you from resizing the partition to a smaller size, even") p_warning(" though macOS reports that space as free.") print() p_message(" This is usually caused by APFS snapshots used by Time Machine, which") p_message(" use up free disk space and block resizing the partition to a smaller") p_message(" size. It can also be caused by having a pending macOS upgrade.") print() p_message(" If you want to resize your partition to a smaller size, please complete") p_message(" any pending macOS upgrades and visit this link to learn how to manually") p_message(" delete Time Machine snapshots:") print() p_plain( f" {col(BLUE, BRIGHT)}https://alx.sh/tmcleanup{col()}") print() if avail < 2 * PART_ALIGN: p_error(" Not enough available space to resize. Please follow the instructions") p_error(" above to continue.") return False if not self.yesno("Continue anyway?"): return False print() if avail < 2 * PART_ALIGN: p_error("Not enough available space to resize.") return False p_question("Enter the new size for your existing partition:") p_message( " You can enter a size such as '1GB', a fraction such as '50%',") p_message( " or the word 'min' for the smallest allowable size.") print() p_message( " Examples:") p_message( " 30% - 30% to macOS, 70% to the new OS") p_message( " 80GB - 80GB to macOS, the rest to your new OS") p_message( " min - Shrink macOS as much as (safely) possible") print() default = "50%" if total / 2 < min_size: default = "min" while True: val = self.get_size("New size", default=default, min=min_size, total=total) if val is None: continue val = align_up(val, PART_ALIGN) if val < min_size: p_error(f"Size is too small, please enter a value > {ssize(min_size)} ({min_perc:.2f}%)") continue if val >= total: p_error(f"Size is too large, please enter a value < {ssize(total)}") continue freeing = total - val print() p_message(f"Resizing will free up {ssize(freeing)} of space.") if freeing <= MIN_INSTALL_FREE: if not self.expert: p_error(f"That's not enough free space for an OS install.") continue else: p_warning(f"That's not enough free space for an OS install.") print() p_message("Note: your system may appear to freeze during the resize.") p_message("This is normal, just wait until the process completes.") if self.yesno("Continue?"): break print() try: self.dutil.resizeContainer(target.name, val) except subprocess.CalledProcessError as e: print() p_error(f"Resize failed. This is usually caused by pre-existing APFS filesystem corruption.") p_warning("Carefully read the diskutil logs above for more information about the cause.") p_warning("This can usually be solved by doing a First Aid repair from Disk Utility in Recovery Mode.") return False print() p_success(f"Resize complete. Press enter to continue.") self.input() print() return True def action_select_disk(self): choices = {"1": "Internal storage"} for i, disk in enumerate(self.external_disks): choices[str(i + 2)] = f"{disk['IORegistryEntryName']} ({ssize(disk['Size'])})" print() p_question("Choose a disk:") idx = int(self.choice("Disk", choices)) if idx == 1: self.cur_disk = self.sys_disk else: self.cur_disk = self.external_disks[idx - 2]["DeviceIdentifier"] return True def main(self): print() p_message("Welcome to the Asahi Linux installer!") print() p_message("This installer is in an alpha state, and may not work for everyone.") p_message("It is intended for developers and early adopters who are comfortable") p_message("debugging issues or providing detailed bug reports.") print() p_message("Please make sure you are familiar with our documentation at:") p_plain( f" {col(BLUE, BRIGHT)}https://alx.sh/w{col()}") print() p_question("Press enter to continue.") self.input() print() self.expert = False if os.environ.get("EXPERT", None): p_message("By default, this installer will hide certain advanced options that") p_message("are only useful for Asahi Linux developers. You can enable expert mode") p_message("to show them. Do not enable this unless you know what you are doing.") p_message("Please do not file bugs if things go wrong in expert mode.") self.expert = self.yesno("Enable expert mode?") print() p_progress("Collecting system information...") self.sysinfo = system.SystemInfo() self.sysinfo.show() print() self.chip_min_ver = CHIP_MIN_VER.get(self.sysinfo.chip_id, None) self.device = DEVICES.get(self.sysinfo.device_class, None) if not self.chip_min_ver or not self.device or (self.device.expert_only and not self.expert): p_error("This device is not supported yet!") p_error("Please check out the Asahi Linux Blog for updates on device support:") print() p_error(" https://asahilinux.org/blog/") print() sys.exit(1) if self.sysinfo.boot_mode == "macOS" and ( (not self.sysinfo.login_user) or self.sysinfo.login_user == "unknown"): p_error("Could not detect logged in user.") p_error("Perhaps you are running this installer over SSH?") p_error("Please make sure a user is logged into the local console.") p_error("You can use SSH as long as there is a local login session.") sys.exit(1) if self.expert: min_ver = MIN_MACOS_VERSION_EXPERT else: min_ver = MIN_MACOS_VERSION if split_ver(self.sysinfo.macos_ver) < split_ver(min_ver): p_error("Your macOS version is too old.") p_error(f"Please upgrade to macOS {min_ver} or newer.") sys.exit(1) while self.main_loop(): pass def main_loop(self): p_progress("Collecting partition information...") self.dutil = diskutil.DiskUtil() self.dutil.get_info() if self.sys_disk is None: self.cur_disk = self.sys_disk = self.dutil.find_system_disk() p_info(f" System disk: {col()}{self.sys_disk}") if self.expert: self.external_disks = self.dutil.find_external_disks() else: self.external_disks = None if self.external_disks: p_info(f" Found {len(self.external_disks)} external disk(s)") self.parts = self.dutil.get_partitions(self.cur_disk) print() p_progress("Collecting OS information...") self.osinfo = osenum.OSEnum(self.sysinfo, self.dutil, self.cur_disk) self.osinfo.collect(self.parts) parts_free = [] parts_empty_apfs = [] parts_resizable = [] oses_incomplete = [] oses_upgradable = [] for i, p in enumerate(self.parts): if p.type in ("Apple_APFS_ISC",): continue if p.free: p.desc = f"(free space: {ssize(p.size)})" if p.size >= STUB_SIZE: parts_free.append(p) elif p.type.startswith("Apple_APFS"): p.desc = "APFS" if p.type == "Apple_APFS_Recovery": p.desc += " (System Recovery)" if p.label is not None: p.desc += f" [{p.label}]" if p.container is None: p.desc += f" (not a container)" else: vols = p.container["Volumes"] p.desc += f" ({ssize(p.size)}, {len(vols)} volume{'s' if len(vols) != 1 else ''})" if self.can_resize(p): parts_resizable.append(p) else: if p.size >= STUB_SIZE * 0.95: parts_empty_apfs.append(p) else: p.desc = f"{p.type} ({ssize(p.size)})" print() if self.cur_disk == self.sys_disk: t = "system" else: t = "external" p_message(f"Partitions in {t} disk ({self.cur_disk}):") if self.cur_disk == self.sys_disk: self.cur_os = None self.is_sfr_recovery = self.sysinfo.boot_vgid in (osenum.UUID_SROS, osenum.UUID_FROS) default_os = None r = col(YELLOW) + "R" + col() b = col(GREEN) + "B" + col() u = col(RED) + "?" + col() d = col(BRIGHT) + "*" + col() is_gpt = self.dutil.disks[self.cur_disk]["Content"] == "GUID_partition_scheme" for i, p in enumerate(self.parts): if p.desc is None: continue p_choice(f" {col(BRIGHT)}{i}{col()}: {p.desc}") if not p.os: continue for os in p.os: if not os.version: continue state = " " if self.sysinfo.boot_vgid == os.vgid and self.sysinfo.boot_uuid == os.rec_vgid: if p.type == "APFS": self.cur_os = os state = r elif self.sysinfo.boot_uuid == os.vgid: self.cur_os = os state = b elif self.sysinfo.boot_vgid == os.vgid: state = u if self.sysinfo.default_boot == os.vgid: default_os = os state += d else: state += " " p_plain(f" OS: [{state}] {os}") if os.stub and os.m1n1_ver and os.m1n1_ver != self.m1n1_ver: oses_upgradable.append((p, os)) elif os.stub and not (os.bp and os.bp.get("coih", None)): oses_incomplete.append((p, os)) print() p_plain(f" [{b} ] = Booted OS, [{r} ] = Booted recovery, [{u} ] = Unknown") p_plain(f" [ {d}] = Default boot volume") print() if self.cur_os is None and self.sysinfo.boot_mode != "macOS": self.cur_os = default_os self.check_cur_os() actions = {} default = None if oses_incomplete: actions["p"] = "Repair an incomplete installation" default = default or "p" if parts_free and is_gpt: actions["f"] = "Install an OS into free space" default = default or "f" if parts_empty_apfs and is_gpt and False: # This feature is confusing, disable it for now actions["a"] = "Install an OS into an existing APFS container" if parts_resizable and is_gpt: actions["r"] = "Resize an existing partition to make space for a new OS" default = default or "r" if self.cur_disk != self.sys_disk: actions["w"] = "Wipe and install into the whole disk" # Never make this default! if self.external_disks: actions["d"] = "Select another disk for installation" default = default or "d" if oses_upgradable: actions["m"] = "Upgrade m1n1 on an existing OS" default = default or "m" if not actions: p_error("No actions available on this system.") p_message("No partitions have enough free space to be resized, and there is") p_message("nothing else to be done.") sys.exit(1) actions["q"] = "Quit without doing anything" default = default or "q" print() p_question("Choose what to do:") act = self.choice("Action", actions, default) if act == "f": return self.action_install_into_free(parts_free) elif act == "a": return self.action_install_into_container(parts_empty_apfs) elif act == "r": return self.action_resize(parts_resizable) elif act == "m": return self.action_repair_or_upgrade(oses_upgradable, upgrade=True) elif act == "p": return self.action_repair_or_upgrade(oses_incomplete, upgrade=False) elif act == "d": return self.action_select_disk() elif act == "w": return self.action_wipe() elif act == "q": return False if __name__ == "__main__": logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(name)-12s %(levelname)-8s %(message)s', datefmt='%m-%d %H:%M', filename='installer.log', filemode='w') console = logging.StreamHandler() console.setLevel(logging.ERROR) formatter = logging.Formatter('%(name)-12s: %(levelname)-8s %(message)s') console.setFormatter(formatter) logging.getLogger('').addHandler(console) logging.info("Startup") logging.info("Environment:") for var in ("INSTALLER_BASE", "INSTALLER_DATA", "REPO_BASE", "IPSW_BASE", "EXPERT", "REPORT", "REPORT_TAG"): logging.info(f" {var}={os.environ.get(var, None)}") try: installer_version = open("version.tag", "r").read().strip() logging.info(f"Version: {installer_version}") InstallerMain(installer_version).main() except KeyboardInterrupt: print() logging.info("KeyboardInterrupt") p_error("Interrupted") except subprocess.CalledProcessError as e: cmd = shlex.join(e.cmd) p_error(f"Failed to run process: {cmd}") if e.output is not None: p_error(f"Output: {e.output}") logging.exception("Process execution failed") p_warning("If you need to file a bug report, please attach the log file:") p_warning(f" {os.getcwd()}/installer.log") except Exception: logging.exception("Exception caught") p_warning("If you need to file a bug report, please attach the log file:") p_warning(f" {os.getcwd()}/installer.log") asahi-installer-0.6.12/src/osenum.py000066400000000000000000000216271451217151600173420ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import os, os.path, plistlib, subprocess, logging from dataclasses import dataclass import m1n1 from util import * UUID_SROS = "3D3287DE-280D-4619-AAAB-D97469CA9C71" UUID_FROS = "C8858560-55AC-400F-BBB9-C9220A8DAC0D" @dataclass class OSInfo: partition: object vgid: str label: str = None sys_volume: str = None data_volume: str = None stub: bool = False version: str = None m1n1_ver: str = None system: object = None data: object = None preboot: object = None recovery: object = None rec_vgid: str = None preboot_vgid: str = None bp: object = None paired: bool = False admin_users: object = None def __str__(self): if self.vgid == UUID_SROS: return f"recoveryOS v{self.version} [Primary recoveryOS]" elif self.vgid == UUID_FROS: return f"recoveryOS v{self.version} [Fallback recoveryOS]" lbl = col(BRIGHT) + self.label + col() if not self.stub: macos = f"{col(BRIGHT, GREEN)}macOS v{self.version}{col()}" if self.m1n1_ver is not None: return f"[{lbl}] {macos} + {col(CYAN)}m1n1 {self.m1n1_ver}{col()} [{self.sys_volume}, {self.vgid}]" elif self.bp and self.bp.get("coih", None): return f"[{lbl}] {macos} + {col(YELLOW)}unknown fuOS{col()} [{self.sys_volume}, {self.vgid}]" else: return f"[{lbl}] {macos} [{self.sys_volume}, {self.vgid}]" elif self.bp and self.bp.get("coih", None): if self.m1n1_ver: return f"[{lbl}] {col(BRIGHT, CYAN)}m1n1 {self.m1n1_ver}{col()} (macOS {self.version} stub) [{self.sys_volume}, {self.vgid}]" else: return f"[{lbl}] {col(BRIGHT, YELLOW)}unknown fuOS{col()} (macOS {self.version} stub) [{self.sys_volume}, {self.vgid}]" else: return f"[{lbl}] {col(BRIGHT, RED)}incomplete install{col()} (macOS {self.version} stub) [{self.sys_volume}, {self.vgid}]" class OSEnum: def __init__(self, sysinfo, dutil, sysdsk): self.sysinfo = sysinfo self.dutil = dutil self.sysdsk = sysdsk def collect(self, parts): logging.info("OSEnum.collect()") for p in parts: p.os = [] if p.type == "Apple_APFS_Recovery": self.collect_recovery(p) else: self.collect_part(p) def collect_recovery(self, part): logging.info(f"OSEnum.collect_recovery(part={part.name})") if part.container is None: return recs = [] for volume in part.container["Volumes"]: if volume["Roles"] == ["Recovery"]: recs.append(volume) if len(recs) != 1: return os = OSInfo(partition=part, vgid=UUID_SROS, rec_vgid=recs[0]["APFSVolumeUUID"], version=self.sysinfo.sfr_ver) logging.info(f" Found SROS: {os}") part.os.append(os) if self.sysinfo.fsfr_ver: os = OSInfo(partition=part, vgid=UUID_FROS, version=self.sysinfo.fsfr_ver) logging.info(f" Found FROS: {os}") part.os.append(os) def collect_part(self, part): logging.info(f"OSEnum.collect_part(part={part.name})") if part.container is None: return part.os = [] ct = part.container ct_name = ct.get("ContainerReference", None) by_role = {} by_device = {} for volume in ct["Volumes"]: by_role.setdefault(tuple(volume["Roles"]), []).append(volume) by_device[volume["DeviceIdentifier"]] = volume volumes = {} for role in ("Preboot", "Recovery"): vols = by_role.get((role,), None) if not vols: logging.info(f" No {role} volume") return elif len(vols) > 1: logging.info(f" Multiple {role} volumes ({vols})") return volumes[role] = vols[0] for vg in ct["VolumeGroups"]: data = [i for i in vg["Volumes"] if i["Role"] == "Data"] system = [i for i in vg["Volumes"] if i["Role"] == "System"] if len(data) != 1 or len(system) != 1: logging.info(f" Weird VG: {vg['Volumes']}") continue data = data[0]["DeviceIdentifier"] system = system[0]["DeviceIdentifier"] volumes["Data"] = by_device[data] volumes["System"] = by_device[system] vgid = vg["APFSVolumeGroupUUID"] if self.sysinfo.boot_uuid == vgid: for volume in self.dutil.disk_parts[ct_name]["APFSVolumes"]: if "MountedSnapshots" not in volume: continue snapshots = volume["MountedSnapshots"] if volume["DeviceIdentifier"] == system and len(snapshots) == 1: volumes = dict(volumes) volumes["System"]["DeviceIdentifier"] = snapshots[0]["SnapshotBSD"] os = self.collect_os(part, volumes, vgid) logging.info(f" Found {os}") part.os.append(os) return part.os def collect_os(self, part, volumes, vgid): logging.info(f"OSEnum.collect_os(part={part.name}, vgid={vgid})") mounts = {} for role in ("Preboot", "Recovery", "System"): mounts[role] = self.dutil.mount(volumes[role]["DeviceIdentifier"]) logging.info(f" mounts[{role}]: {mounts[role]}") # Data will fail to mount for FileVault-enabled OSes; ignore that. try: mounts["Data"] = self.dutil.mount(volumes["Data"]["DeviceIdentifier"]) logging.info(f" mounts[Data]: {mounts['Data']}") except: mounts["Data"] = None logging.info(f" Failed to mount Data (FileVault?)") rec_vgid = volumes["Recovery"]["APFSVolumeUUID"] preboot_vgid = volumes["Preboot"]["APFSVolumeUUID"] stub = not os.path.exists(os.path.join(mounts["System"], "Library")) sys_volume = volumes["System"]["DeviceIdentifier"] data_volume = volumes["Data"]["DeviceIdentifier"] label = volumes["System"]["Name"] osi = OSInfo(partition=part, vgid=vgid, stub=stub, label=label, sys_volume=sys_volume, data_volume=data_volume, system=mounts["System"], data=mounts["Data"], preboot=mounts["Preboot"], recovery=mounts["Recovery"], rec_vgid=rec_vgid, preboot_vgid=preboot_vgid) for name in ("SystemVersion.plist", "SystemVersion-disabled.plist"): try: logging.info(f" Trying {name}...") sysver = plistlib.load(open(os.path.join(mounts["System"], "System/Library/CoreServices", name), "rb")) osi.version = sysver["ProductVersion"] logging.info(f" Version: {osi.version}") break except FileNotFoundError: logging.info(f" Not Found") continue try: auri = plistlib.load(open(os.path.join(mounts["Preboot"], vgid, "var/db/AdminUserRecoveryInfo.plist"), "rb")) osi.admin_users = list(auri.keys()) logging.info(f" Admin users: {osi.admin_users}") except: logging.warning(f" Failed to get AdminUserRecoveryInfo.plist") pass try: bps = self.bputil("-d", "-v", vgid) except subprocess.CalledProcessError: logging.warning(f" bputil failed") return osi osi.bp = {} for k in ("coih", "nsih"): tag = f"({k}): ".encode("ascii") if tag in bps: val = bps.split(tag)[1].split(b"\n")[0].decode("ascii") if val == "absent": val = None osi.bp[k] = val logging.info(f" BootPolicy[{k}] = {val}") if coih := osi.bp.get("coih", None): fuos_path = os.path.join(mounts["Preboot"], vgid, "boot", osi.bp["nsih"], "System/Library/Caches/com.apple.kernelcaches", "kernelcache.custom." + coih) if os.path.exists(fuos_path): osi.m1n1_ver = m1n1.get_version(fuos_path) if osi.m1n1_ver: logging.info(f" m1n1 version found: {osi.m1n1_ver}") if b": Paired" in bps: osi.paired = True return osi def bputil(self, *args): result = subprocess.run(["bputil"] + list(args), stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) return result.stdout asahi-installer-0.6.12/src/osinstall.py000066400000000000000000000161561451217151600200450ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import os, shutil, sys, stat, subprocess, urlcache, zipfile, logging import m1n1 from util import * class OSInstaller(PackageInstaller): PART_ALIGNMENT = 1024 * 1024 def __init__(self, dutil, data, template): super().__init__() self.dutil = dutil self.data = data self.template = template self.name = template["default_os_name"] self.ucache = None self.efi_part = None self.idata_targets = [] self.install_size = self.min_size @property def default_os_name(self): return self.template["default_os_name"] @property def min_size(self): return sum(self.align(psize(part["size"])) for part in self.template["partitions"]) @property def expandable(self): return any(part.get("expand", False) for part in self.template["partitions"]) @property def needs_firmware(self): return any(p.get("copy_firmware", False) for p in self.template["partitions"]) def align(self, v): return align_up(v, self.PART_ALIGNMENT) def load_package(self): package = self.template.get("package", None) if not package: return if not package.startswith("http"): package = os.environ.get("REPO_BASE", ".") + "/os/" + package logging.info(f"OS package URL: {package}") if package.startswith("http"): p_progress("Downloading OS package info...") self.ucache = urlcache.URLCache(package) self.pkg = zipfile.ZipFile(self.ucache) else: p_progress("Loading OS package info...") self.pkg = zipfile.ZipFile(open(package, "rb")) self.flush_progress() logging.info(f"OS package opened") def flush_progress(self): if self.ucache: self.ucache.flush_progress() def partition_disk(self, prev, total_size=None): self.part_info = [] logging.info("OSInstaller.partition_disk({prev}=!r)") if total_size is None: expand_size = 0 else: expand_size = total_size - self.min_size self.install_size = total_size assert expand_size >= 0 for part in self.template["partitions"]: logging.info(f"Adding partition: {part}") size = self.align(psize(part["size"])) ptype = part["type"] fmt = part.get("format", None) name = f"{part['name']} - {self.name}" logging.info(f"Partition Name: {name}") if part.get("expand", False): size += expand_size logging.info(f"Expanded to {size} (+{expand_size})") p_progress(f"Adding partition {part['name']} ({ssize(size)})...") info = self.dutil.addPartition(prev, f"%{ptype}%", "%noformat%", size) if ptype == "EFI": self.efi_part = info self.part_info.append(info) if fmt == "fat": p_plain(" Formatting as FAT...") args = ["newfs_msdos", "-F", "32", "-v", name[:11]] if "volume_id" in part: args.extend(["-I", part["volume_id"]]) args.append(f"/dev/r{info.name}") logging.info(f"Run: {args!r}") subprocess.run(args, check=True, stdout=subprocess.PIPE) elif fmt: raise Exception(f"Unsupported format {fmt}") prev = info.name def download_extras(self): p_progress("Downloading extra files...") logging.info("OSInstaller.download_extras()") mountpoint = self.dutil.mount(self.efi_part.name) dest = os.path.join(mountpoint, "asahi", "extras") os.makedirs(dest, exist_ok=True) count = len(self.template["extras"]) for i, url in enumerate(self.template["extras"]): base = os.path.basename(url) p_plain(f" Downloading {base} ({i + 1}/{count})...") ucache = urlcache.URLCache(url) data = ucache.read() with open(os.path.join(dest, base), "wb") as fd: fd.write(data) def install(self, stub_ins): p_progress("Installing OS...") logging.info("OSInstaller.install()") # Force a reconnect, since the connection is likely to have timed out if self.ucache is not None: self.ucache.close_connection() icon = self.template.get("icon", None) if icon: self.extract_file(icon, stub_ins.icon_path) self.flush_progress() for part, info in zip(self.template["partitions"], self.part_info): logging.info(f"Installing partition {part!r} -> {info.name}") image = part.get("image", None) if image: p_plain(f" Extracting {image} into {info.name} partition...") logging.info(f"Extract: {image}") zinfo = self.pkg.getinfo(image) with self.pkg.open(image) as sfd, \ open(f"/dev/r{info.name}", "r+b") as dfd: self.fdcopy(sfd, dfd, zinfo.file_size) self.flush_progress() source = part.get("source", None) if source: p_plain(f" Copying from {source} into {info.name} partition...") mountpoint = self.dutil.mount(info.name) logging.info(f"Copy: {source} -> {mountpoint}") self.extract_tree(source, mountpoint) self.flush_progress() if part.get("copy_firmware", False): mountpoint = self.dutil.mount(info.name) p_plain(f" Copying firmware into {info.name} partition...") base = os.path.join(mountpoint, "vendorfw") logging.info(f"Firmware -> {base}") shutil.copytree(self.firmware_package.path, base) if part.get("copy_installer_data", False): mountpoint = self.dutil.mount(info.name) data_path = os.path.join(mountpoint, "asahi") os.makedirs(data_path, exist_ok=True) self.idata_targets.append(data_path) if "extras" in self.template: assert self.efi_part is not None self.download_extras() p_progress("Preparing to finish installation...") logging.info(f"Building boot object") boot_object = self.template["boot_object"] next_object = self.template.get("next_object", None) logging.info(f" Boot object: {boot_object}") logging.info(f" Next object: {next_object}") m1n1_vars = [] if self.efi_part: m1n1_vars.append(f"chosen.asahi,efi-system-partition={self.efi_part.uuid.lower()}") if next_object is not None: assert self.efi_part is not None m1n1_vars.append(f"chainload={self.efi_part.uuid.lower()};{next_object}") logging.info(f"m1n1 vars:") for i in m1n1_vars: logging.info(f" {i}") m1n1.build(os.path.join("boot", boot_object), stub_ins.boot_obj_path, m1n1_vars) logging.info(f"Built boot object at {stub_ins.boot_obj_path}") asahi-installer-0.6.12/src/reporting.py000066400000000000000000000130601451217151600200350ustar00rootroot00000000000000import json, os, logging, time from urllib import request, parse from util import * EXPLANATIONS = { "device_class": "# The model code of your device. For example, j274ap means\n" "# 'Mac mini (M1, 2020)'.", "chip_id": "# The kind of chip your device has. For example, 0x8103 means Apple M1.\n" "# This is redundant (it is the same for any given device_class), but it\n" "# makes grouping reports by chips instead of devices a bit easier.", "macos_ver": "# The macOS version you are installing from. This helps us know how up\n" "# to date people are, so we can decide when to start requiring a newer\n" "# version. The text in parentheses is the build ID, which is the same\n" "# for any given release version of macOS (but varies for betas).", "sfr_ver": "# Your System Firmware version. This is usually the same as the macOS\n" "# version, unless you have multiple macOS installs.", "boot_mode": "# Whether you are installing from macOS or recoveryOS.", "os_name": "# The OS you selected to install. This lets us know what the most\n" "# popular OS choices are.", "os_package": "# The OS package filename. This tells us the particular OS version that\n" "# was installed.", "os_firmware": "# The firmware version used for this specific OS install. This is\n" "# one of a few specific options, since our kernels must support all\n" "# firmware versions used in the wild.", "disk_size": "# The SSD size of your machine, in gigabytes. This helps us gauge how\n" "# painful having to keep a macOS install around is for our users.", "disk_fraction": "# The fraction of your disk you allocated to your install, rounded to\n" "# 5%. This helps us understand how many people are using Asahi as\n" "# their primary OS, secondary OS, or just trying it out.", "installer": "# Version and configuration information for the installer. This lets us\n" "# know whether you used the official installer or something else.\n" "# The information is the same for everyone who installs using the same\n" "# command line.", } def show_data(data): print() p_message(f"This is the data that will be sent:") for line in json.dumps(data, indent=4).split("\n"): if not line: continue for key, exp in EXPLANATIONS.items(): indent = line[:len(line) - len(line.lstrip())] if line.strip().startswith(f'"{key}"'): exp = indent + exp.replace("\n", "\n" + indent) p_message(exp) break p_info(line) print() def report_inner(m, url, tag): disk_size = m.dutil.get_disk_size(m.cur_disk) size = round(20 * m.osins.install_size / disk_size) / 20 data = { "device_class": m.sysinfo.device_class, "chip_id": f"{m.sysinfo.chip_id:#x}", "macos_ver": f"{m.sysinfo.macos_ver} ({m.sysinfo.macos_build})", "sfr_ver": f"{m.sysinfo.sfr_ver} ({m.sysinfo.sfr_build})", "boot_mode": m.sysinfo.boot_mode, "os_name": m.osins.template["name"], "os_package": m.osins.template.get("package", None), "os_firmware": m.ipsw.version, "disk_size": round(disk_size / 1000_000_000), "disk_fraction": size, "installer": { "tag": tag, "version": m.version, "env": {}, }, } for var in ("INSTALLER_BASE", "INSTALLER_DATA", "REPO_BASE"): data["installer"]["env"][var] = os.environ.get(var, None) print() print() p_question("Help us improve Asahi Linux!") p_message("We'd love to know how many people are installing Asahi and on what") p_message("kind of hardware. Would you mind sending a one-time installation") p_message("report to us?") print() p_warning("This will only report what kind of machine you have, the OS you're") p_warning("installing, basic version info, and the rough install size.") p_warning("No personally identifiable information (such as serial numbers,") p_warning("specific partition sizes, etc.) is included. You can view the") p_warning("exact data that will be sent.") while True: print() p_question("Report your install?") act = m.choice("Choice (y/n/d)", { "y": "Yes", "n": "No", "d": "View the data that will be sent", }, default=None) if act == "n": return elif act == "d": show_data(data) continue elif act == "y": break else: assert False logging.info(f"Report data: {data!r}") try: headers = {"Content-Type": "application­/json"} postdata = json.dumps(data).encode("utf-8") req = request.Request(url, data=postdata, method="POST", headers=headers) with request.urlopen(req) as fd: fd.read() if fd.status == 200: p_success("Your install has been counted. Thank you! ❤") else: p_warning("Failed to send report! No worries.") except Exception as e: p_warning("Failed to send report! No worries.") print() print() time.sleep(1) def report(m): url = os.environ.get("REPORT", None) tag = os.environ.get("REPORT_TAG", None) if not tag or not url: return try: report_inner(m, url, tag) except Exception as e: logging.exception("Reporting failed, continuing...") asahi-installer-0.6.12/src/step2/000077500000000000000000000000001451217151600165075ustar00rootroot00000000000000asahi-installer-0.6.12/src/step2/Finish Installation.app/000077500000000000000000000000001451217151600231305ustar00rootroot00000000000000asahi-installer-0.6.12/src/step2/Finish Installation.app/Contents/000077500000000000000000000000001451217151600247255ustar00rootroot00000000000000asahi-installer-0.6.12/src/step2/Finish Installation.app/Contents/Info.plist000066400000000000000000000005411451217151600266750ustar00rootroot00000000000000 CFBundleDisplayName Finish Installation CFBundleExecutable step2_launcher.sh asahi-installer-0.6.12/src/step2/Finish Installation.app/Contents/MacOS/000077500000000000000000000000001451217151600256675ustar00rootroot00000000000000asahi-installer-0.6.12/src/step2/Finish Installation.app/Contents/MacOS/step2_launcher.sh000077500000000000000000000001631451217151600311440ustar00rootroot00000000000000#!/bin/sh exec /System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal "${0%/*}/../Resources/step2.sh" asahi-installer-0.6.12/src/step2/Finish Installation.app/Contents/Resources/000077500000000000000000000000001451217151600266775ustar00rootroot00000000000000asahi-installer-0.6.12/src/step2/Finish Installation.app/Contents/Resources/en.lproj/000077500000000000000000000000001451217151600304265ustar00rootroot00000000000000InfoPlist.strings000066400000000000000000000000571451217151600336730ustar00rootroot00000000000000asahi-installer-0.6.12/src/step2/Finish Installation.app/Contents/Resources/en.lproj"CFBundleDisplayName" = "Finish Installation"; asahi-installer-0.6.12/src/step2/IAPhysicalMedia.plist000066400000000000000000000006171451217151600225160ustar00rootroot00000000000000 AppName Finish Installation.app ProductBuildVersion 00A191 ProductVersion 12.1 asahi-installer-0.6.12/src/step2/step2.sh000077500000000000000000000111261451217151600201040ustar00rootroot00000000000000#!/bin/sh # SPDX-License-Identifier: MIT set -e VGID="##VGID##" PREBOOT="##PREBOOT##" self="$0" cd "${self%%step2.sh}" system_dir="$(cd ../../../; pwd)" os_name="${system_dir##*/}" # clear printf '\033[2J\033[H' echo "Asahi Linux installer (second step)" echo echo "VGID: $VGID" echo "System volume: $system_dir" echo BOLD="$(printf '\033[1m')" RST="$(printf '\033[m')" bputil -d -v "$VGID" >/tmp/bp.txt if ! grep -q ': Paired' /tmp/bp.txt; then echo "Your system did not boot into the correct recoveryOS." echo echo "Each OS in your machine comes with its own copy of recoveryOS." echo "In order to complete the installation, we need to boot into" echo "the brand new recoveryOS that matches the OS which you are" echo "installing. The final installation step cannot be completed from" echo "a different recoveryOS." echo echo "Normally this should happen automatically after the initial" echo "installer sets up your new OS as the default boot option," echo "but it seems something went wrong there. Let's try that again." echo echo "Press enter to continue." read while ! bless --setBoot --mount "$system_dir"; do echo echo "bless failed. Did you mistype your password?" echo "Press enter to try again." read done echo echo "Phew, hopefully that fixed it!" echo echo "Your system will now shut down. Once the screen goes blank," echo "please wait 10 seconds, then press the power button and do not" echo "release it until you see the 'Entering startup options...'" echo "message, then select '$os_name' again." echo echo "Press enter to shut down your system." read shutdown -h now exit 1 fi if ! grep -q 'one true recoveryOS' /tmp/bp.txt; then echo "Your system did not boot in One True RecoveryOS (1TR) mode." echo echo "To finish the installation, the system must be in this special" echo "mode. Perhaps you forgot to hold down the power button, or" echo "momentarily released it at some point?" echo echo "Note that tapping and then pressing the power button again will" echo "allow you to see the boot picker screen, but you will not be" echo "in the correct 1TR mode. You must hold down the power button" echo "in one continuous motion as you power on the machine." echo echo "Your system will now shut down. Once the screen goes blank," echo "please wait 10 seconds, then press the power button and do not" echo "release it until you see the 'Entering startup options...'" echo "message, then select '$os_name' again." echo echo "Press enter to shut down your system." read shutdown -h now exit 1 fi echo "You will see some messages advising you that you are changing the" echo "security level of your system. These changes apply only to your" echo "Asahi Linux install, and are necessary to install a third-party OS." echo echo "Apple Silicon platforms maintain a separate security level for each" echo "installed OS, and are designed to retain their security with mixed OSes." echo "${BOLD}The security level of your macOS install will not be affected.${RST}" echo echo "You will be prompted for login credentials two times." echo "Please enter your macOS credentials (for the macOS that you" echo "used to run the first step of the installation)." echo echo "Press enter to continue." echo read while ! bputil -nc -v "$VGID"; do echo echo "bputil failed. Did you mistype your password?" echo "Press enter to try again." read done echo echo if [ -e "/System/Volumes/iSCPreboot/$VGID/boot" ]; then # This is an external volume, and kmutil has a problem with trying to pick # up the AdminUserRecoveryInfo.plist from the wrong place. Work around that. diskutil mount "$PREBOOT" preboot="$(diskutil info "$PREBOOT" | grep "Mount Point" | sed 's, *Mount Point: *,,')" cp -R "$preboot/$VGID/var" "/System/Volumes/iSCPreboot/$VGID/" fi while ! kmutil configure-boot -c boot.bin --raw --entry-point 2048 --lowest-virtual-address 0 -v "$system_dir"; do echo echo "kmutil failed. Did you mistype your password?" echo "Press enter to try again." read done echo echo "Wrapping up..." echo mount -u -w "$system_dir" if [ -e "$system_dir/.IAPhysicalMedia" ]; then mv "$system_dir/.IAPhysicalMedia" "$system_dir/IAPhysicalMedia-disabled.plist" fi if [ -e "$system_dir/System/Library/CoreServices/SystemVersion-disabled.plist" ]; then mv -f "$system_dir/System/Library/CoreServices/SystemVersion"{-disabled,}".plist" fi echo echo "Installation complete! Press enter to reboot." read reboot asahi-installer-0.6.12/src/stub.py000066400000000000000000000422211451217151600170020ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import os, os.path, plistlib, shutil, sys, stat, subprocess, urlcache, zipfile, logging, json import osenum from asahi_firmware.wifi import WiFiFWCollection from asahi_firmware.bluetooth import BluetoothFWCollection from asahi_firmware.multitouch import MultitouchFWCollection from asahi_firmware.kernel import KernelFWCollection from asahi_firmware.isp import ISPFWCollection from util import * class StubInstaller(PackageInstaller): def __init__(self, sysinfo, dutil, osinfo): super().__init__() self.dutil = dutil self.sysinfo = sysinfo self.osinfo = osinfo self.ucache = None self.copy_idata = [] self.stub_info = {} self.pkg = None def load_ipsw(self, ipsw_info): self.install_version = ipsw_info.version.split(maxsplit=1)[0] base = os.environ.get("IPSW_BASE", None) url = ipsw_info.url if base: url = base + "/" + os.path.split(url)[-1] logging.info(f"IPSW URL: {url}") if url.startswith("http"): p_progress("Downloading macOS OS package info...") self.ucache = urlcache.URLCache(url) self.pkg = zipfile.ZipFile(self.ucache) else: p_progress("Loading macOS OS package info...") self.pkg = zipfile.ZipFile(open(url, "rb")) self.flush_progress() logging.info(f"OS package opened") print() def prepare_volume(self, part): logging.info(f"StubInstaller.prepare_volume({part.name=!r})") self.part = part by_role = {} ctref = self.part.container["ContainerReference"] p_progress("Preparing target volumes...") for volume in self.part.container["Volumes"]: roles = tuple(volume["Roles"]) logging.info(f" {volume['DeviceIdentifier']} roles: {roles}") by_role.setdefault(roles, []).append(volume) for role in ("Preboot", "Recovery", "Data", "System"): vols = by_role.get(role, []) if len(vols) > 1: raise Exception(f"Multiple {role} volumes") self.label = self.part.label or "Linux" if not by_role.get(("Data",), None): if default_vol := by_role.get((), None): self.dutil.changeVolumeRole(default_vol[0]["DeviceIdentifier"], "D") self.dutil.rename(default_vol[0]["DeviceIdentifier"], self.label + " - Data") else: self.dutil.addVolume(ctref, self.label, role="D") self.dutil.refresh_part(self.part) else: self.label = self.label.rstrip(" - Data") for volume in self.part.container["Volumes"]: if volume["Roles"] == ["Data",]: data_volume = volume["DeviceIdentifier"] break else: raise Exception("Could not find Data volume") if not by_role.get(("System",), None): self.dutil.addVolume(ctref, self.label, role="S", groupWith=data_volume) if not by_role.get(("Preboot",), None): self.dutil.addVolume(ctref, "Preboot", role="B") if not by_role.get(("Recovery",), None): self.dutil.addVolume(ctref, "Recovery", role="R") self.dutil.refresh_part(self.part) def check_volume(self, part=None): if part: self.part = part logging.info(f"StubInstaller.check_volume({self.part.name=!r})") p_progress("Checking volumes...") os = self.osinfo.collect_part(self.part) if len(os) != 1: raise Exception("Container is not ready for OS install") self.osi = os[0] def chflags(self, flags, path): logging.info(f"chflags {flags} {path}") subprocess.run(["chflags", flags, path], check=True) def get_paths(self): self.resources = os.path.join(self.osi.system, "Finish Installation.app/Contents/Resources") self.step2_sh = os.path.join(self.resources, "step2.sh") self.boot_obj_path = os.path.join(self.resources, "boot.bin") self.iapm_path = os.path.join(self.osi.system, ".IAPhysicalMedia") self.iapm_dis_path = os.path.join(self.osi.system, "IAPhysicalMedia-disabled.plist") self.core_services = os.path.join(self.osi.system, "System/Library/CoreServices") self.sv_path = os.path.join(self.core_services, "SystemVersion.plist") self.sv_dis_path = os.path.join(self.core_services, "SystemVersion-disabled.plist") self.icon_path = os.path.join(self.osi.system, ".VolumeIcon.icns") def check_existing_install(self, osi): self.osi = osi self.get_paths() if not os.path.exists(self.step2_sh): logging.error("step2.sh is missing") return False if not os.path.exists(self.boot_obj_path): logging.error("boot.bin is missing") return False if not any(os.path.exists(i) for i in (self.iapm_path, self.iapm_dis_path)): logging.error(".IAPhysicalMedia is missing") return False if not any(os.path.exists(i) for i in (self.sv_path, self.sv_dis_path)): logging.error("SystemVersion is missing") return False return True def prepare_for_bless(self): if not os.path.exists(self.sv_path): os.replace(self.sv_dis_path, self.sv_path) def prepare_for_step2(self): if os.path.exists(self.sv_path): os.replace(self.sv_path, self.sv_dis_path) if not os.path.exists(self.iapm_path): os.replace(self.iapm_dis_path, self.iapm_path) def install_files(self, cur_os): logging.info("StubInstaller.install_files()") logging.info(f"VGID: {self.osi.vgid}") logging.info(f"OS info: {self.osi}") p_progress("Beginning stub OS install...") ipsw = self.pkg self.get_paths() logging.info("Parsing metadata...") sysver = plistlib.load(ipsw.open("SystemVersion.plist")) manifest = plistlib.load(ipsw.open("BuildManifest.plist")) bootcaches = plistlib.load(ipsw.open("usr/standalone/bootcaches.plist")) self.flush_progress() self.manifest = manifest for identity in manifest["BuildIdentities"]: if (identity["ApBoardID"] != f'0x{self.sysinfo.board_id:02X}' or identity["ApChipID"] != f'0x{self.sysinfo.chip_id:04X}' or identity["Info"]["DeviceClass"] != self.sysinfo.device_class or identity["Info"]["RestoreBehavior"] != "Erase" or identity["Info"]["Variant"] != "macOS Customer"): continue break else: raise Exception("Failed to locate a usable build identity for this device") logging.info(f'Using OS build {identity["Info"]["BuildNumber"]} for {self.sysinfo.device_class}') self.all_identities = manifest["BuildIdentities"] self.identity = identity manifest["BuildIdentities"] = [identity] self.stub_info.update({ "vgid": self.osi.vgid, "system_version": sysver, "manifest_info": { "build_number": identity["Info"]["BuildNumber"], "variant": identity["Info"]["Variant"], "device_class": identity["Info"]["DeviceClass"], "board_id": identity["ApBoardID"], "chip_id": identity["ApChipID"], } }) p_progress("Setting up System volume...") logging.info("Setting up System volume") self.extract("usr/standalone/bootcaches.plist", self.osi.system) shutil.copy("logo.icns", self.icon_path) cs = os.path.join(self.osi.system, "System/Library/CoreServices") os.makedirs(cs, exist_ok=True) sysver["ProductUserVisibleVersion"] += " (stub)" self.extract("PlatformSupport.plist", cs) self.flush_progress() # Make the icon work try: logging.info(f"xattr -wx com.apple.FinderInfo .... {self.osi.system}") subprocess.run(["xattr", "-wx", "com.apple.FinderInfo", "0000000000000000040000000000000000000000000000000000000000000000", self.osi.system], check=True) except: p_error("Failed to apply extended attributes, logo will not work.") p_progress("Setting up Data volume...") logging.info("Setting up Data volume") os.makedirs(os.path.join(self.osi.data, "private/var/db/dslocal"), exist_ok=True) p_progress("Setting up Preboot volume...") logging.info("Setting up Preboot volume") pb_vgid = os.path.join(self.osi.preboot, self.osi.vgid) os.makedirs(pb_vgid, exist_ok=True) bless2 = bootcaches["bless2"] restore_bundle = os.path.join(pb_vgid, bless2["RestoreBundlePath"]) os.makedirs(restore_bundle, exist_ok=True) restore_manifest = os.path.join(restore_bundle, "BuildManifest.plist") with open(restore_manifest, "wb") as fd: plistlib.dump(manifest, fd) self.copy_idata.append((restore_manifest, "BuildManifest.plist")) self.extract("SystemVersion.plist", restore_bundle) self.extract("RestoreVersion.plist", restore_bundle) self.copy_idata.append((os.path.join(restore_bundle, "RestoreVersion.plist"), "RestoreVersion.plist")) self.extract("usr/standalone/bootcaches.plist", restore_bundle) self.extract_tree("BootabilityBundle/Restore/Bootability", os.path.join(restore_bundle, "Bootability")) self.extract_file("BootabilityBundle/Restore/Firmware/Bootability.dmg.trustcache", os.path.join(restore_bundle, "Bootability/Bootability.trustcache")) self.extract_tree("Firmware/Manifests/restore/macOS Customer/", restore_bundle) copied = set() self.kernel_path = None for key, val in identity["Manifest"].items(): if key in ("BaseSystem", "OS", "Ap,SystemVolumeCanonicalMetadata"): continue if key.startswith("Cryptex"): continue path = val["Info"]["Path"] if path in copied: continue self.extract(path, restore_bundle) if path.startswith("kernelcache."): name = os.path.basename(path) self.copy_idata.append((os.path.join(restore_bundle, name), name)) if self.kernel_path is None: self.kernel_path = os.path.join(restore_bundle, name) copied.add(path) self.flush_progress() os.makedirs(os.path.join(pb_vgid, "var/db"), exist_ok=True) admin_users = os.path.join(cur_os.preboot, cur_os.vgid, "var/db/AdminUserRecoveryInfo.plist") tg_admin_users = os.path.join(pb_vgid, "var/db/AdminUserRecoveryInfo.plist") if os.path.exists(tg_admin_users): self.chflags("noschg", tg_admin_users) shutil.copy(admin_users, tg_admin_users) self.copy_idata.append((tg_admin_users, "AdminUserRecoveryInfo.plist")) admin_users = plistlib.load(open(tg_admin_users, "rb")) self.stub_info["admin_users"] = {} for user, info in admin_users.items(): self.stub_info["admin_users"][user] = { "uid": info["GeneratedUID"], "real_name": info["RealName"], } # Stop macOS <12.0 bootability stufff from clobbering this file self.chflags("schg", tg_admin_users) # This is a workaround for some screwiness in the macOS <12.0 bootability # code, which ends up putting the apticket in the wrong volume... sys_restore_bundle = os.path.join(self.osi.system, bless2["RestoreBundlePath"]) if os.path.lexists(sys_restore_bundle): os.unlink(sys_restore_bundle) os.symlink(restore_bundle, sys_restore_bundle) p_progress("Setting up Recovery volume...") logging.info("Setting up Recovery volume") rec_vgid = os.path.join(self.osi.recovery, self.osi.vgid) os.makedirs(rec_vgid, exist_ok=True) basesystem_path = os.path.join(rec_vgid, "usr/standalone/firmware") os.makedirs(basesystem_path, exist_ok=True) logging.info("Extracting arm64eBaseSystem.dmg") self.copy_compress(identity["Manifest"]["BaseSystem"]["Info"]["Path"], os.path.join(basesystem_path, "arm64eBaseSystem.dmg")) self.flush_progress() p_progress("Wrapping up...") logging.info("Writing SystemVersion.plist") with open(self.sv_path, "wb") as fd: plistlib.dump(sysver, fd) self.copy_idata.append((self.sv_path, "SystemVersion.plist")) if os.path.exists(self.sv_dis_path): os.remove(self.sv_dis_path) logging.info("Copying Finish Installation.app") shutil.copytree("step2/Finish Installation.app", os.path.join(self.osi.system, "Finish Installation.app")) logging.info("Writing step2.sh") step2_sh = open("step2/step2.sh").read() step2_sh = step2_sh.replace("##VGID##", self.osi.vgid) step2_sh = step2_sh.replace("##PREBOOT##", self.osi.preboot_vgid) with open(self.step2_sh, "w") as fd: fd.write(step2_sh) os.chmod(self.step2_sh, 0o755) logging.info("Copying .IAPhysicalMedia") shutil.copy("step2/IAPhysicalMedia.plist", self.iapm_path) if os.path.exists(self.iapm_dis_path): os.remove(self.iapm_dis_path) print() p_success("Stub OS installation complete.") logging.info("Stub OS installed") print() def collect_firmware(self, pkg): p_progress("Collecting firmware...") logging.info("StubInstaller.collect_firmware()") logging.info("Collecting FUD firmware") if os.path.exists("fud_firmware"): shutil.rmtree("fud_firmware") os.makedirs("fud_firmware", exist_ok=True) copied = set() for identity in [self.identity]: if (identity["Info"]["RestoreBehavior"] != "Erase" or identity["Info"]["Variant"] != "macOS Customer"): continue device = identity["Info"]["DeviceClass"] if not device.endswith("ap"): continue logging.info(f"Collecting FUD firmware for device {device}") device = device[:-2] for key, val in identity["Manifest"].items(): if key in ("BaseSystem", "OS", "Ap,SystemVolumeCanonicalMetadata", "StaticTrustCache", "SystemVolume"): continue path = val["Info"]["Path"] if (not val["Info"].get("IsFUDFirmware", False) or val["Info"].get("IsLoadedByiBoot", False) or val["Info"].get("IsLoadedByiBootStage1", False) or not path.endswith(".im4p")): continue if path not in copied: self.extract(path, "fud_firmware") copied.add(path) fud_dir = os.path.join("fud_firmware", device) os.makedirs(fud_dir, exist_ok=True) os.symlink(os.path.join("..", path), os.path.join(fud_dir, key + ".im4p")) img = os.path.join(self.osi.recovery, self.osi.vgid, "usr/standalone/firmware/arm64eBaseSystem.dmg") logging.info("Attaching recovery ramdisk") subprocess.run(["hdiutil", "attach", "-quiet", "-readonly", "-mountpoint", "recovery", img], check=True) logging.info("Collecting WiFi firmware") col = WiFiFWCollection("recovery/usr/share/firmware/wifi/") pkg.add_files(sorted(col.files())) logging.info("Collecting Bluetooth firmware") col = BluetoothFWCollection("recovery/usr/share/firmware/bluetooth/") pkg.add_files(sorted(col.files())) logging.info("Collecting Multitouch firmware") col = MultitouchFWCollection("fud_firmware/") pkg.add_files(sorted(col.files())) logging.info("Collecting ISP firmware") col = ISPFWCollection("recovery/usr/sbin/") pkg.add_files(sorted(col.files())) logging.info("Collecting Kernel firmware") col = KernelFWCollection(self.kernel_path) pkg.add_files(sorted(col.files())) logging.info("Making fallback firmware archive") subprocess.run(["tar", "czf", "all_firmware.tar.gz", "fud_firmware", "-C", "recovery/usr/share", "firmware", "-C", "../../usr/sbin", "appleh13camerad", ], check=True) self.copy_idata.append(("all_firmware.tar.gz", "all_firmware.tar.gz")) logging.info("Detaching recovery ramdisk") subprocess.run(["hdiutil", "detach", "-quiet", "recovery"]) def collect_installer_data(self, path): p_progress("Collecting installer data...") logging.info(f"Copying installer data to {path}") for src, name in self.copy_idata: shutil.copy(src, os.path.join(path, name)) with open(os.path.join(path, "stub_info.json"), "w") as fd: json.dump(self.stub_info, fd) asahi-installer-0.6.12/src/system.py000066400000000000000000000145461451217151600173620ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import base64, plistlib, struct, subprocess, logging from util import * class SystemInfo: def __init__(self): self.fetch() def fetch(self): result = subprocess.run(["ioreg", "-alp", "IODeviceTree"], stdout=subprocess.PIPE, check=True) self.ioreg = plistlib.loads(result.stdout) for dt in self.ioreg["IORegistryEntryChildren"]: if dt.get("IOObjectClass", None) == "IOPlatformExpertDevice": break else: raise Exception("Could not find IOPlatformExpertDevice") self.dt = dt self.chosen = chosen = self.get_child(dt, "chosen") self.product = product = self.get_child(dt, "product") sys_compat = self.get_list(dt["compatible"]) self.device_class = sys_compat[0].lower() self.product_type = sys_compat[1] self.board_id = self.get_int(chosen["board-id"]) self.chip_id = self.get_int(chosen["chip-id"]) self.sys_firmware = self.get_str(chosen["system-firmware-version"]) self.boot_uuid = self.get_str(chosen["boot-uuid"]) boot_vgid = chosen.get("associated-volume-group", None) if boot_vgid is None: boot_vgid = chosen.get("apfs-preboot-uuid", None) if boot_vgid is not None: self.boot_vgid = self.get_str(boot_vgid) else: boot_path = chosen.get("boot-objects-path", None) if boot_path: self.boot_vgid = self.get_str(boot_path).split("/")[1] else: self.boot_vgid = self.boot_uuid self.product_name = self.get_str(product["product-name"]) self.soc_name = self.get_str(product["product-soc-name"]) self.get_nvram_data() bputil_info = b'' for vgid in (self.boot_vgid, self.default_boot): if not vgid: continue try: bputil_info = subprocess.run(["bputil", "-d", "-v", vgid], check=True, capture_output=True).stdout break except subprocess.CalledProcessError: continue self.boot_mode = "Unknown" # one of 'macOS', 'one true recoveryOS', 'recoveryOS'(?), 'ordinary recoveryOS' if b"Current OS environment: " in bputil_info: self.boot_mode = (bputil_info.split(b"Current OS environment: ")[1] .split(b"\n")[0].decode("ascii")) elif b"OS Type" in bputil_info: boot_mode = bputil_info.split(b"OS Type")[1].split(b"\n")[0] self.boot_mode = boot_mode.split(b": ")[1].decode("ascii") self.macos_ver, self.macos_build = self.get_version("/System/Library/CoreServices/SystemVersion.plist") self.sfr_ver, self.sfr_build = self.get_version("/System/Volumes/iSCPreboot/SFR/current/SystemVersion.plist") self.fsfr_ver, self.fsfr_build = self.get_version("/System/Volumes/iSCPreboot/SFR/fallback/SystemVersion.plist") self.sfr_full_ver = self.get_restore_version("/System/Volumes/iSCPreboot/SFR/current/RestoreVersion.plist") self.login_user = None scout = subprocess.run(["scutil"], input=b"show State:/Users/ConsoleUser\n", stdout=subprocess.PIPE).stdout.strip() for line in scout.split(b"\n"): if b"kCGSSessionUserNameKey : " in line: consoleuser = line.split(b"kCGSSessionUserNameKey : ")[1].decode("ascii") if consoleuser != "_mbsetupuser": self.login_user = consoleuser def get_nvram_data(self): nvram_data = subprocess.run(["nvram", "-p"], stdout=subprocess.PIPE, check=True).stdout self.nvram = {} for line in nvram_data.rstrip(b"\n").split(b"\n"): try: k, v = line.split(b"\t", 1) k = k.decode("ascii") v = v.decode("utf-8") self.nvram[k] = v except: logging.warning(f"Bad nvram line: {line!r}") continue # Hopefully we don't need this value... self.default_boot = None if "boot-volume" in self.nvram: self.default_boot = self.nvram["boot-volume"].split(":")[2] def get_version(self, name): try: data = plistlib.load(open(name, "rb")) return data["ProductVersion"], data["ProductBuildVersion"] except: return None, None def get_restore_version(self, name): try: data = plistlib.load(open(name, "rb")) return data["RestoreLongVersion"] except: return None def show(self): p_info(f" Product name: {col()}{self.product_name}") p_info(f" SoC: {col()}{self.soc_name}") p_info(f" Device class: {col()}{self.device_class}") p_info(f" Product type: {col()}{self.product_type}") p_info(f" Board ID: {col()}{self.board_id:#x}") p_info(f" Chip ID: {col()}{self.chip_id:#x}") p_info(f" System firmware: {col()}{self.sys_firmware}") p_info(f" Boot UUID: {col()}{self.boot_uuid}") p_info(f" Boot VGID: {col()}{self.boot_vgid}") p_info(f" Default boot VGID: {col()}{self.default_boot}") p_info(f" Boot mode: {col()}{self.boot_mode}") p_info(f" OS version: {col()}{self.macos_ver} ({self.macos_build})") p_info(f" SFR version: {col()}{self.sfr_full_ver}") p_info(f" System rOS version: {col()}{self.sfr_ver} ({self.sfr_build})") if self.fsfr_ver: p_info(f" Fallback rOS version: {col()}{self.fsfr_ver} ({self.fsfr_build})") else: p_info(f" No Fallback rOS") p_info(f" Login user: {col()}{self.login_user}") def get_child(self, obj, name): for child in obj["IORegistryEntryChildren"]: if child.get("IORegistryEntryName", None) == name: break else: raise Exception(f"Could not find {name}") return child def get_list(self, val): return [i.decode("ascii") for i in val.rstrip(b"\x00").split(b"\x00")] def get_str(self, val): return val.rstrip(b"\0").decode("ascii") def get_int(self, val): return struct.unpack(" 0: self.readahead = self.MIN_READAHEAD size = min(size, self.readahead * self.BLOCKSIZE) else: break off = 0 blk2 = blk while off < len(data): self.cache[blk2] = CacheBlock(idx=blk2, data=data[off:off + self.BLOCKSIZE]) off += self.BLOCKSIZE blk2 += 1 return self.cache[blk] def seek(self, offset, whence=os.SEEK_SET): if whence == os.SEEK_SET: self.p = offset elif whence == os.SEEK_END: self.p = self.size + offset elif whence == os.SEEK_CUR: self.p += offset def tell(self): return self.p def read(self, count=None): if count is None: count = self.size - self.p blk_start = self.p // self.BLOCKSIZE blk_end = (self.p + count - 1) // self.BLOCKSIZE blocks = blk_end - blk_start + 1 d = [] for blk in range(blk_start, blk_end + 1): readahead = blk_end - blk + 1 d.append(self.get_block(blk, readahead).data) prog = (blk - blk_start + 1) / blocks * 100 self.blocks_read += 1 trim = self.p - (blk_start * self.BLOCKSIZE) d[0] = d[0][trim:] d = b"".join(d)[:count] assert len(d) == count self.p += count return d def flush_progress(self): if self.blocks_read > 0: sys.stdout.write("\n") self.blocks_read = 0 return True else: return False if __name__ == "__main__": import sys, zipfile from util import PackageInstaller url = sys.argv[1] ucache = URLCache(url) zf = zipfile.ZipFile(ucache) pi = PackageInstaller() pi.ucache = ucache pi.pkg = zf for f in zf.infolist(): print(f) for i in sys.argv[2:]: dn = os.path.dirname(i) if dn: os.makedirs(dn, exist_ok=True) pi.extract_file(i, i, False) asahi-installer-0.6.12/src/util.py000066400000000000000000000172521451217151600170100ustar00rootroot00000000000000# SPDX-License-Identifier: MIT import re, logging, sys, os, stat, shutil, struct, subprocess, zlib, time from ctypes import * if sys.platform == 'darwin': lzfse = CDLL('libcompression.dylib') else: lzfse = None COMPRESSION_LZFSE = 0x801 CHUNK_SIZE = 0x10000 def ssize(v): suffixes = ["B", "KB", "MB", "GB", "TB"] for i in suffixes: if v < 1000 or i == suffixes[-1]: if isinstance(v, int): return f"{v} {i}" else: return f"{v:.2f} {i}" v /= 1000 def psize(v, align=None): v = v.upper().replace(" ", "") base = 1000 if v[-2] == "I": base = 1024 v = v[:-2] + v[-1] suffixes = {"TB": 4, "GB": 3, "MB": 2, "KB": 1, "B": 0, "": 0} for suffix, power in suffixes.items(): if v.endswith(suffix): val = int(float(v[:-len(suffix)]) * (base ** power)) break else: return None if align is not None: if isinstance(align, str): align = psize(align) assert align is not None val = align_up(val, align) return val def split_ver(s): parts = re.split(r"[-,. ]", s) parts2 = [] for i in parts: try: parts2.append(int(i)) except ValueError: parts2.append(i) if len(parts2) > 3 and parts2[-2] == "beta": parts2[-3] -= 1 parts2[-2] = 99 return tuple(parts2) def align_up(v, a=16384): return (v + a - 1) & ~(a - 1) align = align_up def align_down(v, a=16384): return v & ~(a - 1) BLACK = 30 RED = 31 GREEN = 32 YELLOW = 33 BLUE = 34 MAGENTA = 35 CYAN = 36 WHITE = 37 BRIGHT = 1 DIM = 2 NORMAL = 22 RESET_ALL = 0 def col(*color): color = ";".join(map(str, color)) return f"\033[{color}m" def p_style(*args, color=[], **kwargs): if isinstance(color, int): color = [color] text = " ".join(map(str, args)) print(col(*color) + text + col(), **kwargs) if "\033" in text: text += col() logging.info(f"MSG: {text}") def p_plain(*args): p_style(*args) def p_info(*args): p_style(*args, color=(BRIGHT, BLUE)) def p_progress(*args): p_style(*args, color=(BRIGHT, MAGENTA)) def p_message(*args): p_style(*args, color=BRIGHT) def p_error(*args): p_style(*args, color=(BRIGHT, RED)) def p_warning(*args): p_style(*args, color=(BRIGHT, YELLOW)) def p_question(*args): p_style(*args, color=(BRIGHT, CYAN)) def p_success(*args): p_style(*args, color=(BRIGHT, GREEN)) def p_prompt(*args): p_style(*args, color=(BRIGHT, CYAN)) def p_choice(*args): p_style(*args) def input_prompt(*args): while True: p_style(f"{col(BRIGHT, WHITE)}»{col(BRIGHT, CYAN)}", *args, end="") val = input() if any (ord(c) < 0x20 for c in val): p_error("Invalid input") continue break logging.info(f"INPUT: {val!r}") return val class PackageInstaller: def __init__(self): self.verbose = "-v" in sys.argv self.printed_progress = False def flush_progress(self): if self.ucache and self.ucache.flush_progress(): self.printed_progress = False return if self.printed_progress: sys.stdout.write("\n") self.printed_progress = False def extract(self, src, dest): logging.info(f" {src} -> {dest}/") self.pkg.extract(src, dest) def fdcopy(self, sfd, dfd, size=None): BLOCK = 16 * 1024 * 1024 copied = 0 bps = 0 st = time.time() self.ucache.bytes_read = 0 while True: if size is not None: prog = copied / size * 100 sys.stdout.write(f"\033[3G{prog:6.2f}% ({ssize(bps)}/s)") sys.stdout.flush() self.printed_progress = True d = sfd.read(BLOCK) if not d: break dfd.write(d) copied += len(d) bps = self.ucache.bytes_read / (time.time() - st) if size is not None: sys.stdout.write("\033[3G100.00% ") sys.stdout.flush() def copy_compress(self, src, path): info = self.pkg.getinfo(src) size = info.file_size istream = self.pkg.open(src) with open(path, 'wb'): pass num_chunks = (size + CHUNK_SIZE - 1) // CHUNK_SIZE cur_pos = (num_chunks + 1) * 4 table = [] scratch = bytes(lzfse.compression_encode_scratch_buffer_size(COMPRESSION_LZFSE)) outbuf = bytes(CHUNK_SIZE) st = time.time() self.ucache.bytes_read = 0 copied = 0 with open(path + '/..namedfork/rsrc', 'wb') as res_fork: res_fork.write(b'\0' * cur_pos) for i in range(num_chunks): table.append(cur_pos) inbuf = istream.read(CHUNK_SIZE) copied += len(inbuf) prog = copied / size * 100 bps = self.ucache.bytes_read / (time.time() - st) sys.stdout.write(f"\033[3G{prog:6.2f}% ({ssize(bps)}/s)") sys.stdout.flush() self.printed_progress = True while 1: csize = lzfse.compression_encode_buffer(outbuf, len(outbuf), inbuf, len(inbuf), scratch, COMPRESSION_LZFSE) if csize == 0: outbuf = bytes(len(outbuf) * 2) else: break res_fork.write(outbuf[:csize]) cur_pos += csize table.append(cur_pos) res_fork.seek(0) for v in table: res_fork.write(struct.pack('> 8*i) & 0xff):02x}" for i in range(8)), path], check=True) os.chflags(path, stat.UF_COMPRESSED) crc = 0 with open(path, 'rb') as result_file: while 1: data = result_file.read(CHUNK_SIZE) if len(data) == 0: break crc = zlib.crc32(data, crc) if crc != info.CRC: raise Exception('Internal error: failed to compress file: crc mismatch') sys.stdout.write("\033[3G100.00% ") sys.stdout.flush() def extract_file(self, src, dest, optional=True): try: info = self.pkg.getinfo(src) with self.pkg.open(src) as sfd, \ open(dest, "wb") as dfd: logging.info(f" {src} -> {dest}") self.fdcopy(sfd, dfd, info.file_size) except KeyError: if not optional: raise if self.verbose: self.flush_progress() def extract_tree(self, src, dest): if src[-1] != "/": src += "/" logging.info(f" {src}* -> {dest}") infolist = self.pkg.infolist() if self.verbose: self.flush_progress() for info in infolist: name = info.filename if not name.startswith(src): continue subpath = name[len(src):] assert subpath[0:1] != "/" destpath = os.path.join(dest, subpath) if info.is_dir(): os.makedirs(destpath, exist_ok=True) elif stat.S_ISLNK(info.external_attr >> 16): link = self.pkg.open(info.filename).read() if os.path.lexists(destpath): os.unlink(destpath) os.symlink(link, destpath) else: self.extract_file(name, destpath) if self.verbose: self.flush_progress() asahi-installer-0.6.12/test.sh000077500000000000000000000012701451217151600162010ustar00rootroot00000000000000#!/bin/sh set -e cd "$(dirname "$0")" base="$PWD" if [ -e $HOME/.cargo/env ] ; then source $HOME/.cargo/env fi export INSTALLER_BASE=https://cdn.asahilinux.org/installer-dev export REPO_BASE=https://cdn.asahilinux.org make -C "m1n1" RELEASE=1 CHAINLOADING=1 -j4 sudo rm -rf /tmp/asahi-install mkdir -p /tmp/asahi-install git describe --tags --always --dirty > /tmp/asahi-install/version.tag cd /tmp/asahi-install ln -sf "$base/src"/* . ln -sf "$base/asahi_firmware" . mkdir -p boot ln -sf "$base/m1n1/build/m1n1.bin" boot/m1n1.bin ln -sf "$base/artwork/logos/icns/AsahiLinux_logomark.icns" logo.icns ln -sf "$base/data/installer_data.json" installer_data.json sudo -E python3 main.py asahi-installer-0.6.12/tools/000077500000000000000000000000001451217151600160235ustar00rootroot00000000000000asahi-installer-0.6.12/tools/cleanbp.sh000077500000000000000000000005751451217151600177750ustar00rootroot00000000000000#!/bin/sh set -e cat > /tmp/uuids.txt <> /tmp/uuids.txt cd /System/Volumes/iSCPreboot for i in ????????-????-????-????-????????????; do if grep -q "$i" /tmp/uuids.txt; then echo "KEEP $i" else echo "RM $i" rm -rf "$i" fi done asahi-installer-0.6.12/tools/wipe-linux.sh000077500000000000000000000021241451217151600204620ustar00rootroot00000000000000#!/bin/sh set -e echo "THIS SCRIPT IS DANGEROUS!" echo "DO NOT BLINDLY RUN IT IF SOMEONE JUST SENT YOU HERE." echo "IT WILL INDISCRIMINATELY WIPE A BUNCH OF PARTITIONS" echo "THAT MAY OR MAY NOT BE THE ONES YOU WANT TO WIPE." echo echo "You are much better off reading and understanding this guide:" echo "https://github.com/AsahiLinux/docs/wiki/Partitioning-cheatsheet" echo echo "Press enter twice if you really want to continue." echo "Press Control-C to exit." read read diskutil list | grep Apple_APFS | grep '\b2\.5 GB' | sed 's/.* //g' | while read i; do diskutil apfs deleteContainer "$i" done diskutil list /dev/disk0 | grep -Ei 'asahi|linux|EFI' | sed 's/.* //g' | while read i; do diskutil eraseVolume free free "$i" done cat > /tmp/uuids.txt <> /tmp/uuids.txt cd /System/Volumes/iSCPreboot for i in ????????-????-????-????-????????????; do if grep -q "$i" /tmp/uuids.txt; then echo "KEEP $i" else echo "RM $i" rm -rf "$i" fi done asahi-installer-0.6.12/vendor/000077500000000000000000000000001451217151600161605ustar00rootroot00000000000000asahi-installer-0.6.12/vendor/libffi/000077500000000000000000000000001451217151600174135ustar00rootroot00000000000000asahi-installer-0.6.12/vendor/libffi/libffi.8.dylib000066400000000000000000004150601451217151600220470ustar00rootroot00000000000000  __TEXT@@__text__TEXT@@__stubs__TEXT __const__TEXTH__unwind_info__TEXT__DATA_CONST@@@@__got__DATA_CONST@p@ __const__DATA_CONSTp@0p@__DATA@@__data__DATA`__bss__DATA`H__LINKEDIT0Z H /opt/homebrew/opt/libffi/lib/libffi.8.dylib4`3`hIP P /;Yf%282  * 8/usr/lib/libSystem.B.dylib&H)`Jg_WO{(Qq(T)@(@y5qqS4@@2 5@@ @y)Q*Q* *?j)Q 2 "qT4{DOCWB_AgŨ@R4 R{DOCWB_AgŨ_{DOCWB_AgŨ@@WO{@ @y @ 5! @`5h@_ @y*Hhth@ @ hj@y_ kIiy@ *) (7h{BOAWè_ RRtWO{  X"Re5 qcT*h")ъ  @ Tl@y2=qTk@T!)TR`R{BOAWè_QqT(@y5qT@R_ R_ @)qdT@ @K@y5qaT J@JJ @ @!)qeT__@4 @*@J@y_=q`T_5qT*@JA*@J@JJ @JA)!B qT__WO{@4@ zv @y)Q?)q(T ki8J @ֈzv zv@9zv9zv@yzvyzv@zvzv@.zv@ @zv@h@kCT{COBWA_Ĩ_WO{C hX@@} X?=}|6] iX)@?T{BOAWè_֞.WO{B @5{BOAWè__WO{C ըX@E pX? <)| ghB?֨\ թX)@?T{COBWA_Ĩ__.WO{b @5{BOAWè_{ @)qDT@ @J@y_)qMTL-Q R qCT_=qT_5qT  R_ q@T R !)qT{_..{@h4 @Kyik@yl-Q qd)CzTAx)!@=qTAx)! )?k#T{_.@4 @*p +yhk@yk Q1qHTMik8 +xhk@K+xh`@@+xhk@9+xhk9+xhk@y +xhky +xhk@+xhk+xhk@K @ kT_WO{C hX@@} X?=}|6] iX)@?T{BOAWè_ֶ-WO{B @5{BOAWè__WO{C ըX@J pX? <)| xhB?֨\ թX)@?T{COBWA_Ĩ_w-WO{b @5{BOAWè__WO{i- @  j-2@u @w ՗X@R#Rc-@5@@@cs# RRRV-`5s@9H7@@RL-ҵ2@H2 @@ @yQy@  5-@u^  RR-Ry@RR-@ K@K  k@K )A!T @  --{FOEWD_C_O{C  -t"@ @  @y y 1@_q$TT?T&I41@h@ @I( X@@R,@,,@  ,{AO¨,R______WO{@@y=qTR@ Պ+ih8J @65@IR!JR+RJA)" RE4124R R RTRRRRu@qKTw@@5"aT2Q 2hR)"R{COBWA_Ĩ_v,_WO{@yh Q qT7RhvSK16q`T>qT@@@yh Q qTWR@T@@@yz5qAT6"@7q TqT qAT@TB @TCTR{COBWA_Ĩ_"R 5qT5"5kT@4@y"O{C]"R{AO¨_og_WO{C (X@@ @4C)yBJ|_J+_riCL  IJM| p}X?) ?)(5H@q+TRRRX@ys@y=qT@@  jy) R4R(KX @6@? qT !q TY)M8 QRJ}+kk `֐G@-OA-!G@- @G@- Ր@ ՐG@mOAmG@m @G@m Ր@ ՐG@OA G@ =G@ Ր=3 =2 =1=0=TBi TR@N !qHTX)Q9 QRJ}+kk `֐G@-OA-!G@- @G@- Ր@ ՐG@mOAmG@m @G@m Ր@ ՐG@OA G@ =G@ Ր=3 =2 =1=0=O@yZ) !D;@zR  Y R<@99@yy@qTY(Y80@yZ) !D;@zR  Y *+RC A8?!T*Y@  @yZ) !D;@zR  Y R +s"@RZ"!TwSvCW 06u*Z )]X)@?ATC{EODWC_BgAoƨ_**4 <DLT\ $$$)@)Q?q(TR)d d +@rIh*h* _@R_og_WO{Cѥ78 UX@ @} հSX?=}|"_@CqKTRRR+RMRRR9@yth@y=q(Tz@h! j@TI_#:@z( N9J?q<Hx4pR j T`4R(KCY)@6@? qT A9?!HT*  _GCT @ %N9J?qRH@yHx4+RMRRB A8?!T*  hy@x4+RMRR2HC A9?!y+RMRRRHT* )Ix49 #h@y !:@zR YHx4RRh@y !:@zR YHx4RZ+RMRR"@Th@y !:@z Hx4RCYr#w!#x?֨Z Չ<X)@?aT`zC{EODWC_BgAoƨ_))O{CH QqhT)  +yJ @0D@2LAp $@THS R"K)0D@2=p 0D@p `T @`0D@2LAp 0D@2=p 0D@p `T @`{AO¨_@P\pO{C @(@3!@y x?5qT6h@{AO¨_O{C@(@!@y y?5qT5kT@ R{AO¨_R=x)86@ABCHIJK ?֣A@?{@ @ `=`;} 9} 7} 5} 3} 1` /` -`-+`)` '` %`m#`!c = b= ``=S` <S` *` @`  <@` |@` _   {  {    A"@CCC! C   > `@;`@9}  }  }  }  }  c @ b@ `@-)`@'c @ b@ `@m!`@c = b= `@`=`@9}  `@y}  `@ }  `9 }  `y}  ` {Ѩ_                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ B@ 0@0@0@0@0@0"@0&@0*@0.@02@06@      44@LLL |<4p ( p ,   <@     00x 2 h0@@@$Hn|.Hf___chkstk_darwin___stack_chk_fail___stack_chk_guard_abort_calloc_free_mach_task_self__malloc_memcpy_pthread_mutex_lock_pthread_mutex_unlock_vm_allocate_vm_deallocate_vm_remap_ffi_ p:get_struct_offsetsraw_java_ctrep_Strarray_to_rawcyraw_closurejava_raw_closureiflosure_corevarmachdep_locsizeto_ptrarraycall؇_locraw_ptrarray_to_rawsizeto_ptrarraycallЍ؎_loclosure_allallocfreeSYSVtrampoline_table_pageramp_ype_is_supportedallocset_parmsget_addrfree_var_SYSV_innerVvoiduintsintpointerfloatdoublecomplex_81632648163264ЁȂfloatdouble4\`DHdDH, @X XK |e?@BCDEFGH;<=>?@ABCDEFGH _ffi_call_ffi_call_SYSV_ffi_closure_SYSV_ffi_closure_SYSV_V_ffi_closure_SYSV_inner_ffi_closure_alloc_ffi_closure_free_ffi_closure_trampoline_table_page_ffi_get_struct_offsets_ffi_java_ptrarray_to_raw_ffi_java_raw_call_ffi_java_raw_size_ffi_java_raw_to_ptrarray_ffi_prep_cif_ffi_prep_cif_core_ffi_prep_cif_machdep_ffi_prep_cif_machdep_var_ffi_prep_cif_var_ffi_prep_closure_ffi_prep_closure_loc_ffi_prep_java_raw_closure_ffi_prep_java_raw_closure_loc_ffi_prep_raw_closure_ffi_prep_raw_closure_loc_ffi_ptrarray_to_raw_ffi_raw_call_ffi_raw_size_ffi_raw_to_ptrarray_ffi_tramp_alloc_ffi_tramp_free_ffi_tramp_get_addr_ffi_tramp_is_supported_ffi_tramp_set_parms_ffi_type_complex_double_ffi_type_complex_float_ffi_type_double_ffi_type_float_ffi_type_pointer_ffi_type_sint16_ffi_type_sint32_ffi_type_sint64_ffi_type_sint8_ffi_type_uint16_ffi_type_uint32_ffi_type_uint64_ffi_type_uint8_ffi_type_void___chkstk_darwin___stack_chk_fail___stack_chk_guard_abort_calloc_free_mach_task_self__malloc_memcpy_pthread_mutex_lock_pthread_mutex_unlock_vm_allocate_vm_deallocate_vm_remap_initialize_aggregate_ffi_translate_args_ffi_java_translate_args_is_vfp_type_ffi_call_int_compress_hfa_type_is_hfa0_is_hfa1_ffi_elements_complex_float_ffi_elements_complex_double_ffi_trampoline_lock_ffi_trampoline_tables y$eq AX`  @libffi.8y NeuxJRNj-7C`q 34Vg}?AlXofkOX||zڽH,XofkOX||zڽH,XofkOX||zڽH, +dJKe颌5?VXofkOX||zڽH,XofkOX||zڽH,A 4btj8:xB>ܒl=lB>"l]pd 8ׅ y=S{#B>"l]pd 8ׅ y=S{#B>"l]pd 8ׅ y=S{#j`m xXXѤW2_$j`m xXXѤW2_$j`m xXXѤW2_$j`m xXXѤW2_$񏶌pNP@_I_MeGZ[XofkOX||zڽH,XofkOX||zڽH,XofkOX||zڽH,:Q"56"J,cP30XofkOX||zڽH,XofkOX||zڽH,XofkOX||zڽH,(PՖ];觐ՊխXofkOX||zڽH,XofkOX||zڽH,XofkOX||zڽH,`S