diff --git a/.gitignore b/.gitignore index 695d9d9..5df72cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .DS_Store *.dSYM bin/* +build *.metallib *.pdb *.exe @@ -22,4 +23,8 @@ sdk/io_stubs.c sdk/orca_surface.c *bind_gen.c +.vscode/launch.json .vscode/settings.json + +__pycache__ +scripts/files diff --git a/.orcaroot b/.orcaroot new file mode 100644 index 0000000..394e764 --- /dev/null +++ b/.orcaroot @@ -0,0 +1,3 @@ +You are currently at the root of the Orca source. Welcome. + +This file exists to signal to the Orca CLI that you are in the Orca source and can do Orca source things. When the CLI detects this file, it will ignore the system Orca installation and use only the contents of this current source checkout. diff --git a/build.sh b/build.sh index f4aba36..6c84383 100755 --- a/build.sh +++ b/build.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -eo pipefail +set -exo pipefail target="$1" diff --git a/milepost b/milepost index 6221370..c103c00 160000 --- a/milepost +++ b/milepost @@ -1 +1 @@ -Subproject commit 6221370aa0e8c520ebde179a1e715a8ed2e28611 +Subproject commit c103c001f7c8e780602193f22360201810802438 diff --git a/orca b/orca new file mode 100755 index 0000000..17ee216 --- /dev/null +++ b/orca @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 + +# This file is copied into the user's home directory on install, +# but also can be run from the root of an Orca source checkout. + +import os +import sys + + +root = True +try: + os.stat(".orcaroot") +except FileNotFoundError: + root = False + +if root: + # Running from Orca source checkout; use local source's scripts. + + scriptdir = os.path.dirname(os.path.abspath(__file__)) + if scriptdir != os.getcwd(): + # Only print this warning if running the system-installed Orca. + # It's annoying to see this if you run ./orca from the source. + print("The Orca tool is running from a local source checkout and will") + print("use that instead of the system Orca installation.") + print() + + sys.path.append(os.getcwd()) + import scripts.orca +else: + # Running from outside Orca source checkout; use system Orca install. + import sys_scripts.orca diff --git a/orca.bat b/orca.bat new file mode 100644 index 0000000..99adab7 --- /dev/null +++ b/orca.bat @@ -0,0 +1,7 @@ +@echo off + +rem Get the directory of this batch script +set "script_dir=%~dp0" + +python3 "%script_dir%orca" %* +exit /b %errorlevel% diff --git a/samples/pong/build.bat b/samples/pong/build.bat index 7ba6677..4cd29cb 100644 --- a/samples/pong/build.bat +++ b/samples/pong/build.bat @@ -14,4 +14,4 @@ set wasmFlags=--target=wasm32^ clang %wasmFlags% -o .\module.wasm ..\..\sdk\orca.c ..\..\cstdlib\src\*.c src\main.c -python3 ..\..\scripts\mkapp.py --orca-dir ..\.. --name Pong --icon icon.png --resource-dir data module.wasm +orca bundle --orca-dir ..\.. --name Pong --icon icon.png --resource-dir data module.wasm diff --git a/samples/pong/build.sh b/samples/pong/build.sh index c941ae0..c53ab3b 100755 --- a/samples/pong/build.sh +++ b/samples/pong/build.sh @@ -30,4 +30,4 @@ wasmFlags="--target=wasm32 \ $CLANG $wasmFlags -o ./module.wasm ../../sdk/orca.c ../../cstdlib/src/*.c src/main.c -python3 ../../scripts/mkapp.py --orca-dir ../.. --name Pong --icon icon.png --resource-dir data module.wasm +orca bundle --orca-dir ../.. --name Pong --icon icon.png --resource-dir data module.wasm diff --git a/samples/ui/build.bat b/samples/ui/build.bat index 768432c..5717a40 100644 --- a/samples/ui/build.bat +++ b/samples/ui/build.bat @@ -14,4 +14,4 @@ set wasmFlags=--target=wasm32^ clang %wasmFlags% -o .\module.wasm ..\..\sdk\orca.c ..\..\cstdlib\src\*.c src\main.c -python3 ..\..\scripts\mkapp.py --orca-dir ..\.. --name UI --resource-dir data module.wasm +orca bundle --orca-dir ..\.. --name UI --resource-dir data module.wasm diff --git a/samples/ui/build.sh b/samples/ui/build.sh old mode 100644 new mode 100755 index 937eb18..8001b8d --- a/samples/ui/build.sh +++ b/samples/ui/build.sh @@ -24,4 +24,4 @@ wasmFlags="--target=wasm32 \ $CLANG $wasmFlags -o ./module.wasm ../../sdk/orca.c ../../cstdlib/src/*.c src/main.c -python3 ../../scripts/mkapp.py --orca-dir ../.. --name UI --resource-dir data module.wasm +orca bundle --orca-dir ../.. --name UI --resource-dir data module.wasm diff --git a/scripts/bindgen.py b/scripts/bindgen.py index 48baa0e..8ba1aa1 100755 --- a/scripts/bindgen.py +++ b/scripts/bindgen.py @@ -3,30 +3,6 @@ from argparse import ArgumentParser import json -parser = ArgumentParser(prog='bindgen.py') -parser.add_argument('api') -parser.add_argument('spec') -parser.add_argument('-g', '--guest-stubs') -parser.add_argument('--guest-include') -parser.add_argument('--wasm3-bindings') - -args = parser.parse_args() - -apiName = args.api -spec = args.spec -guest_stubs_path = args.guest_stubs -if guest_stubs_path == None: - guest_stubs_path = 'bindgen_' + apiName + '_guest_stubs.c' - -wasm3_bindings_path = args.wasm3_bindings -if wasm3_bindings_path == None: - wasm3_bindings_path = 'bindgen_' + apiName + '_wasm3_bindings.c' - -host_bindings = open(wasm3_bindings_path, 'w') -guest_bindings = None - -specFile = open(spec, 'r') -data = json.load(specFile) def needs_arg_ptr_stub(decl): res = (decl['ret']['tag'] == 'S') @@ -35,167 +11,211 @@ def needs_arg_ptr_stub(decl): res = True return(res) -for decl in data: - if needs_arg_ptr_stub(decl): - guest_bindings = open(guest_stubs_path, 'w') - if args.guest_include != None: - s = '#include"' + args.guest_include + '"\n\n' - print(s, file=guest_bindings) - break -for decl in data: +def bindgen(apiName, spec, **kwargs): + guest_stubs_path = kwargs.get("guest_stubs") + guest_include = kwargs.get("guest_include") + wasm3_bindings_path = kwargs.get("wasm3_bindings") - name = decl['name'] - cname = decl.get('cname', name) + if guest_stubs_path == None: + guest_stubs_path = 'bindgen_' + apiName + '_guest_stubs.c' + if wasm3_bindings_path == None: + wasm3_bindings_path = 'bindgen_' + apiName + '_wasm3_bindings.c' - if needs_arg_ptr_stub(decl): - argPtrStubName = name + '_argptr_stub' - # pointer arg stub declaration - s = '' - if decl['ret']['tag'] == 'S': - s += 'void' - else: - s += decl['ret']['name'] + host_bindings = open(wasm3_bindings_path, 'w') + guest_bindings = None - s += ' ORCA_IMPORT(' + argPtrStubName + ') (' + specFile = open(spec, 'r') + data = json.load(specFile) - if decl['ret']['tag'] == 'S': - s += decl['ret']['name'] + '* __retArg' - if len(decl['args']) > 0: - s += ', ' + for decl in data: + if needs_arg_ptr_stub(decl): + guest_bindings = open(guest_stubs_path, 'w') + if guest_include != None: + s = '#include"' + guest_include + '"\n\n' + print(s, file=guest_bindings) + break - for i, arg in enumerate(decl['args']): - s += arg['type']['name'] - if arg['type']['tag'] == 'S': - s += '*' - s += ' ' + arg['name'] - if i+1 < len(decl['args']): - s += ', ' - s += ');\n\n' + for decl in data: - # forward function to pointer arg stub declaration - s += decl['ret']['name'] + ' ' + name + '(' - for i, arg in enumerate(decl['args']): - s += arg['type']['name'] + ' ' + arg['name'] - if i+1 < len(decl['args']): - s += ', ' - s += ')\n' - s += '{\n' - s += '\t' - if decl['ret']['tag'] == 'S': - s += decl['ret']['name'] + ' __ret;\n\t' - elif decl['ret']['tag'] != 'v': - s += decl['ret']['name'] - s += ' __ret = ' - s += argPtrStubName + '(' + name = decl['name'] + cname = decl.get('cname', name) - if decl['ret']['tag'] == 'S': - s += '&__ret' - if len(decl['args']) > 0: - s += ', ' - - for i, arg in enumerate(decl['args']): - if arg['type']['tag'] == 'S': - s += '&' - - s += arg['name'] - if i+1 < len(decl['args']): - s += ', ' - s += ');\n' - if decl['ret']['tag'] != 'v': - s += '\treturn(__ret);\n' - s += '}\n\n' - - print(s, file=guest_bindings) - - # host-side stub - s = 'const void* ' + cname + '_stub(IM3Runtime runtime, IM3ImportContext _ctx, uint64_t* _sp, void* _mem)' - - gen_stub = decl.get('gen_stub', True) - if gen_stub == False: - s += ';\n\n' - else: - s += '\n{\n\t' - retTag = decl['ret']['tag'] - - if retTag == 'i': - s += '*((i32*)&_sp[0]) = ' - elif retTag == 'I': - s += '*((i64*)&_sp[0]) = ' - elif retTag == 'f': - s += '*((f32*)&_sp[0]) = ' - elif retTag == 'F': - s += '*((f64*)&_sp[0]) = ' - elif retTag == 'S': - retTypeName = decl['ret']['name'] - retTypeCName = decl['ret'].get('cname', retTypeName) - s += '*(' + retTypeCName + '*)((char*)_mem + *(i32*)&_sp[0]) = ' - - s += cname + '(' - - firstArgIndex = 0 - if retTag != 'v': - firstArgIndex = 1 - - for i, arg in enumerate(decl['args']): - typeName = arg['type']['name'] - typeCName = arg['type'].get('cname', typeName) - argTag = arg['type']['tag'] - if argTag == 'i': - s += '*(i32*)&_sp[' + str(firstArgIndex + i) + ']' - elif argTag == 'I': - s += '*(i64*)&_sp[' + str(firstArgIndex + i) + ']' - elif argTag == 'f': - s += '*(f32*)&_sp[' + str(firstArgIndex + i) + ']' - elif argTag == 'F': - s += '*(f64*)&_sp[' + str(firstArgIndex + i) + ']' - elif argTag == 'p': - s += '(void*)((char*)_mem + *(i32*)&_sp[' + str(firstArgIndex + i) + '])' - elif argTag == 'S': - s += '*(' + typeCName + '*)((char*)_mem + *(i32*)&_sp[' + str(firstArgIndex + i) + '])' + if needs_arg_ptr_stub(decl): + argPtrStubName = name + '_argptr_stub' + # pointer arg stub declaration + s = '' + if decl['ret']['tag'] == 'S': + s += 'void' else: - print('unrecognized type ' + c + ' in procedure signature\n') - break + s += decl['ret']['name'] - if i+1 < len(decl['args']): - s += ', ' + s += ' ORCA_IMPORT(' + argPtrStubName + ') (' - s += ');\n\treturn(0);\n}\n\n' + if decl['ret']['tag'] == 'S': + s += decl['ret']['name'] + '* __retArg' + if len(decl['args']) > 0: + s += ', ' + + for i, arg in enumerate(decl['args']): + s += arg['type']['name'] + if arg['type']['tag'] == 'S': + s += '*' + s += ' ' + arg['name'] + if i+1 < len(decl['args']): + s += ', ' + s += ');\n\n' + + # forward function to pointer arg stub declaration + s += decl['ret']['name'] + ' ' + name + '(' + for i, arg in enumerate(decl['args']): + s += arg['type']['name'] + ' ' + arg['name'] + if i+1 < len(decl['args']): + s += ', ' + s += ')\n' + s += '{\n' + s += '\t' + if decl['ret']['tag'] == 'S': + s += decl['ret']['name'] + ' __ret;\n\t' + elif decl['ret']['tag'] != 'v': + s += decl['ret']['name'] + s += ' __ret = ' + s += argPtrStubName + '(' + + if decl['ret']['tag'] == 'S': + s += '&__ret' + if len(decl['args']) > 0: + s += ', ' + + for i, arg in enumerate(decl['args']): + if arg['type']['tag'] == 'S': + s += '&' + + s += arg['name'] + if i+1 < len(decl['args']): + s += ', ' + s += ');\n' + if decl['ret']['tag'] != 'v': + s += '\treturn(__ret);\n' + s += '}\n\n' + + print(s, file=guest_bindings) + + # host-side stub + s = 'const void* ' + cname + '_stub(IM3Runtime runtime, IM3ImportContext _ctx, uint64_t* _sp, void* _mem)' + + gen_stub = decl.get('gen_stub', True) + if gen_stub == False: + s += ';\n\n' + else: + s += '\n{\n\t' + retTag = decl['ret']['tag'] + + if retTag == 'i': + s += '*((i32*)&_sp[0]) = ' + elif retTag == 'I': + s += '*((i64*)&_sp[0]) = ' + elif retTag == 'f': + s += '*((f32*)&_sp[0]) = ' + elif retTag == 'F': + s += '*((f64*)&_sp[0]) = ' + elif retTag == 'S': + retTypeName = decl['ret']['name'] + retTypeCName = decl['ret'].get('cname', retTypeName) + s += '*(' + retTypeCName + '*)((char*)_mem + *(i32*)&_sp[0]) = ' + + s += cname + '(' + + firstArgIndex = 0 + if retTag != 'v': + firstArgIndex = 1 + + for i, arg in enumerate(decl['args']): + typeName = arg['type']['name'] + typeCName = arg['type'].get('cname', typeName) + argTag = arg['type']['tag'] + if argTag == 'i': + s += '*(i32*)&_sp[' + str(firstArgIndex + i) + ']' + elif argTag == 'I': + s += '*(i64*)&_sp[' + str(firstArgIndex + i) + ']' + elif argTag == 'f': + s += '*(f32*)&_sp[' + str(firstArgIndex + i) + ']' + elif argTag == 'F': + s += '*(f64*)&_sp[' + str(firstArgIndex + i) + ']' + elif argTag == 'p': + s += '(void*)((char*)_mem + *(i32*)&_sp[' + str(firstArgIndex + i) + '])' + elif argTag == 'S': + s += '*(' + typeCName + '*)((char*)_mem + *(i32*)&_sp[' + str(firstArgIndex + i) + '])' + else: + print('unrecognized type ' + c + ' in procedure signature\n') + break + + if i+1 < len(decl['args']): + s += ', ' + + s += ');\n\treturn(0);\n}\n\n' + + print(s, file=host_bindings) + + # link function + s = 'int bindgen_link_' + apiName + '_api(IM3Module module)\n{\n\t' + s += 'M3Result res;\n' + + for decl in data: + name = decl['name'] + cname = decl.get('cname', name) + + if needs_arg_ptr_stub(decl): + name = name + '_argptr_stub' + + m3Sig = '' + if decl['ret']['tag'] == 'S': + m3Sig += 'v' + else: + m3Sig += decl['ret']['tag'] + + m3Sig += '(' + if decl['ret']['tag'] == 'S': + m3Sig += 'i' + for arg in decl['args']: + tag = arg['type']['tag'] + if tag == 'p' or tag == 'S': + tag = 'i' + m3Sig += tag + m3Sig += ')' + + + s += '\tres = m3_LinkRawFunction(module, "*", "' + name + '", "' + m3Sig + '", ' + cname + '_stub);\n' + s += '\tif(res != m3Err_none && res != m3Err_functionLookupFailed) { log_error("error: %s\\n", res); return(-1); }\n\n' + + + s += '\treturn(0);\n}\n' print(s, file=host_bindings) -# link function -s = 'int bindgen_link_' + apiName + '_api(IM3Module module)\n{\n\t' -s += 'M3Result res;\n' -for decl in data: - name = decl['name'] - cname = decl.get('cname', name) +if __name__ == "__main__": + parser = ArgumentParser(prog='bindgen.py') + parser.add_argument('api') + parser.add_argument('spec') + parser.add_argument('-g', '--guest-stubs') + parser.add_argument('--guest-include') + parser.add_argument('--wasm3-bindings') - if needs_arg_ptr_stub(decl): - name = name + '_argptr_stub' + args = parser.parse_args() - m3Sig = '' - if decl['ret']['tag'] == 'S': - m3Sig += 'v' - else: - m3Sig += decl['ret']['tag'] + apiName = args.api + spec = args.spec + guest_stubs_path = args.guest_stubs + if guest_stubs_path == None: + guest_stubs_path = 'bindgen_' + apiName + '_guest_stubs.c' - m3Sig += '(' - if decl['ret']['tag'] == 'S': - m3Sig += 'i' - for arg in decl['args']: - tag = arg['type']['tag'] - if tag == 'p' or tag == 'S': - tag = 'i' - m3Sig += tag - m3Sig += ')' - - - s += '\tres = m3_LinkRawFunction(module, "*", "' + name + '", "' + m3Sig + '", ' + cname + '_stub);\n' - s += '\tif(res != m3Err_none && res != m3Err_functionLookupFailed) { log_error("error: %s\\n", res); return(-1); }\n\n' - - -s += '\treturn(0);\n}\n' - -print(s, file=host_bindings) + wasm3_bindings_path = args.wasm3_bindings + if wasm3_bindings_path == None: + wasm3_bindings_path = 'bindgen_' + apiName + '_wasm3_bindings.c' + + bindgen(apiName, spec, + guest_stubs=guest_stubs_path, + guest_include=args.guest_include, + wasm3_bindings=wasm3_bindings_path, + ) diff --git a/scripts/mkapp.py b/scripts/bundle.py similarity index 55% rename from scripts/mkapp.py rename to scripts/bundle.py index 26e8252..1cd4791 100644 --- a/scripts/mkapp.py +++ b/scripts/bundle.py @@ -6,19 +6,53 @@ import shutil import subprocess from argparse import ArgumentParser +from .log import * + + +def attach_bundle_commands(subparsers): + mkapp_cmd = subparsers.add_parser("bundle", help="Package a WebAssembly module into a standalone Orca application.") + init_parser(mkapp_cmd) + + +def init_parser(parser): + parser.add_argument("-d", "--resource", action="append", dest="resource_files", help="copy a file to the app's resource directory") + parser.add_argument("-D", "--resource-dir", action="append", dest="resource_dirs", help="copy a directory to the app's resource directory") + parser.add_argument("-i", "--icon", help="an image file to use as the application's icon") + parser.add_argument("-C", "--out-dir", default=os.getcwd(), help="where to place the final application bundle (defaults to the current directory)") + parser.add_argument("-n", "--name", default="out", help="the app's name") + parser.add_argument("-O", "--orca-dir", default=".") + parser.add_argument("--version", default="0.0.0", help="a version number to embed in the application bundle") + parser.add_argument("module", help="a .wasm file containing the application's wasm module") + parser.set_defaults(func=shellish(make_app)) + + +def make_app(args): + #----------------------------------------------------------- + # Dispatch to platform-specific function + #----------------------------------------------------------- + platformName = platform.system() + if platformName == 'Darwin': + macos_make_app(args) + elif platformName == 'Windows': + windows_make_app(args) + else: + log_error("Platform '" + platformName + "' is not supported for now...") + exit(1) + + def macos_make_app(args): #----------------------------------------------------------- #NOTE: make bundle directory structure #----------------------------------------------------------- app_name = args.name bundle_name = app_name + '.app' - bundle_path = args.out_dir + '/' + bundle_name - contents_dir = bundle_path + '/Contents' - exe_dir = contents_dir + '/MacOS' - res_dir = contents_dir + '/resources' - guest_dir = contents_dir + '/app' - wasm_dir = guest_dir + '/wasm' - data_dir = guest_dir + '/data' + bundle_path = os.path.join(args.out_dir, bundle_name) + contents_dir = os.path.join(bundle_path, 'Contents') + exe_dir = os.path.join(contents_dir, 'MacOS') + res_dir = os.path.join(contents_dir, 'resources') + guest_dir = os.path.join(contents_dir, 'app') + wasm_dir = os.path.join(guest_dir, 'wasm') + data_dir = os.path.join(guest_dir, 'data') if os.path.exists(bundle_path): shutil.rmtree(bundle_path) @@ -33,11 +67,11 @@ def macos_make_app(args): #----------------------------------------------------------- #NOTE: copy orca runtime executable and libraries #----------------------------------------------------------- - orca_exe = args.orca_dir + '/bin/orca' - milepost_lib = args.orca_dir + '/bin/libmilepost.dylib' - gles_lib = args.orca_dir + '/bin/libGLESv2.dylib' - egl_lib = args.orca_dir + '/bin/libEGL.dylib' - renderer_lib = args.orca_dir + '/bin/mtl_renderer.metallib' + orca_exe = os.path.join(args.orca_dir, 'build/bin/orca') + milepost_lib = os.path.join(args.orca_dir, 'build/bin/libmilepost.dylib') + gles_lib = os.path.join(args.orca_dir, 'build/bin/libGLESv2.dylib') + egl_lib = os.path.join(args.orca_dir, 'build/bin/libEGL.dylib') + renderer_lib = os.path.join(args.orca_dir, 'build/bin/mtl_renderer.metallib') shutil.copy(orca_exe, exe_dir) shutil.copy(milepost_lib, exe_dir) @@ -48,18 +82,18 @@ def macos_make_app(args): #----------------------------------------------------------- #NOTE: copy wasm module and data #----------------------------------------------------------- - shutil.copy(args.module, wasm_dir + '/module.wasm') + shutil.copy(args.module, os.path.join(wasm_dir, 'module.wasm')) if args.resource_files != None: for resource in args.resource_files: - shutil.copytree(resource, data_dir + '/' + os.path.basename(resource), dirs_exist_ok=True) + shutil.copytree(resource, os.path.join(data_dir, os.path.basename(resource)), dirs_exist_ok=True) if args.resource_dirs != None: for resource_dir in args.resource_dirs: for resource in os.listdir(resource_dir): - src = resource_dir + '/' + resource + src = os.path.join(resource_dir, resource) if os.path.isdir(src): - shutil.copytree(src, data_dir + '/' + os.path.basename(resource), dirs_exist_ok=True) + shutil.copytree(src, os.path.join(data_dir, os.path.basename(resource)), dirs_exist_ok=True) else: shutil.copy(src, data_dir) @@ -67,15 +101,15 @@ def macos_make_app(args): #NOTE: copy runtime resources #----------------------------------------------------------- # default fonts - shutil.copy(args.orca_dir + '/resources/OpenSansLatinSubset.ttf', res_dir) - shutil.copy(args.orca_dir + '/resources/Menlo.ttf', res_dir) - shutil.copy(args.orca_dir + '/resources/Menlo Bold.ttf', res_dir) - shutil.copy(args.orca_dir + '/resources/Menlo Italics.ttf', res_dir) + shutil.copy(os.path.join(args.orca_dir, 'resources/OpenSansLatinSubset.ttf'), res_dir) + shutil.copy(os.path.join(args.orca_dir, 'resources/Menlo.ttf'), res_dir) + shutil.copy(os.path.join(args.orca_dir, 'resources/Menlo Bold.ttf'), res_dir) + shutil.copy(os.path.join(args.orca_dir, 'resources/Menlo Italics.ttf'), res_dir) #----------------------------------------------------------- #NOTE make icon #----------------------------------------------------------- - src_image=args.icon + src_image = args.icon #if src_image == None: # src_image = orca_dir + '/resources/default_app_icon.png' @@ -104,7 +138,7 @@ def macos_make_app(args): size = size*2 - subprocess.run(['iconutil', '-c', 'icns', '-o', res_dir + '/icon.icns', iconset]) + subprocess.run(['iconutil', '-c', 'icns', '-o', os.path.join(res_dir, 'icon.icns'), iconset]) shutil.rmtree(iconset) #----------------------------------------------------------- @@ -114,7 +148,7 @@ def macos_make_app(args): bundle_sig = "????" icon_file = '' - plist_contents = """ + plist_contents = f""" @@ -139,7 +173,7 @@ def macos_make_app(args): True - """.format(app_name=app_name, version=version, bundle_sig=bundle_sig, icon_file=icon_file) + """ plist_file = open(contents_dir + '/Info.plist', 'w') print(plist_contents, file=plist_file) @@ -150,12 +184,12 @@ def windows_make_app(args): #----------------------------------------------------------- app_name = args.name bundle_name = app_name - bundle_dir = args.out_dir + '/' + bundle_name - exe_dir = bundle_dir + '/bin' - res_dir = bundle_dir + '/resources' - guest_dir = bundle_dir + '/app' - wasm_dir = guest_dir + '/wasm' - data_dir = guest_dir + '/data' + bundle_dir = os.path.join(args.out_dir, bundle_name) + exe_dir = os.path.join(bundle_dir, 'bin') + res_dir = os.path.join(bundle_dir, 'resources') + guest_dir = os.path.join(bundle_dir, 'app') + wasm_dir = os.path.join(guest_dir, 'wasm') + data_dir = os.path.join(guest_dir, 'data') if os.path.exists(bundle_dir): shutil.rmtree(bundle_dir) @@ -169,10 +203,10 @@ def windows_make_app(args): #----------------------------------------------------------- #NOTE: copy orca runtime executable and libraries #----------------------------------------------------------- - orca_exe = args.orca_dir + '/bin/orca.exe' - milepost_lib = args.orca_dir + '/bin/milepost.dll' - gles_lib = args.orca_dir + '/milepost/bin/libGLESv2.dll' - egl_lib = args.orca_dir + '/milepost/bin/libEGL.dll' + orca_exe = os.path.join(args.orca_dir, 'build/bin/orca.exe') + milepost_lib = os.path.join(args.orca_dir, 'build/bin/milepost.dll') + gles_lib = os.path.join(args.orca_dir, 'milepost/build/bin/libGLESv2.dll') + egl_lib = os.path.join(args.orca_dir, 'milepost/build/bin/libEGL.dll') shutil.copy(orca_exe, exe_dir) shutil.copy(milepost_lib, exe_dir) @@ -202,47 +236,20 @@ def windows_make_app(args): #NOTE: copy runtime resources #----------------------------------------------------------- # default fonts - shutil.copy(args.orca_dir + '/resources/OpenSansLatinSubset.ttf', res_dir) - shutil.copy(args.orca_dir + '/resources/Menlo.ttf', res_dir) - shutil.copy(args.orca_dir + '/resources/Menlo Bold.ttf', res_dir) - shutil.copy(args.orca_dir + '/resources/Menlo Italics.ttf', res_dir) + shutil.copy(os.path.join(args.orca_dir, 'resources/OpenSansLatinSubset.ttf'), res_dir) + shutil.copy(os.path.join(args.orca_dir, 'resources/Menlo.ttf'), res_dir) + shutil.copy(os.path.join(args.orca_dir, 'resources/Menlo Bold.ttf'), res_dir) + shutil.copy(os.path.join(args.orca_dir, 'resources/Menlo Italics.ttf'), res_dir) #----------------------------------------------------------- #NOTE make icon #----------------------------------------------------------- #TODO -#--------------------------------------------------------------------------------------------- -# NOTE: get args -# -# mkapp.py [options] module -# -# -n, --name the name of the app -# -r, --res-file copies a file to the app bundle's resource directory -# -R, --res-dir copies the contents of a directory to the bundle's resource directory -# -i, --icon icon file -# -D, --out-dir output directory -#---------------------------------------------------------------------------------------------- -parser = ArgumentParser(prog='mkapp') -parser.add_argument("-d", "--resource", action='append', dest='resource_files') -parser.add_argument("-D", "--resource-dir", action='append', dest='resource_dirs') -parser.add_argument("-i", "--icon") -parser.add_argument("-C", "--out-dir", default=os.getcwd()) -parser.add_argument("-n", "--name", default='out') -parser.add_argument("-O", "--orca-dir", default='.') -parser.add_argument("--version", default='0.0.0') -parser.add_argument("module") +if __name__ == "__main__": + parser = ArgumentParser(prog='mkapp') + init_parser(parser) -args = parser.parse_args() - -#---------------------------------------------------------------------------------------------- -# Dispatch to platform-specific function -#---------------------------------------------------------------------------------------------- -platformName = platform.system() -if platformName == 'Darwin': - macos_make_app(args) -elif platformName == 'Windows': - windows_make_app(args) -else: - print("Platform '" + platformName + "' is not supported for now...") + args = parser.parse_args() + make_app(args) diff --git a/scripts/checksum.py b/scripts/checksum.py new file mode 100644 index 0000000..972ce9a --- /dev/null +++ b/scripts/checksum.py @@ -0,0 +1,30 @@ +import hashlib +import json + +from .log import * + + +def checkfile(filepath): + newsum = filesum(filepath) + + sums = {} + with open("scripts/checksums.json", "r") as sumsfile: + sums = json.loads(sumsfile.read()) + if filepath not in sums: + msg = log_warning(f"no checksum saved for file {filepath}") + msg.more(f"file had checksum: {newsum}") + return False + sum = sums[filepath] + + if sum != newsum: + msg = log_warning(f"checksums did not match for {filepath}:") + msg.more(f"expected: {sum}") + msg.more(f" got: {newsum}") + return False + + return True + + +def filesum(filepath): + with open(filepath, "rb") as file: + return hashlib.sha256(file.read()).hexdigest() diff --git a/scripts/checksums.json b/scripts/checksums.json new file mode 100644 index 0000000..74cea47 --- /dev/null +++ b/scripts/checksums.json @@ -0,0 +1,10 @@ +{ + "scripts/files/angle-windows-2019-2023-07-05.zip": "a333b5ccc8462151ee8df65c43cfacd70d9db2413f2e495da65670737b5b2d96", + "scripts/files/angle-macos-jank-2023-07-05.zip": "c234b8db179a24757ab9f46610a032123718dd9bed967d2bf8e27d0d17eb0aff", + "milepost/build/bin/libEGL.dll": "b7bf51f83e88129ddc20c0c2cb904ec04c89059a30a2cd29b9b1ea11c80388fb", + "milepost/build/bin/libEGL.dll.lib": "4cec54c534136da413dea86bd271ccb9c5ae88e40aa91d1de7a01e701be8e1d7", + "milepost/build/bin/libGLESv2.dll": "193b53b0a16b702eaa28a73e84527acf7aecfd665e3e3f54a8d9db9ae73111e1", + "milepost/build/bin/libGLESv2.dll.lib": "ee87aac129efe8fe871825d181b85da1b1ea6626cb48be52c2e689f2804b953f", + "milepost/build/bin/libEGL.dylib": "96baea4b1c8578d30738764784d07b509d1153df694c094faba2ee8ccbfde2a9", + "milepost/build/bin/libGLESv2.dylib": "98ce9f6248a6447ba9199e96b8f6d87df9548d43ce583a2615717aab168ecb71" +} diff --git a/scripts/dev.py b/scripts/dev.py new file mode 100644 index 0000000..571b9d0 --- /dev/null +++ b/scripts/dev.py @@ -0,0 +1,584 @@ +import argparse +from datetime import datetime +import glob +import os +import platform +import urllib.request +import shutil +import subprocess +from zipfile import ZipFile + +from . import checksum +from .bindgen import bindgen +from .log import * +from .utils import pushd, removeall + + +ANGLE_VERSION = "2023-07-05" + + +def attach_dev_commands(subparsers): + dev_cmd = subparsers.add_parser("dev", help="Commands for building Orca itself. Must be run from the root of an Orca source checkout.") + dev_cmd.set_defaults(func=orca_root_only) + + dev_sub = dev_cmd.add_subparsers(required=is_orca_root(), title='commands') + + build_cmd = dev_sub.add_parser("build-runtime", help="Build the Orca runtime from source.") + build_cmd.add_argument("--release", action="store_true", help="compile Orca in release mode (default is debug)") + build_cmd.set_defaults(func=dev_shellish(build_runtime)) + + clean_cmd = dev_sub.add_parser("clean", help="Delete all build artifacts and start fresh.") + clean_cmd.set_defaults(func=dev_shellish(clean)) + + install_cmd = dev_sub.add_parser("install", help="Install the Orca tools into a system folder.") + install_cmd.add_argument("--no-confirm", action="store_true", help="don't ask the user for confirmation before installing") + install_cmd.set_defaults(func=dev_shellish(install)) + + +def is_orca_root(): + try: + os.stat(".orcaroot") + return True + except FileNotFoundError: + return False + + +def orca_root_only(args): + print("The Orca dev commands can only be run from an Orca source checkout.") + print() + print("If you want to build Orca yourself, download the source here:") + print("https://git.handmade.network/hmn/orca") + exit(1) + + +def dev_shellish(func): + return shellish(func) if is_orca_root() else orca_root_only + + +def build_runtime(args): + ensure_programs() + ensure_angle() + + build_milepost("lib", args.release) + build_wasm3(args.release) + build_orca(args.release) + + +def clean(args): + yeet("build") + yeet("milepost/build") + yeet("scripts/files") + yeet("scripts/__pycache__") + + +def build_milepost(target, release): + print("Building milepost...") + with pushd("milepost"): + os.makedirs("build/bin", exist_ok=True) + os.makedirs("build/lib", exist_ok=True) + os.makedirs("resources", exist_ok=True) + + if target == "lib": + if platform.system() == "Windows": + build_milepost_lib_win(release) + elif platform.system() == "Darwin": + build_milepost_lib_mac(release) + else: + log_error(f"can't build milepost for unknown platform '{platform.system()}'") + exit(1) + elif target == "test": + with pushd("examples/test_app"): + # TODO? + subprocess.run(["./build.sh"]) + elif target == "clean": + removeall("bin") + else: + log_error(f"unrecognized milepost target '{target}'") + exit(1) + + +def build_milepost_lib_win(release): + # TODO(ben): delete embed_text.py + embed_text_glsl("src\\glsl_shaders.h", "glsl_", [ + "src\\glsl_shaders\\common.glsl", + "src\\glsl_shaders\\blit_vertex.glsl", + "src\\glsl_shaders\\blit_fragment.glsl", + "src\\glsl_shaders\\path_setup.glsl", + "src\\glsl_shaders\\segment_setup.glsl", + "src\\glsl_shaders\\backprop.glsl", + "src\\glsl_shaders\\merge.glsl", + "src\\glsl_shaders\\raster.glsl", + "src\\glsl_shaders\\balance_workgroups.glsl", + ]) + + includes = [ + "/I", "src", + "/I", "src/util", + "/I", "src/platform", + "/I", "ext", + "/I", "ext/angle_headers", + ] + libs = [ + "user32.lib", + "opengl32.lib", + "gdi32.lib", + "shcore.lib", + "delayimp.lib", + "dwmapi.lib", + "comctl32.lib", + "ole32.lib", + "shell32.lib", + "shlwapi.lib", + "/LIBPATH:./build/bin", + "libEGL.dll.lib", + "libGLESv2.dll.lib", + "/DELAYLOAD:libEGL.dll", + "/DELAYLOAD:libGLESv2.dll", + ] + + subprocess.run([ + "cl", "/nologo", + "/we4013", "/Zi", "/Zc:preprocessor", + "/DMP_BUILD_DLL", + "/std:c11", "/experimental:c11atomics", + *includes, + "src/milepost.c", "/Fo:build/bin/milepost.o", + "/LD", "/link", + "/MANIFEST:EMBED", "/MANIFESTINPUT:src/win32_manifest.xml", + *libs, + "/OUT:build/bin/milepost.dll", + "/IMPLIB:build/bin/milepost.dll.lib", + ], check=True) + + +def build_milepost_lib_mac(release): + sdk_dir = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk" + + flags = ["-mmacos-version-min=10.15.4", "-maes"] + cflags = ["-std=c11"] + debug_flags = ["-O3"] if release else ["-g", "-DDEBUG", "-DLOG_COMPILE_DEBUG"] + ldflags = [f"-L{sdk_dir}/usr/lib", f"-F{sdk_dir}/System/Library/Frameworks/"] + includes = ["-Isrc", "-Isrc/util", "-Isrc/platform", "-Iext", "-Iext/angle_headers"] + + # compile metal shader + subprocess.run([ + "xcrun", "-sdk", "macosx", "metal", + # TODO: shaderFlagParam + "-fno-fast-math", "-c", + "-o", "build/mtl_renderer.air", + "src/mtl_renderer.metal", + ], check=True) + subprocess.run([ + "xcrun", "-sdk", "macosx", "metallib", + "-o", "build/bin/mtl_renderer.metallib", + "build/mtl_renderer.air", + ], check=True) + + # compile milepost. We use one compilation unit for all C code, and one + # compilation unit for all Objective-C code + subprocess.run([ + "clang", + *debug_flags, "-c", + "-o", "build/milepost_c.o", + *cflags, *flags, *includes, + "src/milepost.c" + ], check=True) + subprocess.run([ + "clang", + *debug_flags, "-c", + "-o", "build/milepost_objc.o", + *flags, *includes, + "src/milepost.m" + ], check=True) + + # build dynamic library + subprocess.run([ + "ld", + *ldflags, "-dylib", + "-o", "build/bin/libmilepost.dylib", + "build/milepost_c.o", "build/milepost_objc.o", + "-Lbuild/bin", "-lc", + "-framework", "Carbon", "-framework", "Cocoa", "-framework", "Metal", "-framework", "QuartzCore", + "-weak-lEGL", "-weak-lGLESv2", + ], check=True) + + # change dependent libs path to @rpath + subprocess.run([ + "install_name_tool", + "-change", "./libEGL.dylib", "@rpath/libEGL.dylib", + "build/bin/libmilepost.dylib", + ], check=True) + subprocess.run([ + "install_name_tool", + "-change", "./libGLESv2.dylib", "@rpath/libGLESv2.dylib", + "build/bin/libmilepost.dylib", + ], check=True) + + # add executable path to rpath. Client executable can still add its own + # rpaths if needed, e.g. @executable_path/libs/ etc. + subprocess.run([ + "install_name_tool", + "-id", "@rpath/libmilepost.dylib", + "build/bin/libmilepost.dylib", + ], check=True) + + +def build_wasm3(release): + print("Building wasm3...") + + os.makedirs("build/bin", exist_ok=True) + os.makedirs("build/lib", exist_ok=True) + os.makedirs("build/obj", exist_ok=True) + + if platform.system() == "Windows": + build_wasm3_lib_win(release) + elif platform.system() == "Darwin": + build_wasm3_lib_mac(release) + else: + log_error(f"can't build wasm3 for unknown platform '{platform.system()}'") + exit(1) + + +def build_wasm3_lib_win(release): + for f in glob.iglob("./ext/wasm3/source/*.c"): + name = os.path.splitext(os.path.basename(f))[0] + subprocess.run([ + "cl", "/nologo", + "/Zi", "/Zc:preprocessor", "/c", + "/O2", + f"/Fo:build/obj/{name}.obj", + "/I", "./ext/wasm3/source", + f, + ], check=True) + subprocess.run([ + "lib", "/nologo", "/out:build/bin/wasm3.lib", + "build/obj/*.obj", + ], check=True) + + +def build_wasm3_lib_mac(release): + includes = ["-Iext/wasm3/source"] + debug_flags = ["-g", "-O2"] + flags = [ + *debug_flags, + "-foptimize-sibling-calls", + "-Wno-extern-initializer", + "-Dd_m3VerboseErrorMessages", + ] + + for f in glob.iglob("ext/wasm3/source/*.c"): + name = os.path.splitext(os.path.basename(f))[0] + ".o" + subprocess.run([ + "clang", "-c", *flags, *includes, + "-o", f"build/obj/{name}", + f, + ], check=True) + subprocess.run(["ar", "-rcs", "build/lib/libwasm3.a", *glob.glob("build/obj/*.o")], check=True) + subprocess.run(["rm", "-rf", "build/obj"], check=True) + + +def build_orca(release): + print("Building Orca...") + + os.makedirs("build/bin", exist_ok=True) + os.makedirs("build/lib", exist_ok=True) + + if platform.system() == "Windows": + build_orca_win(release) + elif platform.system() == "Darwin": + build_orca_mac(release) + else: + log_error(f"can't build Orca for unknown platform '{platform.system()}'") + exit(1) + + +def build_orca_win(release): + # copy libraries + shutil.copy("milepost/build/bin/milepost.dll", "build/bin") + shutil.copy("milepost/build/bin/milepost.dll.lib", "build/bin") + + gen_all_bindings() + + # compile orca + includes = [ + "/I", "src", + "/I", "sdk", + "/I", "ext/wasm3/source", + "/I", "milepost/src", + "/I", "milepost/ext", + ] + libs = [ + "/LIBPATH:build/bin", + "milepost.dll.lib", + "wasm3.lib", + ] + + subprocess.run([ + "cl", + "/Zi", "/Zc:preprocessor", + "/std:c11", "/experimental:c11atomics", + *includes, + "src/main.c", + "/link", *libs, + "/out:build/bin/orca.exe", + ], check=True) + + +def build_orca_mac(release): + # copy libraries + shutil.copy("milepost/build/bin/mtl_renderer.metallib", "build/bin/") + shutil.copy("milepost/build/bin/libmilepost.dylib", "build/bin/") + shutil.copy("milepost/build/bin/libGLESv2.dylib", "build/bin/") + shutil.copy("milepost/build/bin/libEGL.dylib", "build/bin/") + + includes = [ + "-Isrc", + "-Isdk", + "-Imilepost/src", + "-Imilepost/src/util", + "-Imilepost/src/platform", + "-Iext/wasm3/source", + "-Imilepost/ext/", + ] + libs = ["-Lbuild/bin", "-Lbuild/lib", "-lmilepost", "-lwasm3"] + debug_flags = ["-O2"] if release else ["-g", "-DLOG_COMPILE_DEBUG"] + flags = [ + *debug_flags, + "-mmacos-version-min=10.15.4", + "-maes", + ] + + gen_all_bindings() + + # compile orca + subprocess.run([ + "clang", *flags, *includes, *libs, + "-o", "build/bin/orca", + "src/main.c", + ], check=True) + + # fix libs imports + subprocess.run([ + "install_name_tool", + "-change", "build/bin/libmilepost.dylib", "@rpath/libmilepost.dylib", + "build/bin/orca", + ], check=True) + subprocess.run([ + "install_name_tool", + "-add_rpath", "@executable_path/", + "build/bin/orca", + ], check=True) + + +def gen_all_bindings(): + bindgen("core", "src/core_api.json", + wasm3_bindings="src/core_api_bind_gen.c", + ) + bindgen("gles", "src/gles_api.json", + wasm3_bindings="src/gles_api_bind_gen.c", + ) + + bindgen("canvas", "src/canvas_api.json", + guest_stubs="sdk/orca_surface.c", + guest_include="graphics.h", + wasm3_bindings="src/canvas_api_bind_gen.c", + ) + bindgen("clock", "src/clock_api.json", + guest_stubs="sdk/orca_clock.c", + guest_include="platform_clock.h", + wasm3_bindings="src/clock_api_bind_gen.c", + ) + bindgen("io", "src/io_api.json", + guest_stubs="sdk/io_stubs.c", + wasm3_bindings="src/io_api_bind_gen.c", + ) + + +def ensure_programs(): + if platform.system() == "Windows": + try: + subprocess.run(["cl"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except FileNotFoundError: + msg = log_error("MSVC was not found on your system.") + msg.more("If you have already installed Visual Studio, make sure you are running in a") + msg.more("Visual Studio command prompt or you have run vcvarsall.bat. Otherwise, download") + msg.more("and install Visual Studio: https://visualstudio.microsoft.com/") + exit(1) + + try: + subprocess.run(["clang", "-v"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + except FileNotFoundError: + msg = log_error("clang was not found on your system.") + if platform.system() == "Windows": + msg.more("We recommend installing clang via the Visual Studio installer.") + elif platform.system() == "Darwin": + msg.more("Run the following to install it:") + msg.more() + msg.more(" brew install llvm") + msg.more() + exit(1) + # TODO(ben): Check for xcode command line tools + + +def ensure_angle(): + if not verify_angle(): + download_angle() + print("Verifying ANGLE download...") + if not verify_angle(): + log_error("automatic ANGLE download failed") + exit(1) + + +def verify_angle(): + checkfiles = None + if platform.system() == "Windows": + checkfiles = [ + "milepost/build/bin/libEGL.dll", + "milepost/build/bin/libEGL.dll.lib", + "milepost/build/bin/libGLESv2.dll", + "milepost/build/bin/libGLESv2.dll.lib", + ] + elif platform.system() == "Darwin": + checkfiles = [ + "milepost/build/bin/libEGL.dylib", + "milepost/build/bin/libGLESv2.dylib", + ] + + if checkfiles is None: + log_warning("could not verify if the correct version of ANGLE is present") + return False + + ok = True + for file in checkfiles: + if not os.path.isfile(file): + ok = False + continue + if not checksum.checkfile(file): + ok = False + continue + + return ok + + +def download_angle(): + print("Downloading ANGLE...") + if platform.system() == "Windows": + build = "windows-2019" + extensions = [ + ("dll", "milepost/build/bin/"), + ("lib", "milepost/build/bin/"), + ] + elif platform.system() == "Darwin": + build = "macos-jank" + extensions = [ + ("dylib", "milepost/build/bin/"), + ] + else: + log_error(f"could not automatically download ANGLE for unknown platform {platform.system()}") + return + + os.makedirs("scripts/files", exist_ok=True) + filename = f"angle-{build}-{ANGLE_VERSION}.zip" + filepath = f"scripts/files/{filename}" + url = f"https://github.com/HandmadeNetwork/build-angle/releases/download/{ANGLE_VERSION}/{filename}" + with urllib.request.urlopen(url) as response: + with open(filepath, "wb") as out: + shutil.copyfileobj(response, out) + + if not checksum.checkfile(filepath): + log_error(f"ANGLE download did not match checksum") + exit(1) + + print("Extracting ANGLE...") + with ZipFile(filepath, "r") as anglezip: + anglezip.extractall(path="scripts/files") + + os.makedirs("milepost/build/bin", exist_ok=True) + for angleDir in ["bin", "lib"]: + for (ext, dest) in extensions: + for filepath in glob.glob(f"scripts/files/angle/{angleDir}/*.{ext}"): + shutil.copy(filepath, dest) + + +def embed_text_glsl(outputpath, prefix, shaders): + output = open(outputpath, "w") + output.write("/*********************************************************************\n") + output.write("*\n") + output.write("*\tfile: %s\n" % os.path.basename(outputpath)) + output.write("*\tnote: string literals auto-generated by build_runtime.py\n") + output.write("*\tdate: %s\n" % datetime.now().strftime("%d/%m%Y")) + output.write("*\n") + output.write("**********************************************************************/\n") + + outSymbol = (os.path.splitext(os.path.basename(outputpath))[0]).upper() + + output.write("#ifndef __%s_H__\n" % outSymbol) + output.write("#define __%s_H__\n" % outSymbol) + output.write("\n\n") + + for fileName in shaders: + f = open(fileName, "r") + lines = f.read().splitlines() + + output.write("//NOTE: string imported from %s\n" % fileName) + + stringName = os.path.splitext(os.path.basename(fileName))[0] + output.write(f"const char* {prefix}{stringName} = ") + + for line in lines: + output.write("\n\"%s\\n\"" % line) + + output.write(";\n\n") + f.close() + + output.write("#endif // __%s_H__\n" % outSymbol) + + output.close() + + +def yeet(path): + os.makedirs(path, exist_ok=True) + shutil.rmtree(path) + + +def install(args): + if platform.system() == "Windows": + dest = os.path.join(os.getenv("LOCALAPPDATA"), "orca") + else: + dest = os.path.expanduser(os.path.join("~", ".orca")) + + if not args.no_confirm: + print("The Orca command-line tools will be installed to:") + print(dest) + print() + while True: + answer = input("Proceed with the installation? (y/n) >") + if answer.lower() in ["y", "yes"]: + break + elif answer.lower() in ["n", "no"]: + return + else: + print("Please enter \"yes\" or \"no\" and press return.") + + bin_dir = os.path.join(dest, "bin") + yeet(bin_dir) + shutil.copytree("scripts", os.path.join(bin_dir, "sys_scripts")) + shutil.copy("orca", bin_dir) + if platform.system() == "Windows": + shutil.copy("orca.bat", bin_dir) + + # TODO: Customize these instructions for Windows + print() + if platform.system() == "Windows": + print("The Orca tools have been installed. Make sure the Orca tools are on your PATH by") + print("adding the following path to your system PATH variable:") + print() + print(bin_dir) + print() + print("You can do this in the Windows settings by searching for \"environment variables\".") + else: + print("The Orca tools have been installed. Make sure the Orca tools are on your PATH by") + print("adding the following to your shell config:") + print() + print(f"export PATH=\"{bin_dir}:$PATH\"") + print() diff --git a/scripts/log.py b/scripts/log.py new file mode 100644 index 0000000..83d80e7 --- /dev/null +++ b/scripts/log.py @@ -0,0 +1,85 @@ +import traceback +import subprocess + + +errors = [] +warnings = [] + + +class Entry: + def __init__(self, msg): + self.msgs = [msg] + + def more(self, *msgs): + if len(msgs) == 0: + msgs = [""] + for msg in msgs: + print(msg) + self.msgs.append(msg) + + +def log_error(msg): + msg = f"ERROR: {msg}" + print(msg) + entry = Entry(msg) + errors.append(entry) + return entry + + +def log_warning(msg): + msg = f"WARNING: {msg}" + print(msg) + entry = Entry(msg) + warnings.append(entry) + return entry + + +def log_finish(success): + if success and len(errors) + len(warnings) == 0: + print("Task completed successfully.") + return + + print() + + result_str = "succeeded" if success else "failed" + errors_str = "1 error" if len(errors) == 1 else f"{len(errors)} errors" + warnings_str = "1 warning" if len(warnings) == 1 else f"{len(warnings)} warnings" + + if len(errors) > 0 and len(warnings) > 0: + print(f"Task {result_str} with {errors_str} and {warnings_str}:") + for entry in warnings: + print("\n".join(entry.msgs)) + for entry in errors: + print("\n".join(entry.msgs)) + elif len(errors) > 0: + print(f"Task {result_str} with {errors_str}:") + for entry in errors: + print("\n".join(entry.msgs)) + elif len(warnings) > 0: + print(f"Task {result_str} with {warnings_str}:") + for entry in warnings: + print("\n".join(entry.msgs)) + + +def shellish(func): + def shellfunc(*args, **kwargs): + exitcode = 0 + try: + func(*args, **kwargs) + except subprocess.CalledProcessError as err: + msg = log_error(f"The following command failed with code {err.returncode}:") + msg.more(" ".join(err.cmd)) + exitcode = err.returncode + except SystemExit as err: + exitcode = err.code + except Exception as err: + log_error(err) + print(traceback.format_exc()) + exitcode = 1 + except: + print("something went REALLY wrong and also Ben does not know how to handle Python errors") + exitcode = 1 + finally: + log_finish(exitcode == 0) + exit(exitcode) + return shellfunc diff --git a/scripts/orca.py b/scripts/orca.py new file mode 100644 index 0000000..f161b05 --- /dev/null +++ b/scripts/orca.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +import argparse + +from .bundle import attach_bundle_commands +from .dev import attach_dev_commands + +parser = argparse.ArgumentParser() + +subparsers = parser.add_subparsers(required=True, title='commands') +attach_bundle_commands(subparsers) +attach_dev_commands(subparsers) + +args = parser.parse_args() +args.func(args) diff --git a/scripts/utils.py b/scripts/utils.py new file mode 100644 index 0000000..72483dc --- /dev/null +++ b/scripts/utils.py @@ -0,0 +1,20 @@ +import glob +import os +import subprocess +import traceback + +from contextlib import contextmanager + +@contextmanager +def pushd(new_dir): + previous_dir = os.getcwd() + os.chdir(new_dir) + try: + yield + finally: + os.chdir(previous_dir) + + +def removeall(dir): + [os.remove(f) for f in glob.iglob("{}/*".format(dir), recursive=True)] + os.removedirs(dir)