/*
This is heavily based (read: copy pasted + some modifications) on Thekla's module
MacOS_Bundler.jai, released with the jai compiler.
Usage:
*/
#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;
}