Hi all,
I have made some good progress reversing the Mac OS ROM in recent months, about which I will soon be posting. To get all that development done on a heterogeneous network, I have needed to put some tools in place. Since they make working on Mac OS 9 in this century so much easier, I thought I'd share.
This is how I set up my "MacSrc" Debian 8 VM to serve the same source tree to both my Mac OS 9 and Linux desktops.
Problem 1: case sensitivity
My source code sloppily expects a case insensitive filesystem like HFS, which Linux is reluctant to provide. Use vfat, Linux's long-filename FAT driver. Never mind that it has no permissions support -- just have it owned by your Netatalk guest user. (I use "mpw", UID/GID 1001.)
apt install dosfstools
# (Finding some actual storage space for your FS is an exercise
# left to the reader. I suggest gobbling an entire virtual disk.)
mkfs.vfat /dev/sdb
echo '/dev/sdb /MacSrc vfat uid=1001,gid=1001 0 0' >> /etc/fstab
mkdir /MacSrc && mount /MacSrc
Problem 2: Netatalk
With the release of Netatalk 3, version 2 has been deprecated. But Netatalk 2.1.6 is the last version that works correctly with MPW. Newer versions cause MPW to save files with incorrect names, which of course is unacceptable on a dev system. Attached to this post is a compiled source tree for that Netatalk version on Debian 8, ready to be installed straight away.
tar xf netatalk-2.1.6-built.tar.gz && cd netatalk-2.1.6 && make install
# Lines to change in /etc/default/netatalk
AFPD_UAMLIST="-U uams_guest.so"
AFPD_GUEST=mpw
ATALKD_RUN=no
PAPD_RUN=no
TIMELORD_RUN=no
A2BOOT_RUN=no
CNID_METAD_RUN=yes
AFPD_RUN=yes
# Append to /usr/local/etc/netatalk/AppleVolumes.default
# (deleting the existing "~" line)
/MacSrc MacSrc options:caseinsensitive
Problem 3: git
We use git to manage the CDG5 repository. While git can deal with any binary file, its build-in diff utility chokes on old-style Mac line endings. This Python FUSE module provides git with a sane view of your Mac source tree while trying very hard not to mangle binary files.
apt install fuse libfuse2 python3-pip
pip3 install fusepy
#!/usr/bin/env python3
# It is best to run this FUSE module as your "mpw" user, to avoid permissions problems.
from __future__ import with_statement
import os
import sys
import errno
from fuse import FUSE, FuseOSError, Operations
class FSCiv(Operations):
def __init__(self, root):
self.root = root
# Constants
# =========
MUNGE_ON = 1
MUNGE_PROTECT = 0
MUNGE_HIDE = -1
# Helpers
# =======
def _full_path(self, partial):
if partial.startswith("/"):
partial = partial[1:]
path = os.path.join(self.root, partial)
return path
def _munge_mode(self, partpath):
split = [x for x in partpath.split('/') if x]
if any(x in split for x in ['.git', 'BuildResults']):
return self.MUNGE_PROTECT
if split and any(split[-1].endswith(x) for x in ['.o','.lib','.sit']):
return self.MUNGE_PROTECT
if '.gitignore' in split:
return self.MUNGE_ON
if any(x.startswith('.') and x not in '..' for x in split):
return self.MUNGE_HIDE
if any(x in split for x in ['Network Trash Folder', 'Temporary Items', 'TheVolumeSettingsFolder']):
return self.MUNGE_HIDE
return self.MUNGE_ON
# Filesystem methods
# ==================
def access(self, path, mode):
munge_mode = self._munge_mode(path)
if munge_mode == self.MUNGE_HIDE:
raise FuseOSError(errno.EACCES)
full_path = self._full_path(path)
if not os.access(full_path, mode):
raise FuseOSError(errno.EACCES)
def chmod(self, path, mode):
full_path = self._full_path(path)
return os.chmod(full_path, mode)
def chown(self, path, uid, gid):
full_path = self._full_path(path)
return os.chown(full_path, uid, gid)
def getattr(self, path, fh=None):
full_path = self._full_path(path)
st = os.lstat(full_path)
return dict((key, getattr(st, key)) for key in ('st_atime', 'st_ctime',
'st_gid', 'st_mode', 'st_mtime', 'st_nlink', 'st_size', 'st_uid'))
def readdir(self, path, fh):
full_path = self._full_path(path)
yield from ['.', '..']
for list_name in os.listdir(full_path):
my_child_path = os.path.join(path, list_name)
if self._munge_mode(my_child_path) != self.MUNGE_HIDE:
yield list_name
def readlink(self, path):
pathname = os.readlink(self._full_path(path))
if pathname.startswith("/"):
# Path name is absolute, sanitize it.
return os.path.relpath(pathname, self.root)
else:
return pathname
def mknod(self, path, mode, dev):
return os.mknod(self._full_path(path), mode, dev)
def rmdir(self, path):
full_path = self._full_path(path)
return os.rmdir(full_path)
def mkdir(self, path, mode):
return os.mkdir(self._full_path(path), mode)
def statfs(self, path):
full_path = self._full_path(path)
stv = os.statvfs(full_path)
return dict((key, getattr(stv, key)) for key in ('f_bavail', 'f_bfree',
'f_blocks', 'f_bsize', 'f_favail', 'f_ffree', 'f_files', 'f_flag',
'f_frsize', 'f_namemax'))
def unlink(self, path):
return os.unlink(self._full_path(path))
def symlink(self, name, target):
return os.symlink(target, self._full_path(name))
def rename(self, old, new):
return os.rename(self._full_path(old), self._full_path(new))
def link(self, target, name):
return os.link(self._full_path(name), self._full_path(target))
def utimens(self, path, times=None):
return os.utime(self._full_path(path), times)
# File methods
# ============
def open(self, path, flags):
full_path = self._full_path(path)
return os.open(full_path, flags)
def create(self, path, mode, fi=None):
full_path = self._full_path(path)
return os.open(full_path, os.O_WRONLY | os.O_CREAT, mode)
def read(self, path, length, offset, fh):
os.lseek(fh, offset, os.SEEK_SET)
buf = os.read(fh, length)
if self._munge_mode(path) == self.MUNGE_ON:
buf = buf.replace(b'\r', b'\n')
return buf
def write(self, path, buf, offset, fh):
os.lseek(fh, offset, os.SEEK_SET)
if self._munge_mode(path) == self.MUNGE_ON:
buf = buf.replace(b'\n', b'\r')
return os.write(fh, buf)
def truncate(self, path, length, fh=None):
full_path = self._full_path(path)
with open(full_path, 'r+') as f:
f.truncate(length)
def flush(self, path, fh):
return os.fsync(fh)
def release(self, path, fh):
return os.close(fh)
def fsync(self, path, fdatasync, fh):
return self.flush(path, fh)
def main(mountpoint, root):
FUSE(FSCiv(root), mountpoint, nothreads=True, foreground=True)
if __name__ == '__main__':
main(sys.argv[2], sys.argv[1])