/* This is heavily based (read: copy pasted + some modifications) on Thekla's module MacOS_Bundler.jai, released with the jai compiler. For usage code, see package_macos_example.jai This routine creates a .app file with the following structure: output_path/ Entitlements.xcent .app/ Contents/ Info.plist MacOS/ <-- the executable, copied from args.exe_path ...copied in args.dynamic_libs Resources/ icon_data.icns ...the contents of args.asset_directory */ #import "Basic"; #import "String"; #import "Process"; #import "File"; #import "Compiler"; #import "POSIX"; FU :: #import "File_Utilities"; Args :: struct { exe_path: string; output_path: string; output_name: string; asset_directory: string; dynamic_libs: []string; icon_file_path: string; debug: bool; }; package_for_macos :: (args: Args) -> error: bool { success := true; exe_name := path_filename(args.exe_path); exe_path := path_strip_filename(args.exe_path); app_bundle_name := tprint("%/%.app", args.output_path, args.output_name); resources_path := tprint("%/Contents/Resources", app_bundle_name); macos_path := tprint("%/Contents/MacOS", app_bundle_name); { // Make Bundle Directories success &= make_directory_if_it_does_not_exist(app_bundle_name, true); success &= make_directory_if_it_does_not_exist(tprint("%/Contents", app_bundle_name), true); success &= make_directory_if_it_does_not_exist(macos_path, true); success &= make_directory_if_it_does_not_exist(resources_path, true); if !success print("Unable to make directory structure\n"); } // Copy Assets if success { if args.asset_directory { success = copy_directory(args.asset_directory, resources_path); } if !success print("Unable to copy asset_directory\n"); } out_exe_path := sprint("%/%", macos_path, args.output_name); if success { success = copy_file(args.exe_path, out_exe_path); chmod(temp_c_string(out_exe_path), 0x1FF); // add execute permissions if !success print("Unable to copy executable to %\n", out_exe_path); } // Copy Dynamic Libs if success { for args.dynamic_libs { basename := path_filename(it); dst := sprint("%/%", macos_path, basename); success = copy_file(it, dst); } } icon_data_file_name: string; if success { if args.icon_file_path { icon_data_file_name = "icon_data.icns"; icns_output_path := tprint("%/%", resources_path, icon_data_file_name); success = convert_image_to_icns(args.icon_file_path, icns_output_path); } } // Write plist if success { builder: String_Builder; plist_start(*builder, PLIST_HEADER); if icon_data_file_name { success := plist_write_key_value(*builder, "CFBundleIconFile", icon_data_file_name); assert(success); } plist_end(*builder); output := builder_to_string(*builder); success = write_entire_file(tprint("%/Contents/Info.plist", app_bundle_name), output); free(output); } // Write Entitlements if success { builder: String_Builder; plist_start(*builder, ENTITLEMENTS_HEADER); if args.debug { plist_write_key_value(*builder, "com.apple.security.get-task-allow", true); } plist_end(*builder); output := builder_to_string(*builder); success = write_entire_file(tprint("%/Entitlements.xcent", args.output_path), output); free(output); } return !success; } convert_image_to_icns :: (input_path: string, output_path: string) -> bool { // @TODO verify if the input image is 128x128 256x256 or 512x512 and scale the image accordingly if not // or at least give an error process_result, output, error := run_command("sips", "-s", "format", "icns", input_path, "--out", output_path); return process_result.exit_code == 0; } /////////////////////////////////////////////////// // Plist Writing plist_start :: (builder: *String_Builder, header: string) { append(builder, header); append(builder, "\n"); append(builder, "\n"); } plist_end :: (builder: *String_Builder) { append(builder, "\n"); append(builder, "\n"); } plist_write_key_value :: (builder: *String_Builder, key: string, value: Any) -> success: bool { plist_write_key(builder, key); if (value.type.type == Type_Info_Tag.BOOL) plist_write_bool(builder, << cast(*bool) value.value_pointer); else if (value.type.type == Type_Info_Tag.STRING) plist_write_string(builder, << cast(*string) value.value_pointer); else return false; return true; } plist_write_key :: (builder: *String_Builder, key: string) { append(builder, ""); append(builder, key); append(builder, ""); } plist_write_string :: (builder: *String_Builder, str: string) { append(builder, "\n"); append(builder, str); append(builder, "\n"); } plist_write_bool :: (builder: *String_Builder, value: bool) { print_to_builder(builder, "<%/>", value); } #scope_file PLIST_HEADER :: #string DONE DONE ENTITLEMENTS_HEADER :: #string DONE DONE /////////////////////////////////////////////////////////////// // Utilities copy_directory :: (from: string, to: string) -> success: bool { User_Data :: struct { from: string; to: string; success: *bool; } success: bool = true; ud := User_Data.{ from = from, to = to, success = *success, }; visitor :: (info: *FU.File_Visit_Info, data: User_Data) { dest_name := advance(info.full_name, data.from.count); dest_name = concat(data.to, dest_name); if info.is_directory { << data.success &= make_directory_if_it_does_not_exist(dest_name); } else { << data.success &= copy_file(info.full_name, dest_name); } } FU.visit_files(from, true, ud, visitor, visit_directories = true); return success; } copy_file :: (from: string, to: string) -> success: bool { success := false; data: string; data, success = read_entire_file(from); if success success = write_entire_file(to, data); return success; } concat :: (strings: .. string, separator: string = "") -> string { len := 0; for strings len += it.count; len += separator.count * (strings.count - 1); result := make_string(len); at := 0; for strings { memcpy(result.data + at, it.data, it.count); if it_index < strings.count - 1 { memcpy(result.data + at + it.count, separator.data, separator.count); } at += it.count + separator.count; } return result; } make_string :: (count: int) -> string { assert(count >= 0); if !count return ""; s: string = ---; s.data = alloc(count); s.count = count; return s; }