282 lines
7.0 KiB
Plaintext
282 lines
7.0 KiB
Plaintext
/*
|
|
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
|
|
<output_name>.app/
|
|
Contents/
|
|
Info.plist
|
|
MacOS/
|
|
<output_name> <-- 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, "<plist>\n");
|
|
append(builder, "<dict>\n");
|
|
}
|
|
|
|
plist_end :: (builder: *String_Builder)
|
|
{
|
|
append(builder, "</dict>\n");
|
|
append(builder, "</plist>\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, "<key>");
|
|
append(builder, key);
|
|
append(builder, "</key>");
|
|
}
|
|
|
|
plist_write_string :: (builder: *String_Builder, str: string)
|
|
{
|
|
append(builder, "<string>\n");
|
|
append(builder, str);
|
|
append(builder, "</string>\n");
|
|
}
|
|
|
|
plist_write_bool :: (builder: *String_Builder, value: bool)
|
|
{
|
|
print_to_builder(builder, "<%/>", value);
|
|
}
|
|
|
|
#scope_file
|
|
|
|
PLIST_HEADER :: #string DONE
|
|
<?xml version="1.0" ?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
DONE
|
|
|
|
ENTITLEMENTS_HEADER :: #string DONE
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
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;
|
|
}
|