I like to do some retro programming, but SheepShaver, the best Mac emulator out there, has a bug that makes copy and paste not function, so is kind of hard to use. I was recently made aware that there is a tool named mpw (lowercase) that emulates just enough of classic MacOS to run Apple’s MPW compiler suite’s command line tools on MacOS X. So I thought I’d give it a try and set that up.

Installing MPW on OS X

  • Build the mpw tool according to the instructions in their Readme.md, basically:
    mkdir build
    cd build
    cmake ..
    make
    
  • Get a copy of MPW for classic MacOS (their Acquiring MPW page page lists some places where you can still get it if you haven’t kept your original E.T.O. or CodeWarrior CDs).
  • Create a folder ~/mpw.
  • Copy the files from the verbatim folder in their repository into it (so you have e.g. a ~/mpw/Environment.text file, not ~/mpw/verbatim/Environment.text).
  • Open the file Environment.text with a text editor and change the MPWVersion ?= 3.2 line to match whatever version your MPW Shell application shows if you choose Get Info on it in Finder.
  • Open the Interfaces&Libraries folder from your copy of MPW, and copy the Interfaces, Libraries, DebuggingLibraries and RuntimeLibraries folders inside it to your ~/mpw folder (so you have e.g. a ~/mpw/Interfaces/CIncludes folder).
  • open the MPW folder of your MPW distribution and copy the Tools folder from there to your ~/mpw folder (so you have e.g. a ~/mpw/Tools/AboutBox file).

Your MPW should now be ready.

Building the standard SillyBalls example that comes with MPW

Create a folder named mpw-sillyballs to hold your project (you can choose any name here, but that’s what I’ll call it going forward).

Obtain the C source code

Find the SillyBalls.c example. For MPW 3.5, it is at MPW/Examples/CExamples/SillyBalls.c. Copy it into a the mpw-sillyballs folder.

Creating a resource file

Every application on classic MacOS needs a few resources that describe the application and its UI. Those are usually defined in a resource file. You can create this resource file in a text description language named Rez. We’ll create a minimal resource file named SillyBalls.r that just tells the operating system about our application’s abilities:

#include <SysTypes.r>
#include <Types.r>

/* here is the quintessential MultiFinder friendliness device, the SIZE resource */

resource 'SIZE' (-1) {
	dontSaveScreen,
	acceptSuspendResumeEvents,
	enableOptionSwitch,
	canBackground,                  /* we can background; we don't currently, but our sleep value */
                                        /* guarantees we don't hog the Mac while we are in the background */
	multiFinderAware,               /* this says we do our own activate/deactivate; don't fake us out */
	backgroundAndForeground,        /* this is definitely not a background-only application! */
	dontGetFrontClicks,             /* change this if you want "do first click" behavior like the Finder */
	ignoreChildDiedEvents,          /* essentially, I'm not a debugger (sub-launching) */
	is32BitCompatible,              /* this app is safe to run in 32-bit address space */
	reserved,
	reserved,
	reserved,
	reserved,
	reserved,
	reserved,
	reserved,
	23 * 1024,        /* 23kb Preferred max. RAM */
	35 * 1024	  /* 35kb Minimal RAM limit */
};

Create the Makefile for building this app

# This should point to wherever you've built the 'mpw' tool from this repository:
MPW=~/Programming/mpw/build/bin/mpw

RINCLUDES=~/mpw/Interfaces/RIncludes

# 'SILB' is the unique "creator code" for this Silly Balls app, and used to associate icons
# with the app and its documents, and tell Finder to use this app to open a file. Make up
# your own unique 4-character code for your app here. All-lowercase codes are usually used
# by Apple, so use at least one uppercase letter.
LDFLAGS =-w -c 'SILB' -t APPL \
	-sn STDIO=Main -sn INTENV=Main -sn %A5Init=Main

PPC_LDFLAGS =-m main -w -c 'SILB' -t APPL

LIBRARIES={Libraries}Stubs.o \
	{Libraries}MacRuntime.o \
	{Libraries}IntEnv.o \
	{Libraries}Interface.o \
	{Libraries}ToolLibs.o \
	{CLibraries}StdCLib.o

PPC_LIBRARIES={SharedLibraries}InterfaceLib \
	{SharedLibraries}StdCLib \
	{PPCLibraries}StdCRuntime.o \
	{PPCLibraries}PPCCRuntime.o

TOOLBOXFLAGS=-d OLDROUTINENAMES=1 -typecheck relaxed

SOURCES=SillyBalls.c

OBJECTS=$(SOURCES:%.c=obj/%.68k.o)
PPC_OBJECTS=$(SOURCES:%.c=obj/%.ppc.o)

RFILES=SillyBalls.r
EXECUTABLE=SillyBalls

all: prepass bin/$(EXECUTABLE).ppc bin/$(EXECUTABLE).68k

prepass:
	mkdir -p obj bin

bin/$(EXECUTABLE).ppc: $(PPC_OBJECTS)
	$(MPW) PPCLink $(PPC_LDFLAGS) $(PPC_OBJECTS) $(PPC_LIBRARIES) -o $@; \
	Rez -rd $(RFILES) -o $@ -i $(RINCLUDES) -append

bin/$(EXECUTABLE).68k: $(OBJECTS)
	$(MPW) link $(LDFLAGS) $(OBJECTS) $(LIBRARIES) -o $@
	Rez -rd $(RFILES) -o $@ -i $(RINCLUDES) -append

obj/%.68k.o : %.c
	$(MPW) SC $(TOOLBOXFLAGS) $< -o $@

obj/%.ppc.o : %.c
	$(MPW) MrC $(TOOLBOXFLAGS) $< -o $@; \

clean:
	rm -rf bin obj

Building the app

Just type make all in your mpw-sillyballs directory. The Makefile will now create a bin/SillyBalls.68k and a bin/SillyBalls.ppc executable.

Integrating with CLion

Since we’re running on a modern OS, I thought I’d try if I could integrate the compiler with a modern IDE. I own CLion already and love its code navigation and refactoring features, so I thought I’d use CLion’s custom compiler support.

So as the CLion docs say, I created a compiler definition YAML file and used the little gear popup’s “Preferences…” menu item in the upper right of the CLion window to tell my project about this compiler definition file.

It gave the error “No compilation commands found.” One Stackoverflow post later, it was revealed that CLion’s Makefile parser is probably not smart enough to recognize $(MPW) SC as the same as mpw SC and the like. So I went and created a little shell script sc.sh to wrap the emulated compiler call as a single command:

#!/bin/zsh

# This script is needed so the CLion IDE will recognize the compiler and run it.

~/Programming/mpw/build/bin/mpw SC $@

And then updated the Makefile to call that instead of $(MPW) SC, and updated the YAML file to its final form:

compilers:
  - description: "MPW SC"
    match-sources: ".*\\.c"
    match-language: "C"
    match-compiler-exe: "(.*/)?sc.sh"
    code-insight-target-name: m68k
    include-dirs:
      - ${user-home}/mpw/Interfaces/CIncludes
    defines-text: "
#define __SC__ 0x0801
#define MPW_C 1
#define OLDROUTINENAMES 1
#define pascal
"

The important parts (besides using the shell script) here are:

  1. match-sources so it knows this compiler deals with .c files
  2. match-language tells CLion’s indexer (a version of clangd) what to parse these sources as.
  3. code-insight-target-name – I was the least sure about this one. m68k is the usual abbreviation for the Motorola 68000 series of CPUs of early Macs, but I couldn’t find a list of target platforms clangd actually supports.
  4. include-dirs tells the indexer where to find the system headers. This means that you can Command-click on WindowPtr in a source file and it will jump to its definition in MacWindows.h.
  5. defines-text tells CLion to pretend some constants had been defined by the compiler. Most compilers define some constants that let you detect which compiler it is. But clangd is based on the clang compiler, so it doesn’t define the right ones for our custom SC compiler, so we have to manually define those that would usually be defined here.
    Apple’s ConditionalMacros.h header looks at these constants to set up some more standardized constants no matter what compiler you’re using, so this is a good place to find out what constants you need and what values they should have.
    For example, if you were setting up the PowerPC compiler MrC, you’d probably have to define __MRC__ to 0x0700 instead. I just picked the highest value that header had for that constant. I should probably actually run this compiler in the emulator and see what the actual value of my version is. I also decided to define away pascal, because it is a keyword that clang otherwise displays an error on. Ideally we’d define this to an unusual calling convention so clangd could possibly error if we pass a Pascal function through a mis-matched function pointer with C calling conventions, but I didn’t yet get around to doing all that. Also, I tried to define TYPE_BOOL to 1 to keep ConditionalMacros.h from re-defining true and false, but it just hard-codes that value for each compiler, so I couldn’t find a way to override it just for CLion’s clangd indexer.

In any event, this made CLion happy, and not only was I now able to jump to system headers using CLion’s standard navigation, it also now let me use the little “Hammer” icon to build my app with make all, and the Build > Clean menu item to run make clean.

Now to set up things for the C++ compiler too, and then I can get coding.

PS - Some of the above description may look eerily like the `mpw` project's Wiki. That's because there was little documentation on how to initially set up the tool, and so I contributed an early draft of this article to their Wiki.