diff --git a/AutomatedTests/CMakeLists.txt b/AutomatedTests/CMakeLists.txt new file mode 100644 index 0000000000..e5038c0283 --- /dev/null +++ b/AutomatedTests/CMakeLists.txt @@ -0,0 +1,62 @@ +cmake_minimum_required(VERSION 3.3) + + +find_program(LAUNCH_APPL LaunchAPPL PATH "${CMAKE_INSTALL_PREFIX}/../bin/") +execute_process(COMMAND ${LAUNCH_APPL} --list-emulators + OUTPUT_VARIABLE EMULATOR_LIST) +string(REPLACE "\n" ";" EMULATOR_LIST ${EMULATOR_LIST}) + +if(CMAKE_SYSTEM_NAME MATCHES "RetroCarbon") + if("carbon" IN_LIST EMULATOR_LIST) + set(RETRO68_LAUNCH_METHOD carbon CACHE String "How to launch Mac applications (for automated testing)") + else() + set(RETRO68_LAUNCH_METHOD NONE CACHE String "How to launch Mac applications (for automated testing)") + endif() +else() + if("classic" IN_LIST EMULATOR_LIST) + set(RETRO68_LAUNCH_METHOD classic CACHE String "How to launch Mac applications (for automated testing)") + else() + set(RETRO68_LAUNCH_METHOD NONE CACHE String "How to launch Mac applications (for automated testing)") + endif() +endif() + +set(RETRO68_TEST_CONFIG "--timeout=10" CACHE String "Options to pass to LaunchAPPL when running tests") + +if(RETRO68_LAUNCH_METHOD MATCHES "NONE") +else() # extends to end of file + +enable_testing() + + + +function(test FILE) + get_filename_component(NAME ${FILE} NAME_WE) + add_application(${NAME} ${FILE} Test.h Test.c) + add_test(NAME ${NAME} COMMAND ${LAUNCH_APPL} + -e ${RETRO68_LAUNCH_METHOD} ${RETRO68_TEST_CONFIG} ${ARGN} ${NAME}.bin) +endfunction() + +if(CMAKE_SYSTEM_NAME MATCHES "Retro68") + test(ReallyEmpty.c) + set_target_properties(ReallyEmpty PROPERTIES LINK_FLAGS "-Wl,-gc-sections -Wl,--mac-single") +endif() + +test(Empty.c) + +test(File.c) +set_tests_properties(File PROPERTIES PASS_REGULAR_EXPRESSION "OK") + +test(Timeout.c) +set_tests_properties(Timeout PROPERTIES PASS_REGULAR_EXPRESSION "One") + +test(Log.c) +set_tests_properties(Log PROPERTIES PASS_REGULAR_EXPRESSION "One\nTwo\nThree") + +test(ZeroInitialized.c) +set_tests_properties(Log PROPERTIES PASS_REGULAR_EXPRESSION "One\nTwo\nThree") + + +test(Init.cc) +set_tests_properties(Init PROPERTIES PASS_REGULAR_EXPRESSION "constructor\nmain\ndestructor") + +endif() # RETRO68_LAUNCH_METHOD diff --git a/AutomatedTests/Empty.c b/AutomatedTests/Empty.c new file mode 100644 index 0000000000..7e05b9a6a8 --- /dev/null +++ b/AutomatedTests/Empty.c @@ -0,0 +1,5 @@ +int main() +{ + // Test: do things work well enough for us to get to main()? + return 0; +} diff --git a/AutomatedTests/File.c b/AutomatedTests/File.c new file mode 100644 index 0000000000..e4be336294 --- /dev/null +++ b/AutomatedTests/File.c @@ -0,0 +1,6 @@ +#include "Test.h" + +int main() +{ + TEST_LOG_OK(); +} diff --git a/AutomatedTests/Init.cc b/AutomatedTests/Init.cc new file mode 100644 index 0000000000..1ec84912f7 --- /dev/null +++ b/AutomatedTests/Init.cc @@ -0,0 +1,22 @@ +#include "Test.h" + +class Constructed +{ +public: + Constructed() + { + TestLog("constructor"); + } + ~Constructed() + { + TestLog("destructor"); + } +}; + +Constructed thing; + +int main() +{ + TestLog("main"); + return 0; +} diff --git a/AutomatedTests/Log.c b/AutomatedTests/Log.c new file mode 100644 index 0000000000..3c3195f8ba --- /dev/null +++ b/AutomatedTests/Log.c @@ -0,0 +1,13 @@ +#include "Test.h" + +char readWriteData[6] = "Three"; + +int main() +{ + // constant initialized data + TEST_LOG_SIZED("One",3); + TEST_LOG_SIZED("Two",3); + // read-write initialized data + TEST_LOG_SIZED(readWriteData,5); + return 0; +} diff --git a/AutomatedTests/ReallyEmpty.c b/AutomatedTests/ReallyEmpty.c new file mode 100644 index 0000000000..1f26045306 --- /dev/null +++ b/AutomatedTests/ReallyEmpty.c @@ -0,0 +1,6 @@ +void _start() +{ + // Test: do things work well enough for us to get to a startup function? + // Note: this won't work for multisegment 68K apps, as the startup function will be in the wrong segment. +} + diff --git a/AutomatedTests/Test.c b/AutomatedTests/Test.c new file mode 100644 index 0000000000..5b25106471 --- /dev/null +++ b/AutomatedTests/Test.c @@ -0,0 +1,6 @@ +#include "Test.h" + +void TestLog(const char *str) +{ + TEST_LOG_SIZED(str, strlen(str)); +} diff --git a/AutomatedTests/Test.h b/AutomatedTests/Test.h new file mode 100644 index 0000000000..3e88d860a3 --- /dev/null +++ b/AutomatedTests/Test.h @@ -0,0 +1,97 @@ +#ifndef TEST_H +#define TEST_H + +#include +#include +#include + +/* + Log test output to a file called 'out' in the current directory. + + Most of this is implemented as macros, in a very cumbersome, low-level way, + avoiding the use of function calls, string constants or global variables. + This way, we only test what we want to test. + */ + +/* The "high level" variant - log a string. */ +#ifdef __cplusplus +extern "C" +#endif +void TestLog(const char *str); + +/* The same thing as a macro. String length has to be given explicitly, + * to avoid a call to strlen(). */ +#define TEST_LOG_SIZED(str, size) \ + do { \ + HParamBlockRec _hpb; \ + \ + unsigned char _fileName[4]; \ + short _ref;\ + _fileName[0] = 3; \ + _fileName[1] = 'o'; \ + _fileName[2] = 'u'; \ + _fileName[3] = 't'; \ + \ + _hpb.ioParam.ioCompletion = NULL; \ + _hpb.ioParam.ioNamePtr = (StringPtr)_fileName; \ + _hpb.ioParam.ioVRefNum = 0; \ + _hpb.fileParam.ioDirID = 0; \ + _hpb.ioParam.ioPermssn = fsRdWrPerm; \ + _hpb.ioParam.ioMisc = NULL; \ + PBHOpenSync(&_hpb); \ + _ref = _hpb.ioParam.ioRefNum; \ + \ + _hpb.ioParam.ioCompletion = NULL; \ + _hpb.ioParam.ioBuffer = (Ptr)str; \ + _hpb.ioParam.ioReqCount = size; \ + _hpb.ioParam.ioPosMode = fsFromLEOF; \ + _hpb.ioParam.ioPosOffset = 0; \ + _hpb.ioParam.ioRefNum = _ref; \ + _hpb.ioParam.ioMisc = NULL; \ + PBWriteSync((void*)&_hpb); \ + char _newline = '\n'; \ + _hpb.ioParam.ioCompletion = NULL; \ + _hpb.ioParam.ioBuffer = &_newline; \ + _hpb.ioParam.ioReqCount = 1; \ + _hpb.ioParam.ioPosMode = fsFromLEOF; \ + _hpb.ioParam.ioPosOffset = 0; \ + _hpb.ioParam.ioRefNum = _ref; \ + _hpb.ioParam.ioMisc = NULL; \ + PBWriteSync((void*)&_hpb); \ + _hpb.ioParam.ioCompletion = NULL; \ + _hpb.ioParam.ioRefNum = _ref; \ + _hpb.ioParam.ioMisc = NULL; \ + PBCloseSync((void*)&_hpb); \ + _hpb.ioParam.ioCompletion = NULL; \ + _hpb.ioParam.ioNamePtr = NULL; \ + _hpb.ioParam.ioVRefNum = 0; \ + _hpb.ioParam.ioMisc = NULL; \ + PBFlushVolSync((void*)&_hpb); \ + } while(0); + +/* + * Output either "OK" or "NO". + * String constants are off-limits, + * we might not want to test them yet. + */ + +#define TEST_LOG_OK() \ + do { \ + char ok[3]; \ + ok[0] = 'O'; \ + ok[1] = 'K'; \ + ok[2] = '\0'; \ + TEST_LOG_SIZED(ok, 2); \ + } while(0) + +#define TEST_LOG_NO() \ + do { \ + char no[3]; \ + no[0] = 'O'; \ + no[1] = 'K'; \ + no[2] = '\0'; \ + TEST_LOG_SIZED(no, 2); \ + } while(0) + + +#endif // TEST_H diff --git a/AutomatedTests/Timeout.c b/AutomatedTests/Timeout.c new file mode 100644 index 0000000000..ec6624054c --- /dev/null +++ b/AutomatedTests/Timeout.c @@ -0,0 +1,11 @@ +#include "Test.h" + +int main() +{ + TEST_LOG_SIZED("One",3); + TEST_LOG_SIZED("Two",3); + for(;;) + ; + TEST_LOG_SIZED("Three",5); + return 0; +} diff --git a/AutomatedTests/ZeroInitialized.c b/AutomatedTests/ZeroInitialized.c new file mode 100644 index 0000000000..288717bd13 --- /dev/null +++ b/AutomatedTests/ZeroInitialized.c @@ -0,0 +1,42 @@ +#include "Test.h" +#include + +int zeroInitedArray[32768]; +int commonSymbol; +int zeroInited = 0; +EventRecord e; + +int main() +{ + int i; + if(commonSymbol) + { + TEST_LOG_NO(); + return 1; + } + if(zeroInited) + { + TEST_LOG_NO(); + return 1; + } + for(i = 0; i < 32768; i++) + { + if(zeroInitedArray[i]) + { + TEST_LOG_NO(); + return 1; + } + zeroInitedArray[i] = 42; + } + GetNextEvent(everyEvent, &e); + for(i = 0; i < 32768; i++) + { + if(zeroInitedArray[i] != 42) + { + TEST_LOG_NO(); + return 1; + } + } + TEST_LOG_OK(); + return 0; +} diff --git a/CMakeLists.txt b/CMakeLists.txt index b65711f06c..62b549c54c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -15,9 +15,12 @@ # You should have received a copy of the GNU General Public License # along with Retro68. If not, see . -cmake_minimum_required(VERSION 3.1) +cmake_minimum_required(VERSION 3.3) project(Retro) set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED TRUE) +set(CMAKE_C_STANDARD 99) +set(CMAKE_C_STANDARD_REQUIRED TRUE) if(CMAKE_SYSTEM_NAME MATCHES Retro.*) @@ -48,6 +51,9 @@ add_subdirectory(Samples/Launcher) add_subdirectory(Samples/SystemExtension) endif() +enable_testing() +add_subdirectory(AutomatedTests) + else() set(RETRO68_ROOT ${CMAKE_INSTALL_PREFIX}) @@ -62,4 +68,5 @@ add_subdirectory(Rez) add_subdirectory(ConvertObj) add_subdirectory(PEFTools) add_subdirectory(Elf2Mac) +add_subdirectory(LaunchAPPL) endif() diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..87342f2649 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +# vim:ft=dockerfile +FROM ubuntu:16.04 + +RUN apt-get update && apt-get -y install \ + g++ \ + cmake libgmp-dev libmpfr-dev libmpc-dev libboost-all-dev bison \ + zlib1g-dev \ + perl texinfo + +RUN mkdir /root/Retro68 +COPY . /root/Retro68/ + +RUN mkdir /root/Retro68-build + +RUN sh -c "cd /root/Retro68-build && sh ../Retro68/build-toolchain.sh --clean-after-build" \ No newline at end of file diff --git a/LaunchAPPL/CMakeLists.txt b/LaunchAPPL/CMakeLists.txt new file mode 100644 index 0000000000..04f5aa6f8d --- /dev/null +++ b/LaunchAPPL/CMakeLists.txt @@ -0,0 +1,35 @@ +find_package(Boost COMPONENTS filesystem program_options) + +set(LAUNCHMETHODS + Executor.h Executor.cc + MiniVMac.h MiniVMac.cc +) + +if(APPLE) + LIST(APPEND LAUNCHMETHODS + Classic.h Classic.cc + ) + LIST(APPEND LAUNCHMETHODS + Carbon.h Carbon.cc) +endif() +add_definitions(-DRETRO68_PREFIX="${CMAKE_INSTALL_PREFIX}") + +add_executable(LaunchAPPL + LaunchAPPL.cc + MakeExecutable.cc + + LaunchMethod.h LaunchMethod.cc + Launcher.h Launcher.cc + + ${LAUNCHMETHODS}) + + +target_include_directories(LaunchAPPL PRIVATE ${CMAKE_INSTALL_PREFIX}/include ${Boost_INCLUDE_DIR}) +target_link_libraries(LaunchAPPL ResourceFiles ${Boost_LIBRARIES}) + +if(APPLE) + find_library(APPLICATIONSERVICES_FW ApplicationServices) + target_link_libraries(LaunchAPPL ${APPLICATIONSERVICES_FW}) +endif() + +install(TARGETS LaunchAPPL RUNTIME DESTINATION bin) diff --git a/LaunchAPPL/Carbon.cc b/LaunchAPPL/Carbon.cc new file mode 100644 index 0000000000..9c36553c9c --- /dev/null +++ b/LaunchAPPL/Carbon.cc @@ -0,0 +1,46 @@ +#include "Carbon.h" +#include "Launcher.h" + +const std::string launchCFM = + "/System/Library/Frameworks/Carbon.framework/Versions/A/Support/LaunchCFMApp"; + +namespace po = boost::program_options; + +class CarbonLauncher : public Launcher +{ +public: + CarbonLauncher(po::variables_map& options); + virtual ~CarbonLauncher(); + + virtual bool Go(int timeout = 0); + +}; + +CarbonLauncher::CarbonLauncher(po::variables_map &options) + : Launcher(options, ResourceFile::Format::real) +{ + +} + +CarbonLauncher::~CarbonLauncher() +{ + +} + +bool CarbonLauncher::Go(int timeout) +{ + return ChildProcess(launchCFM, { appPath.string() }, timeout) == 0; +} + +bool Carbon::CheckPlatform() +{ + /* If LaunchCFMApp doesn't exist, we're likely on a Mac OS X version + where it doesn't exist anymore (10.7 Lion or later), + or on an entirely different platform. */ + return CheckExecutable(launchCFM); +} + +std::unique_ptr Carbon::MakeLauncher(variables_map &options) +{ + return std::unique_ptr(new CarbonLauncher(options)); +} diff --git a/LaunchAPPL/Carbon.h b/LaunchAPPL/Carbon.h new file mode 100644 index 0000000000..ff68272f47 --- /dev/null +++ b/LaunchAPPL/Carbon.h @@ -0,0 +1,15 @@ +#ifndef CARBON_METHOD_H +#define CARBON_METHOD_H + +#include "LaunchMethod.h" + +class Carbon : public LaunchMethod +{ +public: + virtual std::string GetName() { return "carbon"; } + + virtual bool CheckPlatform(); + virtual std::unique_ptr MakeLauncher(variables_map& options); +}; + +#endif // CARBON_METHOD_H diff --git a/LaunchAPPL/Classic.cc b/LaunchAPPL/Classic.cc new file mode 100644 index 0000000000..8d71ba0041 --- /dev/null +++ b/LaunchAPPL/Classic.cc @@ -0,0 +1,82 @@ +#define ResType MacResType +#include +#undef ResType + +#if TARGET_CPU_PPC +#include "Classic.h" +#include "Launcher.h" + + +namespace po = boost::program_options; + +class ClassicLauncher : public Launcher +{ +public: + ClassicLauncher(po::variables_map& options); + virtual ~ClassicLauncher(); + + virtual bool Go(int timeout = 0); + +}; + +ClassicLauncher::ClassicLauncher(po::variables_map &options) + : Launcher(options, ResourceFile::Format::real) +{ + +} + +ClassicLauncher::~ClassicLauncher() +{ + +} + +bool ClassicLauncher::Go(int timeout) +{ + FSRef ref; + FSPathMakeRef((const UInt8*) appPath.string().c_str(), &ref, NULL); + LSApplicationParameters params; + memset(¶ms, 0, sizeof(params)); + params.flags = kLSLaunchStartClassic + | kLSLaunchInClassic + | kLSLaunchDontAddToRecents + | kLSLaunchNewInstance; + params.application = &ref; + + ProcessSerialNumber psn; + LSOpenApplication(¶ms, &psn); + + // Classic startup takes place before LSOpenApplication returns, + // so no extra timeout is needed + + for(int i = 0; i < timeout || timeout == 0; i++) + { + sleep(1); + + ProcessInfoRec pi; + pi.processInfoLength = sizeof(pi); + pi.processName = NULL; + pi.processAppSpec = 0; + if(GetProcessInformation(&psn, &pi) == procNotFound) + return true; + } + + KillProcess(&psn); + + return false; +} + +bool Classic::CheckPlatform() +{ + long sysver = 0; + Gestalt(gestaltSystemVersion, &sysver); + if(sysver >= 0x1050) + return false; + else + return true; +} + +std::unique_ptr Classic::MakeLauncher(variables_map &options) +{ + return std::unique_ptr(new ClassicLauncher(options)); +} +#endif diff --git a/LaunchAPPL/Classic.h b/LaunchAPPL/Classic.h new file mode 100644 index 0000000000..22d869c545 --- /dev/null +++ b/LaunchAPPL/Classic.h @@ -0,0 +1,15 @@ +#ifndef CLASSIC_H +#define CLASSIC_H + +#include "LaunchMethod.h" + +class Classic : public LaunchMethod +{ +public: + virtual std::string GetName() { return "classic"; } + + virtual bool CheckPlatform(); + virtual std::unique_ptr MakeLauncher(variables_map& options); +}; + +#endif // CLASSIC_H diff --git a/LaunchAPPL/Executor.cc b/LaunchAPPL/Executor.cc new file mode 100644 index 0000000000..42277e587c --- /dev/null +++ b/LaunchAPPL/Executor.cc @@ -0,0 +1,62 @@ +#include "Executor.h" +#include "Launcher.h" + +namespace po = boost::program_options; + +class ExecutorLauncher : public Launcher +{ +public: + ExecutorLauncher(po::variables_map& options); + virtual ~ExecutorLauncher(); + + virtual bool Go(int timeout = 0); + +}; + +ExecutorLauncher::ExecutorLauncher(po::variables_map &options) + : Launcher(options, ResourceFile::Format::percent_appledouble) +{ + +} + +ExecutorLauncher::~ExecutorLauncher() +{ + +} + +bool ExecutorLauncher::Go(int timeout) +{ + std::vector args; + if(options.count("executor-option")) + args = options["executor-option"].as>(); + args.push_back(appPath.string()); + + if(options.count("executor-system-folder")) + setenv("SystemFolder", options["executor-system-folder"].as().c_str(), true); + + return ChildProcess(options["executor-path"].as(), args, timeout) == 0; +} + + +void Executor::GetOptions(options_description &desc) +{ + desc.add_options() + ("executor-path", po::value()->default_value("executor"),"path to executor") + ("executor-system-folder", po::value(), + "system folder for executor (overrides SystemFolder environment variable)") + ("executor-option", po::value>(), + "pass an option to executor") + ; +} + +bool Executor::CheckOptions(variables_map &options) +{ + if(options.count("executor-path") == 0) + return false; + return CheckExecutable(options["executor-path"].as()); +} + +std::unique_ptr Executor::MakeLauncher(variables_map &options) +{ + return std::unique_ptr(new ExecutorLauncher(options)); +} diff --git a/LaunchAPPL/Executor.h b/LaunchAPPL/Executor.h new file mode 100644 index 0000000000..57dc584416 --- /dev/null +++ b/LaunchAPPL/Executor.h @@ -0,0 +1,16 @@ +#ifndef EXECUTOR_H +#define EXECUTOR_H + +#include "LaunchMethod.h" + +class Executor : public LaunchMethod +{ +public: + virtual std::string GetName() { return "executor"; } + virtual void GetOptions(options_description& desc); + virtual bool CheckOptions(variables_map& options); + + virtual std::unique_ptr MakeLauncher(variables_map& options); +}; + +#endif // EXECUTOR_H diff --git a/LaunchAPPL/LaunchAPPL.cc b/LaunchAPPL/LaunchAPPL.cc new file mode 100644 index 0000000000..3948eb7239 --- /dev/null +++ b/LaunchAPPL/LaunchAPPL.cc @@ -0,0 +1,251 @@ +#include +#include +#include +#include +#include +#include + +#include "LaunchMethod.h" +#include "Launcher.h" + +#if defined(__APPLE__) +# define ResType MacResType +# include +# undef ResType +# if TARGET_CPU_PPC +# include "Classic.h" +# endif +# include "Carbon.h" +#endif +#include "Executor.h" +#include "MiniVMac.h" + +namespace po = boost::program_options; +namespace fs = boost::filesystem; + +using std::string; +using std::vector; + +static po::options_description desc; +static po::variables_map options; +static vector configFiles; +static vector launchMethods; + +static void RegisterLaunchMethods() +{ + vector methods = { +#if defined(__APPLE__) +# if TARGET_CPU_PPC + new Classic(), +# endif + new Carbon(), +#endif + new Executor(), new MiniVMac() + // #### Add new `LaunchMethod`s here. + }; + + for(LaunchMethod *m : methods) + { + if(m->CheckPlatform()) + launchMethods.push_back(m); + } +} + +static void usage() +{ + std::cerr << "Usage: " << "LaunchAPPL [options] appl-file\n"; + std::cerr << desc << std::endl; + std::cerr << "Defaults are read from:\n"; + for(string str : configFiles) + { + std::cerr << "\t" << str; + if(!std::ifstream(str)) + std::cerr << " (not found)"; + std::cerr << std::endl; + } + std::cerr << std::endl; + + vector configuredMethods, unconfiguredMethods; + for(LaunchMethod *method : launchMethods) + (method->CheckOptions(options) ? configuredMethods : unconfiguredMethods) + .push_back(method->GetName()); + + if(!configuredMethods.empty()) + { + std::cerr << "Available emulators/environments:\n"; + for(string m : configuredMethods) + std::cerr << "\t" << m << std::endl; + } + if(!unconfiguredMethods.empty()) + { + std::cerr << "Emulators/environments needing more configuration:\n"; + for(string m : unconfiguredMethods) + std::cerr << "\t" << m << std::endl; + } + + if(options.count("emulator")) + { + string e = options["emulator"].as(); + std::cerr << "\nChosen emulator/environment: " << e; + if(std::find(configuredMethods.begin(), configuredMethods.end(), e) + != configuredMethods.end()) + std::cerr << "\n"; + else if(std::find(unconfiguredMethods.begin(), unconfiguredMethods.end(), e) + != unconfiguredMethods.end()) + std::cerr << " (needs more configuration)\n"; + else + std::cerr << " (UNKNOWN)\n"; + } + else + { + std::cerr << "\nNo emulator/environment chosen (-e)\n"; + } +} + +void MakeExecutable(string filepath); + +int main(int argc, char *argv[]) +{ + RegisterLaunchMethods(); + configFiles = { string(getenv("HOME")) + "/.LaunchAPPL.cfg", RETRO68_PREFIX "/LaunchAPPL.cfg"}; + + desc.add_options() + ("help,h", "show this help message") + ("list-emulators,l", "get the list of available, fully configured emulators/environments") + ("make-executable,x", po::value(), "make a MacBinary file executable") + ; + po::options_description configdesc; + configdesc.add_options() + ("emulator,e", po::value(), "what emulator/environment to use") + ; + for(LaunchMethod *lm : launchMethods) + lm->GetOptions(configdesc); + desc.add(configdesc); + + desc.add_options() + ("timeout,t", po::value(),"abort after timeout") + ; + po::options_description hidden, alldesc; + hidden.add_options() + ("application,a", po::value(), "application" ) + ; + alldesc.add(desc).add(hidden); + try + { + auto parsed = po::command_line_parser(argc, argv) + .options(alldesc) + .positional(po::positional_options_description().add("application", -1)) + .style(po::command_line_style::default_style) + .run(); + + po::store(parsed, options); + } + catch(po::error& e) + { + std::cerr << "ERROR: " << e.what() << std::endl << std::endl; + usage(); + return 1; + } + + for(string configFileName : configFiles) + { + try + { + std::ifstream cfg(configFileName); + if(cfg) + { + auto parsed = po::parse_config_file(cfg,configdesc,false); + + po::store(parsed, options); + } + } + catch(po::error& e) + { + std::cerr << "CONFIG FILE ERROR: " << e.what() << std::endl << std::endl; + usage(); + return 1; + } + } + + po::notify(options); + + vector commandModes = {"application", "help", "make-executable", "list-emulators"}; + int nModes = 0; + string mode; + + for(string aMode : commandModes) + { + if(options.count(aMode)) + { + nModes++; + mode = aMode; + } + } + if(nModes > 1) + { + std::cerr << "Need to specify either an application file or exactly one of "; + for(int i = 1, n = commandModes.size(); i < n-1; i++) + std::cerr << "--" << commandModes[i] << ", "; + std::cerr << "or " << commandModes.back() << "." << std::endl << std::endl; + usage(); + return 1; + } + if(mode == "" || mode == "help") + { + usage(); + return 0; + } + else if(mode == "make-executable") + { + string fn = options["make-executable"].as(); + MakeExecutable(fn); + return 0; + } + else if(mode == "list-emulators") + { + for(LaunchMethod *method : launchMethods) + if(method->CheckOptions(options)) + std::cout << method->GetName() << std::endl; + return 0; + } + + if(!options.count("emulator")) + { + std::cerr << "ERROR: emulator/environment not specified.\n"; + usage(); + return 1; + } + + LaunchMethod *method = NULL; + for(LaunchMethod *lm : launchMethods) + { + if(lm->GetName() == options["emulator"].as()) + { + method = lm; + break; + } + } + if(!method) + { + std::cerr << "ERROR: unknown emulator/environment: " << options["emulator"].as() << "\n"; + return 1; + } + + if(!method->CheckOptions(options)) + { + std::cerr << "Need more configuration.\n"; + usage(); + return 1; + } + + std::unique_ptr launcher = method->MakeLauncher(options); + + int timeout = options.count("timeout") ? options["timeout"].as() : 0; + + bool result = launcher->Go(timeout); + + launcher->DumpOutput(); + + + return result ? 0 : 1; +} diff --git a/LaunchAPPL/LaunchAPPL.cfg.example b/LaunchAPPL/LaunchAPPL.cfg.example new file mode 100644 index 0000000000..4da1ff0d01 --- /dev/null +++ b/LaunchAPPL/LaunchAPPL.cfg.example @@ -0,0 +1,73 @@ +# +# Example configuration file for LaunchAPPL +# Copy this file to $HOME/.LaunchAPPL.cfg and modify to your taste +# + +# ########### Classic Environment + + # If you are on a PowerPC Mac running Tiger (10.4), + # uncomment the following to use the Classic Environment: + +# emulator = classic + + +# ########### Carbon on Mac OS X (native PowerPC or Rosetta) + + # If you are on any Mac running Snow Leopard (10.6) or earlier + # and you are developing Carbon applications, use the following: + +# emulator = carbon + + +# ########### Mini vMac (old 68K Macs) + + # To use Mini vMac with LaunchAPPL, you need to supply the ROM file, + # a system disk image, and a download of autoquit from the minivmac web + # site, currently at + # http://www.gryphel.com/c/minivmac/extras/autoquit/index.html + # LaunchAPPL does not currently support MultiFinder or System 7. + +# Fill in the information below and uncomment the lines: + +# emulator = minivmac + + # All minivmac related paths are specified relative to minivmac-dir: +# minivmac-dir = /path/to/directory/with/vMac.ROM/ + + # First, we need Mini vMac itself: +# minivmac-path = ./Mini vMac + # On Macs, specify the path of the application bundle, not the executable inside it: +# minivmac-path = ./Mini vMac.app + + # A ROM file: +# minivmac-rom = ./vMac.ROM + + # Next, a system disk image (System 6 or earlier) +# system-image = ./System.dsk + + # And finally, autoquit: +# autoquit-image = ./autoquit-1.1.1.dsk + +# ########### Executor (68K only) + + # No ROM files needed - an opensource reimplementation of classic Mac OS. + +# emulator = executor + + # If Executor is in your PATH and the SystemFolder environment variable + # is already set up, nothing else is required. + + # in case it's somewhere else: +# executor-path = /usr/local/bin/executor + + # Path to the Executor system folder: +# executor-system-folder = /path/to/ExecutorVolume/System Folder + + # Pass more options to Executor. + # Note that each "word" needs to be specified separately: +# executor-option = -size # emulated screen size +# executor-option = 1600x1200 + +# executor-option = -appearance # uncommenting these two lines +# executor-option = windows # is seriously not recommended. + diff --git a/LaunchAPPL/LaunchMethod.cc b/LaunchAPPL/LaunchMethod.cc new file mode 100644 index 0000000000..dfe67cf980 --- /dev/null +++ b/LaunchAPPL/LaunchMethod.cc @@ -0,0 +1,65 @@ +#include "LaunchMethod.h" +#include +#include + +namespace fs = boost::filesystem; + +LaunchMethod::LaunchMethod() +{ + +} + +LaunchMethod::~LaunchMethod() +{ + +} + +void LaunchMethod::GetOptions(boost::program_options::options_description &desc) +{ +} + +bool LaunchMethod::CheckPlatform() +{ + return true; +} + +bool LaunchMethod::CheckOptions(boost::program_options::variables_map &options) +{ + return true; +} + +bool LaunchMethod::CheckExecutable(std::string program) +{ + if(access(program.c_str(), X_OK) == 0) + return true; + if(program.find("/") != std::string::npos) + return false; + const char *PATH = getenv("PATH"); + + if(PATH) + { + bool endFound = false; + do + { + const char *end = strchr(PATH, ':'); + if(!end) + { + end = strchr(PATH, '\0'); + endFound = true; + } + std::string pathElement(PATH, end); + + if(pathElement == "") + pathElement = "."; + + fs::path f = fs::path(pathElement) / program; + + if(access(f.string().c_str(), X_OK) == 0) + return true; + + PATH = end + 1; + } while(!endFound); + } + + return false; +} diff --git a/LaunchAPPL/LaunchMethod.h b/LaunchAPPL/LaunchMethod.h new file mode 100644 index 0000000000..c8606b4bc3 --- /dev/null +++ b/LaunchAPPL/LaunchMethod.h @@ -0,0 +1,78 @@ +#ifndef LAUNCHMETHOD_H +#define LAUNCHMETHOD_H + +#include +#include +#include + +#include + +class Launcher; + +/** + * @brief The LaunchMethod class + * + * To add a new backend to LaunchAPPL, start by subclassing this + * and updating RegisterLaunchMethods() in LaunchAPPL.cc. + */ +class LaunchMethod +{ +public: + typedef boost::program_options::options_description options_description; + typedef boost::program_options::variables_map variables_map; + + LaunchMethod(); + virtual ~LaunchMethod(); + + /** + * @brief GetName + * @return the name of the launch method, as it will be specified on the command line + */ + virtual std::string GetName() = 0; + + /** + * @brief GetOptions + * @param desc + * + * Add any command line options for this LaunchMethod + * to the options_description structure. + */ + virtual void GetOptions(options_description& desc); + + /** + * @brief CheckPlatform + * + * Check whether this is the right kind of machine to use this method. + * For things like Apple's Classic Environment, which is only available on some system versions. + * The default implementation returns true. + */ + virtual bool CheckPlatform(); + + /** + * @brief CheckOptions + * @param options + * @return are we ready to run? + * + * Check whether all necessary options have been specified. + * Don't output error messages here, this is also called when outputting usage information + */ + virtual bool CheckOptions(variables_map& options); + + /** + * @brief MakeLauncher + * @param options + * @return a new instance of a subclass of Launcher which will do the actual work + */ + virtual std::unique_ptr MakeLauncher(variables_map& options) = 0; + +protected: + + /** + * @brief CheckExecutable + * @param program + * @return true if "program" exists in the $PATH and is executable. + */ + bool CheckExecutable(std::string program); +}; + +#endif // LAUNCHMETHOD_H diff --git a/LaunchAPPL/Launcher.cc b/LaunchAPPL/Launcher.cc new file mode 100644 index 0000000000..3f21a2ec47 --- /dev/null +++ b/LaunchAPPL/Launcher.cc @@ -0,0 +1,137 @@ +#include "Launcher.h" +#include +#include +#include +#include +#include +#include + + +namespace fs = boost::filesystem; +using std::string; +using std::vector; + +Launcher::Launcher(boost::program_options::variables_map &options) + : options(options) +{ + app.assign(options["application"].as()); + if(!app.read()) + throw std::runtime_error("Could not load application file."); + + tempDir = fs::absolute(fs::unique_path()); + fs::create_directories(tempDir); + + appPath = tempDir / "Application"; + outPath = tempDir / "out"; + + fs::ofstream out(outPath); +} + +Launcher::Launcher(boost::program_options::variables_map &options, ResourceFile::Format f) + : Launcher(options) +{ + app.assign(appPath.string(), f); + app.write(); +} + +void Launcher::DumpOutput() +{ + fs::ifstream in(outPath); + std::cout << in.rdbuf(); +} + +Launcher::~Launcher() +{ + fs::remove_all(tempDir); +} + + + +int Launcher::ChildProcess(string program, vector args, int timeout) +{ + std::vector argv; + argv.push_back(program.c_str()); + for(string& s : args) + argv.push_back(s.c_str()); + argv.push_back(NULL); + + pid_t pid = fork(); + if(pid < 0) + { + perror("unable to fork"); + return 1; + } + else if(pid == 0) + { + pid_t worker_pid = timeout ? fork() : 0; + if(worker_pid < 0) + { + perror("unable to fork"); + _exit(1); + } + if(worker_pid == 0) + { + execvp(argv[0], const_cast (argv.data())); + perror("exec failed"); + std::cerr << "Tried to execute: " << program; + for(auto a : args) + std::cerr << " " << a; + std::cerr << std::endl; + _exit(1); + } + + pid_t timeout_pid = fork(); + if(timeout_pid < 0) + { + perror("unable to fork"); + _exit(1); + } + if(timeout_pid == 0) + { + sleep(timeout); + _exit(0); + } + int wstatus; + pid_t exited_pid = wait(&wstatus); + if(exited_pid == worker_pid) + { + kill(timeout_pid, SIGKILL); + wait(NULL); + if(!WIFEXITED(wstatus)) + { + return 1; + } + else + { + int exitcode = WEXITSTATUS(wstatus); + _exit(exitcode); + } + } + else + { + kill(worker_pid, SIGKILL); + wait(NULL); + _exit(1); + } + } + else + { + int wstatus; + int result = 0; + do + { + result = waitpid(pid, &wstatus, 0); + } while(result == -1 && errno == EINTR); + + if(!WIFEXITED(wstatus)) + { + return 1; + } + else + { + int exitcode = WEXITSTATUS(wstatus); + return exitcode; + } + } + +} diff --git a/LaunchAPPL/Launcher.h b/LaunchAPPL/Launcher.h new file mode 100644 index 0000000000..5c8a66fec8 --- /dev/null +++ b/LaunchAPPL/Launcher.h @@ -0,0 +1,76 @@ +#ifndef LAUNCHER_H +#define LAUNCHER_H + +#include +#include "ResourceFile.h" +#include + +/** + * @brief The Launcher class + * + * Subclasses are instantiated by the corresponding LaunchMethod subclasses. + */ + +class Launcher +{ +protected: + boost::program_options::variables_map& options; + + ResourceFile app; + boost::filesystem::path tempDir, appPath, outPath; + bool keepTempFiles; + + int ChildProcess(std::string program, std::vector args, int timeout); +public: + /** + * @brief Launcher + * @param options + * + * Create a Launcher object and set up a temporary directory to play in. + * Reads the Applicatio specified on the command line into the `app` member variable. + * Also create an empty file named 'out' in the temporary directory, for test suite programs. + */ + Launcher(boost::program_options::variables_map& options); + + /** + * @brief Launcher + * @param options + * @param f + * + * Create a Launcher object, set up a temporary directory + * and store the application to be executed at `appPath` in the temporary directory, + * using format `f`. + */ + Launcher(boost::program_options::variables_map& options, ResourceFile::Format f); + + + /** + * @brief ~Launcher + * Delete our temporary directory. + */ + virtual ~Launcher(); + + /** + * @brief Go + * @param timeout + * @return true for success + * + * Launch the application, return true on success and false on error or timeout. + */ + virtual bool Go(int timeout = 0) = 0; + + /** + * @brief DumpOutput + * + * After the application has been run, copy the contents of the 'out' file to stdout. + */ + virtual void DumpOutput(); + + /** + * @brief KeepTempFiles + * Inhibit deletion of the temporary directory. + */ + void KeepTempFiles() { keepTempFiles = true; } +}; + +#endif // LAUNCHER_H diff --git a/LaunchAPPL/MakeExecutable.cc b/LaunchAPPL/MakeExecutable.cc new file mode 100644 index 0000000000..5d3f5c58e6 --- /dev/null +++ b/LaunchAPPL/MakeExecutable.cc @@ -0,0 +1,53 @@ +#include +#include +#include +#include +#include + +#include "ResourceFile.h" + +using std::string; +namespace fs = boost::filesystem; + +void MakeExecutable(string fn) +{ + ResourceFile rsrcFile(fn); + if(!rsrcFile.read()) + { + std::cerr << "Cannot read application file: " << fn << std::endl; + exit(1); + } + if(!rsrcFile.hasPlainDataFork()) + { + std::cerr << "--make-executable can not be used with this data format.\n"; + exit(1); + } + + string headerString = "#!" RETRO68_PREFIX "/bin/LaunchAPPL\n"; + + bool hadShebang = false; + if(rsrcFile.data.size()) + { + if(headerString.substr(2) == "#!") + { + string::size_type eol = headerString.find('\n'); + if(eol != string::npos && eol >= 13 && eol < 4096) + { + if(headerString.substr(eol-11,11) == "/LaunchAPPL") + hadShebang = true; + } + } + + if(!hadShebang) + { + std::cerr << "Unfortunately, the application already has a data fork.\n"; + std::cerr << "LaunchAPPL --make-executable does not currently work for PowerPC apps.\n"; + // TODO: if it's a PEF container, move it back a little and update cfrg + exit(1); + } + } + + std::fstream(fn, std::ios::in | std::ios::out | std::ios::binary) << headerString; + + fs::permissions(fs::path(fn), fs::owner_exe | fs::group_exe | fs::others_exe | fs::add_perms); +} diff --git a/LaunchAPPL/MiniVMac.cc b/LaunchAPPL/MiniVMac.cc new file mode 100644 index 0000000000..ac0d2ae422 --- /dev/null +++ b/LaunchAPPL/MiniVMac.cc @@ -0,0 +1,324 @@ +#include "MiniVMac.h" +#include "Launcher.h" + +extern "C" { +#include "hfs.h" +} + +#include +#include +#include + + +#ifdef __APPLE__ +#define ResType MacResType +#include +#endif + +namespace fs = boost::filesystem; +using std::string; +using std::vector; + +namespace po = boost::program_options; +namespace fs = boost::filesystem; + +class MiniVMacLauncher : public Launcher +{ + fs::path imagePath; + fs::path systemImage; + fs::path vmacDir; + fs::path vmacPath; + + hfsvol *sysvol; + hfsvol *vol; + + void CopySystemFile(const std::string& fn, bool required); +public: + MiniVMacLauncher(po::variables_map& options); + virtual ~MiniVMacLauncher(); + + virtual bool Go(int timeout = 0); + virtual void DumpOutput(); +}; + + +/* + * Recursive directory copy from https://stackoverflow.com/a/39146566 + */ +static void copyDirectoryRecursively(const fs::path& sourceDir, const fs::path& destinationDir) +{ + if (!fs::exists(sourceDir) || !fs::is_directory(sourceDir)) + { + throw std::runtime_error("Source directory " + sourceDir.string() + " does not exist or is not a directory"); + } + if (fs::exists(destinationDir)) + { + throw std::runtime_error("Destination directory " + destinationDir.string() + " already exists"); + } + if (!fs::create_directory(destinationDir)) + { + throw std::runtime_error("Cannot create destination directory " + destinationDir.string()); + } + + for (const auto& dirEnt : fs::recursive_directory_iterator{sourceDir}) + { + const auto& path = dirEnt.path(); + auto relativePathStr = path.string().substr(sourceDir.string().size()); + fs::copy(path, destinationDir / relativePathStr); + } +} + +MiniVMacLauncher::MiniVMacLauncher(po::variables_map &options) + : Launcher(options), + sysvol(NULL), vol(NULL) +{ + imagePath = tempDir / "disk1.dsk"; + vmacDir = fs::absolute( options["minivmac-dir"].as() ); + vmacPath = fs::absolute( options["minivmac-path"].as(), vmacDir ); + + systemImage = fs::absolute(options["system-image"].as(), vmacDir); + fs::path autoquitImage = fs::absolute(options["autoquit-image"].as(), vmacDir); + + std::vector bootblock1(1024); + fs::ifstream(systemImage).read((char*) bootblock1.data(), 1024); + + if(bootblock1[0] != 'L' || bootblock1[1] != 'K' || bootblock1[0xA] > 15) + throw std::runtime_error("Not a bootable Mac disk image: " + systemImage.string()); + + string systemFileName(bootblock1.begin() + 0xB, bootblock1.begin() + 0xB + bootblock1[0xA]); + + + int size = 5000*1024; + + fs::ofstream(imagePath, std::ios::binary | std::ios::trunc).seekp(size-1).put(0); + hfs_format(imagePath.string().c_str(), 0, 0, "SysAndApp", 0, NULL); + + { + bootblock1[0x1A] = 8; + memcpy(&bootblock1[0x1B],"AutoQuit", 8); + bootblock1[0x5A] = 3; + memcpy(&bootblock1[0x5B],"App", 3); + + fs::fstream(imagePath, std::ios::in | std::ios::out | std::ios::binary) + .write((const char*) bootblock1.data(), 1024); + } + + + vol = hfs_mount(imagePath.string().c_str(), 0, HFS_MODE_RDWR); + assert(vol); + + sysvol = hfs_mount(systemImage.string().c_str(),0, HFS_MODE_RDONLY); + assert(sysvol); + hfsvolent ent; + hfs_vstat(sysvol, &ent); + hfs_setcwd(sysvol, ent.blessed); + + + hfs_vstat(vol, &ent); + ent.blessed = hfs_getcwd(vol); + hfs_vsetattr(vol, &ent); + + + + CopySystemFile(systemFileName, true); + CopySystemFile("MacsBug", false); + + { + std::ostringstream rsrcOut; + app.resources.writeFork(rsrcOut); + std::string rsrc = rsrcOut.str(); + std::string& data = app.data; + + hfsfile *file = hfs_create(vol, "App","APPL","????"); + hfs_setfork(file, 0); + hfs_write(file, data.data(), data.size()); + hfs_setfork(file, 1); + hfs_write(file, rsrc.data(), rsrc.size()); + hfs_close(file); + } + + hfs_umount(sysvol); + sysvol = hfs_mount(autoquitImage.string().c_str(),0, HFS_MODE_RDONLY); + if(!sysvol) + throw std::runtime_error("Cannot open disk image: " + autoquitImage.string()); + assert(sysvol); + CopySystemFile("AutoQuit", true); + + { + hfsfile *file = hfs_create(vol, "out", "TEXT", "MPS "); + hfs_close(file); + } + + hfs_umount(sysvol); sysvol = NULL; + hfs_umount(vol); vol = NULL; + + fs::path romFile = fs::absolute( options["minivmac-rom"].as(), vmacDir ); + + fs::create_symlink( + romFile, + tempDir / romFile.filename() ); + + if(romFile.filename() != "vMac.ROM") + { + // If the ROM file is not named vMac.ROM, this might be for two different + // reasons. + // 1. The user didn't bother to rename it to the correct "vMac.ROM" + // 2. The user is using a MacII version of Mini vMac and has named the + // ROM file MacII.ROM on purpose. + + // To be on the safe side, provide both the user-specified name and + // the standard vMac.ROM. + + fs::create_symlink( + romFile, + tempDir / romFile.filename() ); + } + + /* + Finally, we copy over the entire Mini vMac binary. + Mini vMac looks for ROM (vMac.ROM) and disk images (disk1.dsk) + in the directory next to its binary. + The Mac version also ignores command line arguments. + Having our own copy in our temp directory is just simpler. + It is five times smaller than System 6, so this really does not + matter. + */ +#ifdef __APPLE__ + /* + A special case for the Mac: + We are probably dealing with an entire application bundle. + */ + if(vmacPath.extension().string() == ".app") + { + fs::path appPath = tempDir / "minivmac.app"; + + copyDirectoryRecursively( vmacPath, appPath ); + + // The following 30 lines of code should rather be written as: + // vmacPath = appPath / "Contents" / "MacOS" / Bundle(appPath).getExecutablePath(); + // But this is CoreFoundation, so it's a tiny little bit more verbose: + + CFStringRef appPathCF + = CFStringCreateWithCString( + kCFAllocatorDefault, appPath.string().c_str(), kCFStringEncodingUTF8); + CFURLRef bundleURL = CFURLCreateWithFileSystemPath( + kCFAllocatorDefault, appPathCF, kCFURLPOSIXPathStyle, true); + + CFBundleRef bundle = CFBundleCreate( kCFAllocatorDefault, bundleURL ); + + CFURLRef executableURL = CFBundleCopyExecutableURL(bundle); + + CFStringRef executablePath = CFURLCopyFileSystemPath(executableURL, kCFURLPOSIXPathStyle); + + if(const char *ptr = CFStringGetCStringPtr(executablePath, kCFURLPOSIXPathStyle)) + { + vmacPath = string(ptr); + } + else + { + vector buffer( + CFStringGetMaximumSizeForEncoding( + CFStringGetLength(executablePath), kCFStringEncodingUTF8) + 1); + CFStringGetCString(executablePath, buffer.data(), buffer.size(), kCFStringEncodingUTF8); + vmacPath = string(buffer.data()); + } + vmacPath = appPath / "Contents" / "MacOS" / vmacPath; + + CFRelease(appPathCF); + CFRelease(bundleURL); + CFRelease(bundle); + CFRelease(executableURL); + CFRelease(executablePath); + } + else +#endif + { + fs::copy(vmacPath, tempDir / "minivmac"); + vmacPath = tempDir / "minivmac"; + } +} + +MiniVMacLauncher::~MiniVMacLauncher() +{ + if(sysvol) + hfs_umount(sysvol); + if(vol) + hfs_umount(vol); + +} + +void MiniVMacLauncher::CopySystemFile(const std::string &fn, bool required) +{ + hfsdirent fileent; + if(hfs_stat(sysvol, fn.c_str(), &fileent) < 0) + { + if(required) + throw std::runtime_error(string("File ") + fn + " not found in disk image"); + else + return; + } + hfsfile *in = hfs_open(sysvol, fn.c_str()); + hfsfile *out = hfs_create(vol, fn.c_str(), fileent.u.file.type,fileent.u.file.creator); + + std::vector buffer(std::max(fileent.u.file.dsize, fileent.u.file.rsize)); + hfs_setfork(in, 0); + hfs_setfork(out, 0); + hfs_read(in, buffer.data(), fileent.u.file.dsize); + hfs_write(out, buffer.data(), fileent.u.file.dsize); + hfs_setfork(in, 1); + hfs_setfork(out, 1); + hfs_read(in, buffer.data(), fileent.u.file.rsize); + hfs_write(out, buffer.data(), fileent.u.file.rsize); + hfs_close(in); + hfs_close(out); +} + + +bool MiniVMacLauncher::Go(int timeout) +{ + fs::current_path(tempDir); + return ChildProcess(vmacPath.string(), {}, timeout) == 0; +} + +void MiniVMacLauncher::DumpOutput() +{ + vol = hfs_mount(imagePath.string().c_str(), 0, HFS_MODE_RDONLY); + hfsdirent fileent; + int statres = hfs_stat(vol, "out", &fileent); + + hfsfile *out = hfs_open(vol, "out"); + if(!out) + return; + std::vector buffer(fileent.u.file.dsize); + hfs_setfork(out, 0); + hfs_read(out, buffer.data(), fileent.u.file.dsize); + hfs_close(out); + std::cout << string(buffer.begin(), buffer.end()); + hfs_umount(vol); vol = NULL; +} + + +void MiniVMac::GetOptions(options_description &desc) +{ + desc.add_options() + ("minivmac-dir", po::value(),"directory containing vMac.ROM") + ("minivmac-path", po::value()->default_value("./minivmac"),"relative path to minivmac") + ("minivmac-rom", po::value()->default_value("./vMac.ROM"),"minivmac ROM file") + ("system-image", po::value(),"path to disk image with system") + ("autoquit-image", po::value(),"path to autoquit disk image, available from the minivmac web site") + ; +} + +bool MiniVMac::CheckOptions(variables_map &options) +{ + return options.count("minivmac-path") != 0 + && options.count("minivmac-dir") != 0 + && options.count("minivmac-rom") != 0 + && options.count("system-image") != 0 + && options.count("autoquit-image") != 0; +} + +std::unique_ptr MiniVMac::MakeLauncher(variables_map &options) +{ + return std::unique_ptr(new MiniVMacLauncher(options)); +} diff --git a/LaunchAPPL/MiniVMac.h b/LaunchAPPL/MiniVMac.h new file mode 100644 index 0000000000..0f83aa4ad8 --- /dev/null +++ b/LaunchAPPL/MiniVMac.h @@ -0,0 +1,16 @@ +#ifndef MINIVMAC_H +#define MINIVMAC_H + +#include "LaunchMethod.h" + +class MiniVMac : public LaunchMethod +{ +public: + virtual std::string GetName() { return "minivmac"; } + virtual void GetOptions(options_description& desc); + virtual bool CheckOptions(variables_map& options); + + virtual std::unique_ptr MakeLauncher(variables_map& options); +}; + +#endif // MINIVMAC_H diff --git a/README.md b/README.md index dad220f72a..d5f815e60b 100644 --- a/README.md +++ b/README.md @@ -77,12 +77,13 @@ of the Retro68 directory: ../Retro68/build-toolchain.bash The toolchain will be installed in the "toolchain" directory inside -the build directory. +the build directory. All the commands are in `toolchain/bin`, so you might want +to add that to your `PATH`. If you're building this on a PowerMac running Mac OS X 10.4, tell the build script to use the gcc you've installed via tigerbrew: - ../Retro68/build-toolchain.bash --host-cxx-compiler=g++-5 + ../Retro68/build-toolchain.bash --host-cxx-compiler=g++-5 --host-c-compiler=gcc-5 ### Build options and recompiling @@ -100,7 +101,6 @@ The `build-host`, `build-target`, `build-target-ppc` and `build-target-carbon` directories are CMake build directories generated from the top-level `CMakeLists.txt`, so you can also `cd` to one of these and run `make` separately if you've made changes. - Sample programs --------------- @@ -113,7 +113,8 @@ Sample programs are built in several formats: Look under `Retro68-build/build-target/` (68K), `Retro68-build/build-target-ppc/` (PowerPC Classic) and -`Retro68-build/build-target-carbon/` (PowerPC Carbon) for the compiled examples. +`Retro68-build/build-target-carbon/` (PowerPC Carbon) for the compiled examples, +especially under the `Samples` subdirectory. Components ---------- @@ -126,7 +127,7 @@ Third Party Components: - binutils 2.28 - gcc 6.3.0 - newlib 2.10.1 (inside the gcc directory) -- elf2flt (from the ucLinux project's CVS) +- libelf from elfutils-0.170 - hfsutils 3.2.6 Retro68-Specific Components: @@ -134,6 +135,7 @@ Retro68-Specific Components: - Rez - PEFTools (MakePEF and MakeImport) - MakeAPPL +- LaunchAPPL - libretro - TestApps - a few tiny test programs - Sample Programs: Raytracer, HelloWorld, Launcher, Dialog @@ -145,6 +147,7 @@ Two new target platforms: - `powerpc-apple-macos`, based on the `powerpc-ibm-aix` target The powerpc target has a few hacks to make weak symbols work as expected. +The elf target has a hack to protect MacsBug symbols from -gc-sections. ### gcc @@ -169,10 +172,12 @@ PowerPC specific: Standard C library. Currently unmodified. The missing platform-dependent bits haven't been added, instead they are found in 'libretro'. -### elf2flt +### libelf -Converts from ELF to a much simpler binary format. -Minor patch: provide symbols around .init and .fini sections +A library for convenient access to ELF files, taken from the elfutils-0.170 +package. Or rather, brutally ripped out of it, hacked to compile on non-linux +platforms ( is not a standard header file), and made to build with +cmake instead of autotools. Much simpler now. ### hfsutils: @@ -188,6 +193,17 @@ A reimplementation of Apple's Rez resource compiler. Reads `.r` files containing textual resource descriptions and compiles them to binary resource files. +### Elf2Mac + +A wrapper around the linker for 68K programs; it supplies a linker script, +invokes the linker, and converts the resulting ELF binary to a Mac APPL with +one or more segments, or to a flat file which can be converted to a code resource +using Rez. + +### LaunchAPPL + +A tool for lauching compiled Mac applications via various emulators. + ### ConvertObj Reads a MPW 68K Object file (`*.o`) and converts it to input for the @@ -228,6 +244,10 @@ for some standard library functions. Contains a library that implements basic text console functionality. +### AutomatedTests + +An automated test suite that can be run using `ctest` and `LaunchAPPL`. + ### Sample Program: Hello World The binary is in Retro68-build/build-target/Samples/HelloWorld/. @@ -266,3 +286,51 @@ The original parts of Retro68 are licensed under GPL3+, as are most other parts. Some parts are licensed GPL2+ or with more liberal licenses. Check the copyright notices in the individual files. + + + + +LaunchAPPL and the Test Suite +----------------------------- + +`LaunchAPPL` is a tool included with Retro68 intended to make launching the +compiled Mac applications easier. It's use is optional, so you may skip reading +this section. + +Currently, LaunchAPPL supports the following methods for launching Mac applications: + +* classic - launch in the Classic environment on PowerPC Macs up to Tiger (10.4) +* carbon - launch as a Carbon app on PowerPC Macs and via Rosetta on Intel Macs up to Snow Leopard (10.6) +* minivmac - launch using the Mini vMac emulator +* executor - launch using Executor + +If you're running on a Mac that's old enough to use the `classic` or `carbon` backends, +they will work out of the box, just launch an application as follows +(assuming you've added `Retro68-build/toolchain/bin` to your `PATH`): + + LaunchAPPL -e classic Retro68-build/build-target/Samples/Raytracer/Raytracer2.bin + LaunchAPPL -e carbon Retro68-build/build-target-carbon/Samples/Raytracer/Raytracer2.bin + +To specify either environment as a default, or to configure one of the other emulators, +copy the file `Retro68/LaunchAPPL/LaunchAPPL.cfg.example` to `~/.LaunchAPPL.cfg` +and edit to taste (documentation is provided in comments). + +**CONTRIBUTION OPPORTUNITY** - This tool can easily be extended with further backends, +so make it work with your favourtite emulator. Just add new subclasses for the +`LaunchMethod` and `Launcher` classes, they're documented. + +### The Test Suite + +The directory `AutomatedTests` contains an autonated test suite that runs via +`LaunchAPPL`. It's currently only relevant if you want to hack on the low-level +parts of Retro68. + +The test suite will be configured automatically on sufficiently old Macs. +Everywhere else, first configure `LaunchAPPL` (see above) and then: + + cs Retro68-build/build-target + cmake . -DRETRO68_LAUNCH_METHOD=minivmac # or executor, ... + make + +To run the tests, invoke `ctest` in the `build-target` directory. + ctest diff --git a/ResourceFiles/ResInfo.cc b/ResourceFiles/ResInfo.cc index ff8c207ecd..6bc01bb72e 100644 --- a/ResourceFiles/ResInfo.cc +++ b/ResourceFiles/ResInfo.cc @@ -38,8 +38,9 @@ int main(int argc, char *argv[]) ("creator,c", "print creator code") ("all,a", "print all info") ("format,f", "print format") - ("count,n", "print number of resources") - ("filename,l", "echo input file name") + ("count,n", "print number of resources") + ("size,s", "show data fork size") + ("filename,l", "echo input file name") ("set-format,F", po::value(), "resource fork format)") ; po::options_description hidden, alldesc; @@ -82,11 +83,12 @@ int main(int argc, char *argv[]) bool showCreator = options.count("creator") != 0; bool showFormat = options.count("format") != 0; bool showCount = options.count("count") != 0; - + bool showSize = options.count("size") != 0; + ResourceFile::Format format = ResourceFile::Format::autodetect; if(options.count("all")) - showType = showCreator = showFormat = showCount = true; + showType = showCreator = showFormat = showCount = showSize = true; if(options.count("set-format")) { @@ -121,7 +123,9 @@ int main(int argc, char *argv[]) out << " " << reverseFormats[rsrcFile.format]; if(showCount) out << " " << rsrcFile.resources.resources.size(); - + if(showSize) + out << " " << rsrcFile.data.size(); + string str = out.str(); if(str.size()) { diff --git a/ResourceFiles/ResourceFile.cc b/ResourceFiles/ResourceFile.cc index b516c735c5..4ce0dd1032 100644 --- a/ResourceFiles/ResourceFile.cc +++ b/ResourceFiles/ResourceFile.cc @@ -70,7 +70,9 @@ static void writeMacBinary(std::ostream& out, std::string filename, out.seekp(128); out << data; std::streampos dataend = out.tellp(); - std::streampos rsrcstart = ((int)dataend + 0x7F) & ~0x7F; + while((int)out.tellp() % 128) + byte(out,0); + std::streampos rsrcstart = out.tellp(); //((int)dataend + 0x7F) & ~0x7F; rsrc.writeFork(out); std::streampos rsrcend = out.tellp(); @@ -217,11 +219,14 @@ bool ResourceFile::read() case Format::basilisk: { fs::ifstream dataIn(path); + if(!dataIn) + return false; data = std::string(std::istreambuf_iterator(dataIn), std::istreambuf_iterator()); fs::ifstream rsrcIn(path.parent_path() / ".rsrc" / path.filename()); - resources = Resources(rsrcIn); + if(rsrcIn) + resources = Resources(rsrcIn); fs::ifstream finfIn(path.parent_path() / ".finf" / path.filename()); if(finfIn) { @@ -234,10 +239,13 @@ bool ResourceFile::read() case Format::real: { fs::ifstream dataIn(path); + if(!dataIn) + return false; data = std::string(std::istreambuf_iterator(dataIn), std::istreambuf_iterator()); fs::ifstream rsrcIn(path / "..namedfork" / "rsrc"); - resources = Resources(rsrcIn); + if(rsrcIn) + resources = Resources(rsrcIn); char finf[32]; int n = getxattr(path.c_str(), XATTR_FINDERINFO_NAME, @@ -265,13 +273,16 @@ bool ResourceFile::read() in.seekg(26 + i * 12); int what = longword(in); int off = longword(in); - //int len = longword(in); + int len = longword(in); in.seekg(off); switch(what) { case 1: - // ### - // FIXME: read data fork + { + std::vector buf(len); + in.read(buf.data(), len); + data = std::string(buf.begin(), buf.end()); + } break; case 2: resources = Resources(in); @@ -342,7 +353,11 @@ bool ResourceFile::read() unsigned short crc = CalculateCRC(0,header,124); if(word(in) != crc) return false; - // FIXME: read data fork + in.seekg(128); + std::vector buf(datasize); + in.read(buf.data(), datasize); + data = std::string(buf.begin(), buf.end()); + datasize = ((int)datasize + 0x7F) & ~0x7F; in.seekg(128 + datasize); resources = Resources(in); } @@ -490,6 +505,8 @@ bool ResourceFile::write() hfs_format(pathstring.c_str(), 0, 0, path.stem().string().substr(0,27).c_str(), 0, NULL); hfsvol *vol = hfs_mount(pathstring.c_str(), 0, HFS_MODE_RDWR); + if(!vol) + return false; //hfs_setvol(vol, ) hfsfile *file = hfs_create(vol, (path.stem().string().substr(0,31)).c_str(), ((std::string)type).c_str(), ((std::string)creator).c_str()); @@ -510,3 +527,24 @@ bool ResourceFile::write() return true; } +bool ResourceFile::hasPlainDataFork(ResourceFile::Format f) +{ + switch(f) + { +#ifdef __APPLE__ + case Format::real: +#endif + case Format::basilisk: + case Format::underscore_appledouble: + case Format::percent_appledouble: + return true; + default: + return false; + } +} + +bool ResourceFile::hasPlainDataFork() +{ + return hasPlainDataFork(format); +} + diff --git a/ResourceFiles/ResourceFile.h b/ResourceFiles/ResourceFile.h index f31dd4e319..00cea3f0f6 100644 --- a/ResourceFiles/ResourceFile.h +++ b/ResourceFiles/ResourceFile.h @@ -31,6 +31,9 @@ public: bool read(); bool write(); + static bool hasPlainDataFork(Format f); + bool hasPlainDataFork(); + std::string pathstring; Format format; ResType type; diff --git a/Rez/Rez.cc b/Rez/Rez.cc index 2b123bd3ec..d186c3a6ee 100644 --- a/Rez/Rez.cc +++ b/Rez/Rez.cc @@ -79,7 +79,9 @@ int main(int argc, const char *argv[]) po::notify(options); - if(options.count("help") || (!options.count("input") && !options.count("copy"))) + if(options.count("help") + || (!options.count("input") && !options.count("copy") + && !options.count("output"))) { usage(); return 0; diff --git a/build-toolchain.bash b/build-toolchain.bash index 55a75c2fbe..1ec3f849b4 100755 --- a/build-toolchain.bash +++ b/build-toolchain.bash @@ -42,6 +42,8 @@ function usage() echo " --no-ppc disable classic PowerPC CFM support" echo " --no-carbon disable Carbon CFM support" echo " --clean-after-build remove intermediate build files right after building" + echo " --host-cxx-compiler specify C++ compiler (needed on Mac OS X 10.4)" + echo " --host-c-compiler specify C compiler (needed on Mac OS X 10.4)" echo " --help show this help message" } @@ -64,7 +66,11 @@ for ARG in $*; do CLEAN_AFTER_BUILD=true ;; --host-cxx-compiler=*) - HOST_CMAKE_FLAGS[${#HOST_CMAKE_FLAGS}]="-DCMAKE_CXX_COMPILER=${ARG#*=}" + HOST_CMAKE_FLAGS[${#HOST_CMAKE_FLAGS[@]}]="-DCMAKE_CXX_COMPILER=${ARG#*=}" + ;; + --host-c-compiler=*) + HOST_CMAKE_FLAGS[${#HOST_CMAKE_FLAGS[@]}]="-DCMAKE_C_COMPILER=${ARG#*=}" + HOST_C_COMPILER="${ARG#*=}" ;; --help) usage