Browse Source

Merge pull request #4970 from odin-lang/bill/android-subtarget

Very Very Rudimentary Support for Android
gingerBill 5 months ago
parent
commit
edf97bdb03
7 changed files with 683 additions and 76 deletions
  1. 174 9
      src/build_settings.cpp
  2. 1 0
      src/checker.cpp
  3. 156 44
      src/linker.cpp
  4. 70 11
      src/main.cpp
  5. 209 0
      src/package_command.cpp
  6. 64 12
      src/path.cpp
  7. 9 0
      src/string.cpp

+ 174 - 9
src/build_settings.cpp

@@ -171,6 +171,7 @@ struct TargetMetrics {
 enum Subtarget : u32 {
 	Subtarget_Default,
 	Subtarget_iOS,
+	Subtarget_Android,
 
 	Subtarget_COUNT,
 };
@@ -178,6 +179,7 @@ enum Subtarget : u32 {
 gb_global String subtarget_strings[Subtarget_COUNT] = {
 	str_lit(""),
 	str_lit("ios"),
+	str_lit("android"),
 };
 
 
@@ -204,20 +206,22 @@ enum BuildModeKind {
 	BuildMode_COUNT,
 };
 
-enum CommandKind : u32 {
+enum CommandKind : u64 {
 	Command_run             = 1<<0,
 	Command_build           = 1<<1,
-	Command_check           = 1<<3,
-	Command_doc             = 1<<5,
-	Command_version         = 1<<6,
-	Command_test            = 1<<7,
+	Command_check           = 1<<2,
+	Command_doc             = 1<<3,
+	Command_version         = 1<<4,
+	Command_test            = 1<<5,
 	
-	Command_strip_semicolon = 1<<8,
-	Command_bug_report      = 1<<9,
+	Command_strip_semicolon = 1<<6,
+	Command_bug_report      = 1<<7,
+
+	Command_package_android = 1<<8,
 
 	Command__does_check = Command_run|Command_build|Command_check|Command_doc|Command_test|Command_strip_semicolon,
 	Command__does_build = Command_run|Command_build|Command_test,
-	Command_all = ~(u32)0,
+	Command_all = ~(CommandKind)0,
 };
 
 gb_global char const *odin_command_strings[32] = {
@@ -228,6 +232,8 @@ gb_global char const *odin_command_strings[32] = {
 	"version",
 	"test",
 	"strip-semicolon",
+	"",
+	"package-android",
 };
 
 
@@ -527,6 +533,22 @@ struct BuildContext {
 
 	String minimum_os_version_string;
 	bool   minimum_os_version_string_given;
+
+
+	int    ODIN_ANDROID_API_LEVEL;
+
+	String ODIN_ANDROID_SDK;
+
+	String ODIN_ANDROID_NDK;
+	String ODIN_ANDROID_NDK_TOOLCHAIN;
+	String ODIN_ANDROID_NDK_TOOLCHAIN_LIB;
+	String ODIN_ANDROID_NDK_TOOLCHAIN_LIB_LEVEL;
+	String ODIN_ANDROID_NDK_TOOLCHAIN_SYSROOT;
+
+	String ODIN_ANDROID_JAR_SIGNER;
+	String android_keystore;
+	String android_keystore_alias;
+	String android_manifest;
 };
 
 gb_global BuildContext build_context = {0};
@@ -946,6 +968,14 @@ gb_internal bool is_arch_x86(void) {
 gb_global String const WIN32_SEPARATOR_STRING = {cast(u8 *)"\\", 1};
 gb_global String const NIX_SEPARATOR_STRING   = {cast(u8 *)"/",  1};
 
+gb_global String const SEPARATOR_STRING =
+#if defined(GB_SYSTEM_WINDOWS)
+	WIN32_SEPARATOR_STRING;
+#else
+	NIX_SEPARATOR_STRING;
+#endif
+
+
 gb_global String const WASM_MODULE_NAME_SEPARATOR = str_lit("..");
 
 gb_internal String internal_odin_root_dir(void);
@@ -1461,6 +1491,103 @@ gb_internal bool has_ansi_terminal_colours(void) {
 	return build_context.has_ansi_terminal_colours && !json_errors();
 }
 
+gb_internal void init_android_values(bool with_sdk) {
+	auto *bc = &build_context;
+	{ // Android SDK/API Level
+		String default_level = str_lit("34");
+		if (!bc->minimum_os_version_string_given) {
+			bc->minimum_os_version_string = default_level;
+		}
+		BigInt level = {};
+		bool success = false;
+		big_int_from_string(&level, bc->minimum_os_version_string, &success);
+		if (!success) {
+			gb_printf_err("Warning: Invalid -minimum-os-version:%.*s for -subtarget:Android, defaulting to %.*s\n", LIT(bc->minimum_os_version_string), LIT(default_level));
+			bc->minimum_os_version_string = default_level;
+			big_int_from_string(&level, bc->minimum_os_version_string, &success);
+			GB_ASSERT(success);
+		}
+
+		i64 new_level = big_int_to_i64(&level);
+
+		if (new_level >= 21) {
+			bc->ODIN_ANDROID_API_LEVEL = cast(int)new_level;
+		} else {
+			gb_printf_err("Warning: Invalid -minimum-os-version:%.*s for -subtarget:Android, defaulting to %.*s\n", LIT(bc->minimum_os_version_string), LIT(default_level));
+			bc->ODIN_ANDROID_API_LEVEL = atoi(cast(char const *)default_level.text);
+		}
+	}
+	bc->ODIN_ANDROID_NDK           = normalize_path(permanent_allocator(), make_string_c(gb_get_env("ODIN_ANDROID_NDK", permanent_allocator())), NIX_SEPARATOR_STRING);
+	bc->ODIN_ANDROID_NDK_TOOLCHAIN = normalize_path(permanent_allocator(), make_string_c(gb_get_env("ODIN_ANDROID_NDK_TOOLCHAIN", permanent_allocator())), NIX_SEPARATOR_STRING);
+	bc->ODIN_ANDROID_SDK           = normalize_path(permanent_allocator(), make_string_c(gb_get_env("ODIN_ANDROID_SDK", permanent_allocator())), NIX_SEPARATOR_STRING);
+
+	#if defined(GB_SYSTEM_WINDOWS)
+		if (bc->ODIN_ANDROID_SDK.len == 0) {
+			bc->ODIN_ANDROID_SDK = normalize_path(permanent_allocator(),
+				path_to_fullpath(permanent_allocator(), str_lit("%LocalAppData%/Android/Sdk"), nullptr),
+				NIX_SEPARATOR_STRING);
+		}
+	#endif
+
+	if (bc->ODIN_ANDROID_NDK.len != 0 && bc->ODIN_ANDROID_NDK_TOOLCHAIN.len == 0) {
+		String arch = str_lit("x86_64");
+		#if defined (GB_CPU_ARM)
+			// TODO(bill): this is a complete guess
+			arch = str_lit("aarch64");
+		#endif
+		#if defined(GB_SYSTEM_WINDOWS)
+			bc->ODIN_ANDROID_NDK_TOOLCHAIN = concatenate4_strings(temporary_allocator(), bc->ODIN_ANDROID_NDK, str_lit("toolchains/llvm/prebuilt/"), str_lit("windows-"), arch);
+		#elif defined(GB_SYSTEM_OSX)
+			// TODO(bill): is this name even correct?
+			bc->ODIN_ANDROID_NDK_TOOLCHAIN = concatenate4_strings(temporary_allocator(), bc->ODIN_ANDROID_NDK, str_lit("toolchains/llvm/prebuilt/"), str_lit("darwin-"), arch);
+		#elif defined(GB_SYSTEM_LINUX)
+			bc->ODIN_ANDROID_NDK_TOOLCHAIN = concatenate4_strings(temporary_allocator(), bc->ODIN_ANDROID_NDK, str_lit("toolchains/llvm/prebuilt/"), str_lit("linux-"), arch);
+		#endif
+
+		bc->ODIN_ANDROID_NDK_TOOLCHAIN = normalize_path(permanent_allocator(), bc->ODIN_ANDROID_NDK_TOOLCHAIN, NIX_SEPARATOR_STRING);
+	}
+
+	if (bc->ODIN_ANDROID_NDK.len == 0 && !with_sdk)  {
+		gb_printf_err("Error: ODIN_ANDROID_NDK not set");
+		gb_exit(1);
+
+	}
+
+	if (bc->ODIN_ANDROID_NDK_TOOLCHAIN.len == 0 && !with_sdk)  {
+		gb_printf_err("Error: ODIN_ANDROID_NDK not set");
+		gb_exit(1);
+	}
+
+	bc->ODIN_ANDROID_NDK_TOOLCHAIN_LIB = concatenate_strings(permanent_allocator(), bc->ODIN_ANDROID_NDK_TOOLCHAIN, str_lit("sysroot/usr/lib/aarch64-linux-android/"));
+
+	char buf[32] = {};
+	gb_snprintf(buf, gb_size_of(buf), "%d/", bc->ODIN_ANDROID_API_LEVEL);
+	bc->ODIN_ANDROID_NDK_TOOLCHAIN_LIB_LEVEL = concatenate_strings(permanent_allocator(), bc->ODIN_ANDROID_NDK_TOOLCHAIN_LIB, make_string_c(buf));
+
+	bc->ODIN_ANDROID_NDK_TOOLCHAIN_SYSROOT = concatenate_strings(permanent_allocator(), bc->ODIN_ANDROID_NDK_TOOLCHAIN, str_lit("sysroot/"));
+
+
+	bc->ODIN_ANDROID_JAR_SIGNER = normalize_path(permanent_allocator(), make_string_c(gb_get_env("ODIN_ANDROID_JAR_SIGNER", permanent_allocator())), NIX_SEPARATOR_STRING);
+	if (with_sdk) {
+		if (bc->ODIN_ANDROID_SDK.len == 0)  {
+			gb_printf_err("Error: ODIN_ANDROID_SDK not set, which is required for -build-mode:executable for -subtarget:android");
+			gb_exit(1);
+		}
+		if (bc->ODIN_ANDROID_JAR_SIGNER.len == 0) {
+			gb_printf_err("Error: ODIN_ANDROID_JAR_SIGNER not set, which is required for -build-mode:executable for -subtarget:android");
+			gb_exit(1);
+		}
+		if (bc->android_keystore.len == 0) {
+			gb_printf_err("Error: -android-keystore:<string> has not been set\n");
+			gb_exit(1);
+		}
+		if (bc->android_keystore_alias.len == 0) {
+			gb_printf_err("Error: -android-keystore_alias:<string> has not been set\n");
+			gb_exit(1);
+		}
+	}
+}
+
 gb_internal bool has_asm_extension(String const &path) {
 	String ext = path_extension(path);
 	if (ext == ".asm") {
@@ -1652,6 +1779,15 @@ gb_internal void init_build_context(TargetMetrics *cross_target, Subtarget subta
 		default:
 			GB_PANIC("Unknown architecture for darwin");
 		}
+	} else if (metrics->os == TargetOs_linux && subtarget == Subtarget_Android) {
+		switch (metrics->arch) {
+		case TargetArch_arm64:
+			bc->metrics.target_triplet = str_lit("aarch64-none-linux-android");
+			bc->reloc_mode = RelocMode_PIC;
+			break;
+		default:
+			GB_PANIC("Unknown architecture for -subtarget:android");
+		}
 	}
 
 	if (bc->metrics.os == TargetOs_windows) {
@@ -1706,6 +1842,8 @@ gb_internal void init_build_context(TargetMetrics *cross_target, Subtarget subta
 		if (subtarget == Subtarget_Default) {
 			bc->metrics.target_triplet = concatenate_strings(permanent_allocator(), bc->metrics.target_triplet, bc->minimum_os_version_string);
 		}
+	} else if (selected_subtarget == Subtarget_Android) {
+		init_android_values(bc->build_mode == BuildMode_Executable);
 	}
 
 	if (!bc->custom_optimization_level) {
@@ -1749,6 +1887,30 @@ gb_internal void init_build_context(TargetMetrics *cross_target, Subtarget subta
 	if (bc->metrics.os == TargetOs_freestanding) {
 		bc->ODIN_DEFAULT_TO_NIL_ALLOCATOR = !bc->ODIN_DEFAULT_TO_PANIC_ALLOCATOR;
 	}
+
+	if (subtarget == Subtarget_Android) {
+		switch (build_context.build_mode) {
+		case BuildMode_DynamicLibrary:
+		case BuildMode_Object:
+		case BuildMode_Assembly:
+		case BuildMode_LLVM_IR:
+			break;
+		default:
+		case BuildMode_Executable:
+		case BuildMode_StaticLibrary:
+			if ((build_context.command_kind & Command__does_build) != 0) {
+				gb_printf_err("Unsupported -build-mode for -subtarget:android\n");
+				gb_printf_err("\tCurrently only supporting: \n");
+				// gb_printf_err("\t\texe\n");
+				gb_printf_err("\t\tshared\n");
+				gb_printf_err("\t\tobject\n");
+				gb_printf_err("\t\tassembly\n");
+				gb_printf_err("\t\tllvm-ir\n");
+				gb_exit(1);
+			}
+			break;
+		}
+	}
 }
 
 #if defined(GB_SYSTEM_WINDOWS)
@@ -1947,7 +2109,10 @@ gb_internal bool init_build_paths(String init_filename) {
 		output_extension = make_string(nullptr, 0);
 		String const single_file_extension = str_lit(".odin");
 
-		if (build_context.metrics.os == TargetOs_windows) {
+		if (selected_subtarget == Subtarget_Android) {
+			// NOTE(bill): It's always shared!
+			output_extension = STR_LIT("so");
+		} else if (build_context.metrics.os == TargetOs_windows) {
 			output_extension = STR_LIT("exe");
 		} else if (build_context.cross_compiling && selected_target_metrics->metrics == &target_essence_amd64) {
 			// Do nothing: we don't want the .bin extension

+ 1 - 0
src/checker.cpp

@@ -1149,6 +1149,7 @@ gb_internal void init_universal(void) {
 		GlobalEnumValue values[Subtarget_COUNT] = {
 			{"Default", Subtarget_Default},
 			{"iOS",     Subtarget_iOS},
+			{"Android", Subtarget_Android},
 		};
 
 		auto fields = add_global_enum_type(str_lit("Odin_Platform_Subtarget_Type"), values, gb_count_of(values));

+ 156 - 44
src/linker.cpp

@@ -7,19 +7,15 @@ struct LinkerData {
 	Array<String> output_temp_paths;
 	String   output_base;
 	String   output_name;
-#if defined(GB_SYSTEM_OSX)
-	b8       needs_system_library_linked;
-#endif
+	bool     needs_system_library_linked;
 };
 
 gb_internal i32 system_exec_command_line_app(char const *name, char const *fmt, ...);
 gb_internal bool system_exec_command_line_app_output(char const *command, gbString *output);
 
-#if defined(GB_SYSTEM_OSX)
 gb_internal void linker_enable_system_library_linking(LinkerData *ld) {
-	ld->needs_system_library_linked = 1;
+	ld->needs_system_library_linked = true;
 }
-#endif
 
 gb_internal void linker_data_init(LinkerData *ld, CheckerInfo *info, String const &init_fullpath) {
 	gbAllocator ha = heap_allocator();
@@ -28,9 +24,7 @@ gb_internal void linker_data_init(LinkerData *ld, CheckerInfo *info, String cons
 	array_init(&ld->foreign_libraries,   ha, 0, 1024);
 	ptr_set_init(&ld->foreign_libraries_set, 1024);
 
-#if defined(GB_SYSTEM_OSX)
-	ld->needs_system_library_linked = 0;
-#endif 
+	ld->needs_system_library_linked = false;
 
 	if (build_context.out_filepath.len == 0) {
 		ld->output_name = remove_directory_from_path(init_fullpath);
@@ -136,6 +130,9 @@ gb_internal i32 linker_stage(LinkerData *gen) {
 		return result;
 	}
 
+	bool is_cross_linking = false;
+	bool is_android = false;
+
 	if (build_context.cross_compiling && selected_target_metrics->metrics == &target_essence_amd64) {
 #if defined(GB_SYSTEM_UNIX)
 		result = system_exec_command_line_app("linker", "x86_64-essence-gcc \"%.*s.o\" -o \"%.*s\" %.*s %.*s",
@@ -147,22 +144,29 @@ gb_internal i32 linker_stage(LinkerData *gen) {
 		);
 #endif
 	} else if (build_context.cross_compiling && build_context.different_os) {
-		gb_printf_err("Linking for cross compilation for this platform is not yet supported (%.*s %.*s)\n",
-			LIT(target_os_names[build_context.metrics.os]),
-			LIT(target_arch_names[build_context.metrics.arch])
-		);
-		build_context.keep_object_files = true;
+		switch (selected_subtarget) {
+		case Subtarget_Android:
+			is_cross_linking = true;
+			is_android = true;
+			goto try_cross_linking;
+		default:
+			gb_printf_err("Linking for cross compilation for this platform is not yet supported (%.*s %.*s)\n",
+				LIT(target_os_names[build_context.metrics.os]),
+				LIT(target_arch_names[build_context.metrics.arch])
+			);
+			build_context.keep_object_files = true;
+			break;
+		}
 	} else {
+try_cross_linking:;
+
 	#if defined(GB_SYSTEM_WINDOWS)
-		bool is_windows = true;
+		bool is_windows = build_context.metrics.os == TargetOs_windows;
 	#else
 		bool is_windows = false;
 	#endif
-	#if defined(GB_SYSTEM_OSX)
-		bool is_osx = true;
-	#else
-		bool is_osx = false;
-	#endif
+
+		bool is_osx = build_context.metrics.os == TargetOs_darwin;
 
 
 		if (is_windows) {
@@ -414,23 +418,27 @@ gb_internal i32 linker_stage(LinkerData *gen) {
 		} else {
 			timings_start_section(timings, str_lit("ld-link"));
 
+			int const ODIN_ANDROID_API_LEVEL = build_context.ODIN_ANDROID_API_LEVEL;
+
+			String ODIN_ANDROID_NDK                     = build_context.ODIN_ANDROID_NDK;
+			String ODIN_ANDROID_NDK_TOOLCHAIN           = build_context.ODIN_ANDROID_NDK_TOOLCHAIN;
+			String ODIN_ANDROID_NDK_TOOLCHAIN_LIB       = build_context.ODIN_ANDROID_NDK_TOOLCHAIN_LIB;
+			String ODIN_ANDROID_NDK_TOOLCHAIN_LIB_LEVEL = build_context.ODIN_ANDROID_NDK_TOOLCHAIN_LIB_LEVEL;
+			String ODIN_ANDROID_NDK_TOOLCHAIN_SYSROOT   = build_context.ODIN_ANDROID_NDK_TOOLCHAIN_SYSROOT;
+
 			// Link using `clang`, unless overridden by `ODIN_CLANG_PATH` environment variable.
 			const char* clang_path = gb_get_env("ODIN_CLANG_PATH", permanent_allocator());
 			if (clang_path == NULL) {
 				clang_path = "clang";
 			}
 
-			// NOTE(vassvik): get cwd, for used for local shared libs linking, since those have to be relative to the exe
-			char cwd[256];
-			#if !defined(GB_SYSTEM_WINDOWS)
-			getcwd(&cwd[0], 256);
-			#endif
-			//printf("%s\n", cwd);
-
 			// NOTE(vassvik): needs to add the root to the library search paths, so that the full filenames of the library
 			//                files can be passed with -l:
-			gbString lib_str = gb_string_make(heap_allocator(), "-L/");
+			gbString lib_str = gb_string_make(heap_allocator(), "");
 			defer (gb_string_free(lib_str));
+			#if !defined(GB_SYSTEM_WINDOWS)
+				lib_str = gb_string_appendc(lib_str, "-L/ ");
+			#endif
 			
 			StringSet asm_files = {};
 			string_set_init(&asm_files, 64);
@@ -496,19 +504,20 @@ gb_internal i32 linker_stage(LinkerData *gen) {
 						}
 
 						String obj_format;
-					#if defined(GB_ARCH_64_BIT)
-						if (is_osx) {
-							obj_format = str_lit("macho64");
-						} else {
-							obj_format = str_lit("elf64");
-						}
-					#elif defined(GB_ARCH_32_BIT)
-						if (is_osx) {
-							obj_format = str_lit("macho32");
+						if (build_context.metrics.ptr_size == 8) {
+							if (is_osx) {
+								obj_format = str_lit("macho64");
+							} else {
+								obj_format = str_lit("elf64");
+							}
 						} else {
-							obj_format = str_lit("elf32");
+							GB_ASSERT(build_context.metrics.ptr_size == 4);
+							if (is_osx) {
+								obj_format = str_lit("macho32");
+							} else {
+								obj_format = str_lit("elf32");
+							}
 						}
-					#endif // GB_ARCH_*_BIT
 
 						if (build_context.metrics.arch == TargetArch_riscv64) {
 							result = system_exec_command_line_app("clang",
@@ -618,6 +627,78 @@ gb_internal i32 linker_stage(LinkerData *gen) {
 
 			gbString object_files = gb_string_make(heap_allocator(), "");
 			defer (gb_string_free(object_files));
+
+
+			if (is_android) { // NOTE(bill): glue code needed for Android
+				TIME_SECTION("Android Native App Glue Compile");
+
+				String android_glue_object = {};
+				String android_glue_static_lib = {};
+
+				char hash_buf[64] = {};
+				gb_snprintf(hash_buf, gb_size_of(hash_buf), "%p", &hash_buf);
+				String hash = make_string_c(hash_buf);
+
+				String temp_dir = normalize_path(temporary_allocator(), temporary_directory(temporary_allocator()), NIX_SEPARATOR_STRING);
+				android_glue_object = concatenate4_strings(temporary_allocator(), temp_dir, str_lit("android_native_app_glue-"), hash, str_lit(".o"));
+				android_glue_static_lib = concatenate4_strings(permanent_allocator(), temp_dir, str_lit("libandroid_native_app_glue-"), hash, str_lit(".a"));
+
+				gbString glue = gb_string_make(heap_allocator(), clang_path);
+				defer (gb_string_free(glue));
+
+				glue = gb_string_append_fmt(glue, " --target=aarch64-linux-android%d ", ODIN_ANDROID_API_LEVEL);
+				glue = gb_string_appendc(glue, "-c \"");
+				glue = gb_string_append_length(glue, ODIN_ANDROID_NDK.text, ODIN_ANDROID_NDK.len);
+				glue = gb_string_appendc(glue, "sources/android/native_app_glue/android_native_app_glue.c");
+				glue = gb_string_appendc(glue, "\" ");
+				glue = gb_string_appendc(glue, "-o \"");
+				glue = gb_string_append_length(glue, android_glue_object.text, android_glue_object.len);
+				glue = gb_string_appendc(glue, "\" ");
+
+				glue = gb_string_appendc(glue, "\"-I");
+				glue = gb_string_append_length(glue, ODIN_ANDROID_NDK_TOOLCHAIN.text, ODIN_ANDROID_NDK_TOOLCHAIN.len);
+				glue = gb_string_appendc(glue, "sysroot/usr/include/");
+				glue = gb_string_appendc(glue, "\" ");
+
+				glue = gb_string_appendc(glue, "\"-I");
+				glue = gb_string_append_length(glue, ODIN_ANDROID_NDK_TOOLCHAIN.text, ODIN_ANDROID_NDK_TOOLCHAIN.len);
+				glue = gb_string_appendc(glue, "sysroot/usr/include/aarch64-linux-android/");
+				glue = gb_string_appendc(glue, "\" ");
+
+
+				glue = gb_string_appendc(glue, "-Wno-macro-redefined ");
+
+				result = system_exec_command_line_app("android-native-app-glue-compile", glue);
+				if (result) {
+					return result;
+				}
+
+				TIME_SECTION("Android Native App Glue ar");
+
+				gbString ar = gb_string_make_length(heap_allocator(), ODIN_ANDROID_NDK_TOOLCHAIN.text, ODIN_ANDROID_NDK_TOOLCHAIN.len);
+				defer (gb_string_free(ar));
+
+				ar = gb_string_appendc(ar, "bin/llvm-ar");
+
+				ar = gb_string_appendc(ar, " rcs ");
+
+				ar = gb_string_appendc(ar, "\"");
+				ar = gb_string_append_length(ar, android_glue_static_lib.text, android_glue_static_lib.len);
+				ar = gb_string_appendc(ar, "\" ");
+
+				ar = gb_string_appendc(ar, "\"");
+				ar = gb_string_append_length(ar, android_glue_object.text, android_glue_object.len);
+				ar = gb_string_appendc(ar, "\" ");
+
+				result = system_exec_command_line_app("android-native-app-glue-ar", ar);
+				if (result) {
+					return result;
+				}
+
+				object_files = gb_string_append_fmt(object_files, "\"%.*s\" ", LIT(android_glue_static_lib));
+			}
+
+
 			for (String object_path : gen->output_object_paths) {
 				object_files = gb_string_append_fmt(object_files, "\"%.*s\" ", LIT(object_path));
 			}
@@ -661,7 +742,9 @@ gb_internal i32 linker_stage(LinkerData *gen) {
 					link_settings = gb_string_appendc(link_settings, "-Wl,-init,'_odin_entry_point' ");
 					link_settings = gb_string_appendc(link_settings, "-Wl,-fini,'_odin_exit_point' ");
 				}
-
+			} else if (is_android) {
+				// Always shared even in android!
+				link_settings = gb_string_appendc(link_settings, "-shared ");
 			}
 
 			if (build_context.build_mode == BuildMode_Executable && build_context.reloc_mode == RelocMode_PIC) {
@@ -670,6 +753,7 @@ gb_internal i32 linker_stage(LinkerData *gen) {
 				if (build_context.metrics.os != TargetOs_openbsd
 					&& build_context.metrics.os != TargetOs_haiku
 					&& build_context.metrics.arch != TargetArch_riscv64
+					&& !is_android
 				) {
 					// OpenBSD and Haiku default to PIE executable. do not pass -no-pie for it.
 					link_settings = gb_string_appendc(link_settings, "-no-pie ");
@@ -710,30 +794,53 @@ gb_internal i32 linker_stage(LinkerData *gen) {
 				platform_lib_str = gb_string_appendc(platform_lib_str, "-Wl,-z,nobtcfi ");
 			}
 
+			if (is_android) {
+				GB_ASSERT(ODIN_ANDROID_NDK_TOOLCHAIN_LIB.len != 0);
+				GB_ASSERT(ODIN_ANDROID_NDK_TOOLCHAIN_LIB_LEVEL.len != 0);
+				GB_ASSERT(ODIN_ANDROID_NDK_TOOLCHAIN_SYSROOT.len != 0);
+
+				platform_lib_str = gb_string_appendc(platform_lib_str, "\"-L");
+				platform_lib_str = gb_string_append_length(platform_lib_str, ODIN_ANDROID_NDK_TOOLCHAIN_LIB_LEVEL.text, ODIN_ANDROID_NDK_TOOLCHAIN_LIB_LEVEL.len);
+				platform_lib_str = gb_string_appendc(platform_lib_str, "\" ");
+
+				platform_lib_str = gb_string_appendc(platform_lib_str, "\"--sysroot=");
+				platform_lib_str = gb_string_append_length(platform_lib_str, ODIN_ANDROID_NDK_TOOLCHAIN_SYSROOT.text, ODIN_ANDROID_NDK_TOOLCHAIN_SYSROOT.len);
+				platform_lib_str = gb_string_appendc(platform_lib_str, "\" ");
+
+				link_settings = gb_string_appendc(link_settings, "-u ANativeActivity_onCreate ");
+			}
+
 			if (!build_context.no_rpath) {
 				// Set the rpath to the $ORIGIN/@loader_path (the path of the executable),
 				// so that dynamic libraries are looked for at that path.
 				if (build_context.metrics.os == TargetOs_darwin) {
 					link_settings = gb_string_appendc(link_settings, "-Wl,-rpath,@loader_path ");
 				} else {
-					link_settings = gb_string_appendc(link_settings, "-Wl,-rpath,\\$ORIGIN ");
+					if (is_android) {
+						// ignore
+					} else {
+						link_settings = gb_string_appendc(link_settings, "-Wl,-rpath,\\$ORIGIN ");
+					}
 				}
 			}
 
 			if (!build_context.no_crt) {
-				platform_lib_str = gb_string_appendc(platform_lib_str, "-lm ");
+				lib_str = gb_string_appendc(lib_str, "-lm ");
 				if (build_context.metrics.os == TargetOs_darwin) {
 					// NOTE: adding this causes a warning about duplicate libraries, I think it is
 					// automatically assumed/added by clang when you don't do `-nostdlib`.
-					// platform_lib_str = gb_string_appendc(platform_lib_str, "-lSystem ");
+					// lib_str = gb_string_appendc(lib_str, "-lSystem ");
 				} else {
-					platform_lib_str = gb_string_appendc(platform_lib_str, "-lc ");
+					lib_str = gb_string_appendc(lib_str, "-lc ");
 				}
 			}
 
 			gbString link_command_line = gb_string_make(heap_allocator(), clang_path);
 			defer (gb_string_free(link_command_line));
 
+			if (is_android) {
+				link_command_line = gb_string_append_fmt(link_command_line, " --target=aarch64-linux-android%d ", ODIN_ANDROID_API_LEVEL);
+			}
 			link_command_line = gb_string_appendc(link_command_line, " -Wno-unused-command-line-argument ");
 			link_command_line = gb_string_appendc(link_command_line, object_files);
 			link_command_line = gb_string_append_fmt(link_command_line, " -o \"%.*s\" ", LIT(output_filename));
@@ -743,6 +850,11 @@ gb_internal i32 linker_stage(LinkerData *gen) {
 			link_command_line = gb_string_append_fmt(link_command_line, " %.*s ", LIT(build_context.extra_linker_flags));
 			link_command_line = gb_string_append_fmt(link_command_line, " %s ", link_settings);
 
+
+			if (is_android) {
+				TIME_SECTION("Linking");
+			}
+
 			if (build_context.linker_choice == Linker_lld) {
 				link_command_line = gb_string_append_fmt(link_command_line, " -fuse-ld=lld");
 				result = system_exec_command_line_app("lld-link", link_command_line);

+ 70 - 11
src/main.cpp

@@ -74,6 +74,7 @@ gb_global Timings global_timings = {0};
 #include "cached.cpp"
 
 #include "linker.cpp"
+#include "package_command.cpp"
 
 #if defined(GB_SYSTEM_WINDOWS) && defined(ODIN_TILDE_BACKEND)
 #define ALLOW_TILDE 1
@@ -408,6 +409,10 @@ enum BuildFlagKind {
 	BuildFlag_Subsystem,
 #endif
 
+	BuildFlag_AndroidKeystore,
+	BuildFlag_AndroidKeystoreAlias,
+	BuildFlag_AndroidManifest,
+
 	BuildFlag_COUNT,
 };
 
@@ -426,12 +431,12 @@ struct BuildFlag {
 	BuildFlagKind      kind;
 	String             name;
 	BuildFlagParamKind param_kind;
-	u32                command_support;
+	u64                command_support;
 	bool               allow_multiple;
 };
 
 
-gb_internal void add_flag(Array<BuildFlag> *build_flags, BuildFlagKind kind, String name, BuildFlagParamKind param_kind, u32 command_support, bool allow_multiple=false) {
+gb_internal void add_flag(Array<BuildFlag> *build_flags, BuildFlagKind kind, String name, BuildFlagParamKind param_kind, u64 command_support, bool allow_multiple=false) {
 	BuildFlag flag = {kind, name, param_kind, command_support, allow_multiple};
 	array_add(build_flags, flag);
 }
@@ -567,7 +572,7 @@ gb_internal bool parse_build_flags(Array<String> args) {
 	add_flag(&build_flags, BuildFlag_Microarch,               str_lit("microarch"),                 BuildFlagParam_String,  Command__does_build);
 	add_flag(&build_flags, BuildFlag_TargetFeatures,          str_lit("target-features"),           BuildFlagParam_String,  Command__does_build);
 	add_flag(&build_flags, BuildFlag_StrictTargetFeatures,    str_lit("strict-target-features"),    BuildFlagParam_None,    Command__does_build);
-	add_flag(&build_flags, BuildFlag_MinimumOSVersion,        str_lit("minimum-os-version"),        BuildFlagParam_String,  Command__does_build);
+	add_flag(&build_flags, BuildFlag_MinimumOSVersion,        str_lit("minimum-os-version"),        BuildFlagParam_String,  Command__does_build | Command_package_android);
 
 	add_flag(&build_flags, BuildFlag_RelocMode,               str_lit("reloc-mode"),                BuildFlagParam_String,  Command__does_build);
 	add_flag(&build_flags, BuildFlag_DisableRedZone,          str_lit("disable-red-zone"),          BuildFlagParam_None,    Command__does_build);
@@ -624,9 +629,20 @@ gb_internal bool parse_build_flags(Array<String> args) {
 	add_flag(&build_flags, BuildFlag_Subsystem,               str_lit("subsystem"),                 BuildFlagParam_String,  Command__does_build);
 #endif
 
+	add_flag(&build_flags, BuildFlag_AndroidKeystore,         str_lit("android-keystore"),          BuildFlagParam_String,  Command_package_android);
+	add_flag(&build_flags, BuildFlag_AndroidKeystoreAlias,    str_lit("android-keystore-alias"),    BuildFlagParam_String,  Command_package_android);
+	add_flag(&build_flags, BuildFlag_AndroidManifest,         str_lit("android-manifest"),          BuildFlagParam_String,  Command_package_android);
+
 
-	GB_ASSERT(args.count >= 3);
-	Array<String> flag_args = array_slice(args, 3, args.count);
+	Array<String> flag_args = {};
+
+	if (build_context.command_kind == Command_package_android) {
+		GB_ASSERT(args.count >= 4);
+		flag_args = array_slice(args, 4, args.count);
+	} else {
+		GB_ASSERT(args.count >= 3);
+		flag_args = array_slice(args, 3, args.count);
+	}
 
 	bool set_flags[BuildFlag_COUNT] = {};
 
@@ -1105,8 +1121,9 @@ gb_internal bool parse_build_flags(Array<String> args) {
 								String str = value.value_string;
 								bool found = false;
 
-								if (selected_target_metrics->metrics->os != TargetOs_darwin) {
-									gb_printf_err("-subtarget can only be used with darwin based targets at the moment\n");
+								if (selected_target_metrics->metrics->os != TargetOs_darwin &&
+								    selected_target_metrics->metrics->os != TargetOs_linux ) {
+									gb_printf_err("-subtarget can only be used with darwin and linux based targets at the moment\n");
 									bad_flags = true;
 									break;
 								}
@@ -1637,6 +1654,20 @@ gb_internal bool parse_build_flags(Array<String> args) {
 						}
 					#endif
 
+						case BuildFlag_AndroidKeystore:
+							GB_ASSERT(value.kind == ExactValue_String);
+							build_context.android_keystore = value.value_string;
+							break;
+
+						case BuildFlag_AndroidKeystoreAlias:
+							GB_ASSERT(value.kind == ExactValue_String);
+							build_context.android_keystore_alias = value.value_string;
+							break;
+
+						case BuildFlag_AndroidManifest:
+							GB_ASSERT(value.kind == ExactValue_String);
+							build_context.android_manifest = value.value_string;
+							break;
 						}
 					}
 
@@ -1652,8 +1683,8 @@ gb_internal bool parse_build_flags(Array<String> args) {
 			gb_printf_err("'%.*s' is supported with the following commands:\n", LIT(name));
 			gb_printf_err("\t");
 			i32 count = 0;
-			for (u32 i = 0; i < 32; i++) {
-				if (found_bf.command_support & (1<<i)) {
+			for (u64 i = 0; i < 64; i++) {
+				if (found_bf.command_support & (1ull<<i)) {
 					if (count > 0) {
 						gb_printf_err(", ");
 					}
@@ -2236,6 +2267,10 @@ gb_internal void print_show_help(String const arg0, String command, String optio
 	} else if (command == "strip-semicolon") {
 		print_usage_line(1, "strip-semicolon");
 		print_usage_line(2, "Parses and type checks .odin file(s) and then removes unneeded semicolons from the entire project.");
+	} else if (command == "package")  {
+		print_usage_line(1, "package <platform>   Packages directory in a specific layout for that platform");
+		print_usage_line(2, "Supported platforms:");
+		print_usage_line(3, "android");
 	}
 
 	bool doc             = command == "doc";
@@ -2245,6 +2280,7 @@ gb_internal void print_show_help(String const arg0, String command, String optio
 	bool strip_semicolon = command == "strip-semicolon";
 	bool check_only      = command == "check" || strip_semicolon;
 	bool check           = run_or_build || check_only;
+	bool is_package      = command == "package";
 
 	if (command == "help") {
 		doc             = true;
@@ -2512,13 +2548,15 @@ gb_internal void print_show_help(String const arg0, String command, String optio
 		}
 	}
 
-	if (run_or_build) {
+	if (run_or_build || is_package) {
 		if (print_flag("-minimum-os-version:<string>")) {
 			print_usage_line(2, "Sets the minimum OS version targeted by the application.");
 			print_usage_line(2, "Default: -minimum-os-version:11.0.0");
 			print_usage_line(2, "Only used when target is Darwin, if given, linking mismatched versions will emit a warning.");
 		}
+	}
 
+	if (run_or_build) {
 		if (print_flag("-no-bounds-check")) {
 			print_usage_line(2, "Disables bounds checking program wide.");
 		}
@@ -3299,6 +3337,19 @@ int main(int arg_count, char const **arg_ptr) {
 			print_show_help(args[0], args[1], args[2]);
 			return 0;
 		}
+	} else if (command == "package") {
+		if (args.count < 4) {
+			usage(args[0]);
+			return 1;
+		}
+		if (args[2] == "android") {
+			build_context.command_kind = Command_package_android;
+		} else {
+			gb_printf_err("Unknown package command: '%.*s'\n", LIT(args[2]));
+			usage(args[0]);
+			return 1;
+		}
+		init_filename = args[3];
 	} else if (command == "root") {
 		gb_printf("%.*s", LIT(odin_root_dir()));
 		return 0;
@@ -3333,10 +3384,14 @@ int main(int arg_count, char const **arg_ptr) {
 			}
 
 			if (!single_file_package) {
-				gb_printf_err("ERROR: `%.*s %.*s` takes a package as its first argument.\n", LIT(args[0]), LIT(command));
+				gb_printf_err("ERROR: `%.*s %.*s` takes a package/directory as its first argument.\n", LIT(args[0]), LIT(command));
 				if (init_filename == "-file") {
 					gb_printf_err("Did you mean `%.*s %.*s <filename.odin> -file`?\n", LIT(args[0]), LIT(command));
 				} else {
+					if (!gb_file_exists(cast(const char*)init_filename.text)) {
+						gb_printf_err("The file '%.*s' was not found.\n", LIT(init_filename));
+						return 1;
+					}
 					gb_printf_err("Did you mean `%.*s %.*s %.*s -file`?\n", LIT(args[0]), LIT(command), LIT(init_filename));
 				}
 
@@ -3370,6 +3425,10 @@ int main(int arg_count, char const **arg_ptr) {
 		return 0;
 	}
 
+	if (command == "package") {
+		return package(init_filename);
+	}
+
 	// NOTE(bill): add 'shared' directory if it is not already set
 	if (!find_library_collection_path(str_lit("shared"), nullptr)) {
 		add_library_collection(str_lit("shared"),

+ 209 - 0
src/package_command.cpp

@@ -0,0 +1,209 @@
+i32 package_android(String init_directory);
+
+i32 package(String init_directory) {
+	switch (build_context.command_kind) {
+	case Command_package_android:
+		return package_android(init_directory);
+	}
+	gb_printf_err("Unknown odin package <platform>\n");
+	return 1;
+}
+
+
+i32 package_android(String original_init_directory) {
+	TEMPORARY_ALLOCATOR_GUARD();
+
+	i32 result = 0;
+	init_android_values(/*with_sdk*/true);
+
+	bool init_directory_ok = false;
+	String init_directory = path_to_fullpath(temporary_allocator(), original_init_directory, &init_directory_ok);
+	if (!init_directory_ok) {
+		gb_printf_err("Error: '%.*s' is not a valid directory", LIT(original_init_directory));
+		return 1;
+	}
+	init_directory = normalize_path(temporary_allocator(), init_directory, NIX_SEPARATOR_STRING);
+
+	int const ODIN_ANDROID_API_LEVEL = build_context.ODIN_ANDROID_API_LEVEL;
+
+	String android_sdk_build_tools = concatenate3_strings(temporary_allocator(),
+		build_context.ODIN_ANDROID_SDK, str_lit("build-tools"), NIX_SEPARATOR_STRING);
+
+	Array<FileInfo> list = {};
+	ReadDirectoryError rd_err = read_directory(android_sdk_build_tools, &list);
+	defer (array_free(&list));
+
+	switch (rd_err) {
+	case ReadDirectory_InvalidPath:
+		gb_printf_err("Invalid path: %.*s\n", LIT(android_sdk_build_tools));
+		return 1;
+	case ReadDirectory_NotExists:
+		gb_printf_err("Path does not exist: %.*s\n", LIT(android_sdk_build_tools));
+		return 1;
+	case ReadDirectory_Permission:
+		gb_printf_err("Unknown error whilst reading path %.*s\n", LIT(android_sdk_build_tools));
+		return 1;
+	case ReadDirectory_NotDir:
+		gb_printf_err("Expected a directory for a package, got a file: %.*s\n", LIT(android_sdk_build_tools));
+		return 1;
+	case ReadDirectory_Empty:
+		gb_printf_err("Empty directory: %.*s\n", LIT(android_sdk_build_tools));
+		return 1;
+	case ReadDirectory_Unknown:
+		gb_printf_err("Unknown error whilst reading path %.*s\n", LIT(android_sdk_build_tools));
+		return 1;
+	}
+
+	auto possible_valid_dirs = array_make<FileInfo>(heap_allocator(), 0, list.count);
+	defer (array_free(&possible_valid_dirs));
+
+
+	for (FileInfo fi : list) if (fi.is_dir) {
+		bool all_numbers = true;
+		for (isize i = 0; i < fi.name.len; i++) {
+			u8 c = fi.name[i];
+			if ('0' <= c && c <= '9') {
+				// true
+			} else if (i == 0) {
+				all_numbers = false;
+			} else if (c == '.') {
+				break;
+			} else {
+				all_numbers = false;
+			}
+		}
+
+		if (all_numbers) {
+			array_add(&possible_valid_dirs, fi);
+		}
+	}
+
+	if (possible_valid_dirs.count == 0) {
+		gb_printf_err("Unable to find any Android SDK/API Level in %.*s\n", LIT(android_sdk_build_tools));
+		return 1;
+	}
+
+	int *dir_numbers = gb_alloc_array(temporary_allocator(), int, possible_valid_dirs.count);
+
+	char buf[1024] = {};
+	for_array(i, possible_valid_dirs) {
+		FileInfo fi = possible_valid_dirs[i];
+		isize n = gb_min(gb_size_of(buf)-1, fi.name.len);
+		memcpy(buf, fi.name.text, n);
+		buf[n] = 0;
+
+		dir_numbers[i] = atoi(buf);
+	}
+
+	isize closest_number_idx = -1;
+	for (isize i = 0; i < possible_valid_dirs.count; i++) {
+		if (dir_numbers[i] >= ODIN_ANDROID_API_LEVEL) {
+			if (closest_number_idx < 0) {
+				closest_number_idx = i;
+			} else if (dir_numbers[i] < dir_numbers[closest_number_idx]) {
+				closest_number_idx = i;
+			}
+		}
+	}
+
+	if (closest_number_idx < 0) {
+		gb_printf_err("Unable to find any Android SDK/API Level in %.*s meeting the minimum API level of %d\n", LIT(android_sdk_build_tools), ODIN_ANDROID_API_LEVEL);
+		return 1;
+	}
+
+	String api_number = possible_valid_dirs[closest_number_idx].name;
+
+	android_sdk_build_tools = concatenate_strings(temporary_allocator(), android_sdk_build_tools, api_number);
+	String android_sdk_platforms = concatenate_strings(temporary_allocator(),
+		build_context.ODIN_ANDROID_SDK,
+		make_string_c(gb_bprintf("platforms/android-%d/", dir_numbers[closest_number_idx]))
+	);
+
+	android_sdk_build_tools = normalize_path(temporary_allocator(), android_sdk_build_tools, NIX_SEPARATOR_STRING);
+	android_sdk_platforms   = normalize_path(temporary_allocator(), android_sdk_platforms,   NIX_SEPARATOR_STRING);
+
+	gbString cmd = gb_string_make(heap_allocator(), "");
+	defer (gb_string_free(cmd));
+
+
+	String current_directory = normalize_path(temporary_allocator(), get_working_directory(temporary_allocator()), NIX_SEPARATOR_STRING);
+	defer (set_working_directory(current_directory));
+
+	if (current_directory.len != 0) {
+		bool ok = set_working_directory(init_directory);
+		if (!ok) {
+			gb_printf_err("Error: Unable to currectly set the current working directory to '%.*s'\n", LIT(init_directory));
+		}
+	}
+
+	String output_filename = str_lit("test");
+	String output_apk = path_remove_extension(output_filename);
+
+	TIME_SECTION("Android aapt");
+	{
+		TEMPORARY_ALLOCATOR_GUARD();
+		gb_string_clear(cmd);
+
+		String manifest = {};
+		if (build_context.android_manifest.len != 0) {
+			manifest = concatenate_strings(temporary_allocator(), current_directory, build_context.android_manifest);
+		} else {
+			manifest = concatenate_strings(temporary_allocator(), init_directory, str_lit("AndroidManifest.xml"));
+		}
+
+		cmd = gb_string_append_length(cmd, android_sdk_build_tools.text, android_sdk_build_tools.len);
+		cmd = gb_string_appendc(cmd, "aapt");
+		cmd = gb_string_appendc(cmd, " package -f");
+		if (manifest.len != 0) {
+			cmd = gb_string_append_fmt(cmd, " -M \"%.*s\"", LIT(manifest));
+		}
+		cmd = gb_string_append_fmt(cmd, " -I \"%.*sandroid.jar\"", LIT(android_sdk_platforms));
+		cmd = gb_string_append_fmt(cmd, " -F \"%.*s.apk-build\"", LIT(output_apk));
+
+		result = system_exec_command_line_app("android-aapt", cmd);
+		if (result) {
+			return result;
+		}
+	}
+
+	TIME_SECTION("Android jarsigner");
+	{
+		TEMPORARY_ALLOCATOR_GUARD();
+		gb_string_clear(cmd);
+
+		cmd = gb_string_append_length(cmd, build_context.ODIN_ANDROID_JAR_SIGNER.text, build_context.ODIN_ANDROID_JAR_SIGNER.len);
+		cmd = gb_string_append_fmt(cmd, " -storepass android");
+		if (build_context.android_keystore.len != 0) {
+			String keystore = concatenate_strings(temporary_allocator(), current_directory, build_context.android_keystore);
+			cmd = gb_string_append_fmt(cmd, " -keystore \"%.*s\"", LIT(keystore));
+		}
+		cmd = gb_string_append_fmt(cmd, " \"%.*s.apk-build\"", LIT(output_apk));
+		if (build_context.android_keystore_alias.len != 0) {
+			String keystore_alias = build_context.android_keystore_alias;
+			cmd = gb_string_append_fmt(cmd, " \"%.*s\"", LIT(keystore_alias));
+		}
+
+		result = system_exec_command_line_app("android-jarsigner", cmd);
+		if (result) {
+			return result;
+		}
+	}
+
+	TIME_SECTION("Android zipalign");
+	{
+		TEMPORARY_ALLOCATOR_GUARD();
+		gb_string_clear(cmd);
+
+		cmd = gb_string_append_length(cmd, android_sdk_build_tools.text, android_sdk_build_tools.len);
+		cmd = gb_string_appendc(cmd, "zipalign");
+		cmd = gb_string_appendc(cmd, " -f 4");
+		cmd = gb_string_append_fmt(cmd, " \"%.*s.apk-build\" \"%.*s.apk\"", LIT(output_apk), LIT(output_apk));
+
+		result = system_exec_command_line_app("android-zipalign", cmd);
+		if (result) {
+			return result;
+		}
+	}
+
+	return 0;
+}

+ 64 - 12
src/path.cpp

@@ -30,28 +30,80 @@ gb_internal String remove_directory_from_path(String const &s) {
 }
 
 
-// NOTE(Mark Naughton): getcwd as String
-#if !defined(GB_SYSTEM_WINDOWS)
-gb_internal String get_current_directory(void) {
-	char cwd[256];
-	getcwd(cwd, 256);
+#if defined(GB_SYSTEM_WINDOWS)
+gb_global SRWLOCK cwd_lock;
+
+String get_working_directory(gbAllocator allocator) {
+	AcquireSRWLockExclusive(&cwd_lock);
+
+	TEMPORARY_ALLOCATOR_GUARD();
+
+	DWORD sz_utf16 = GetCurrentDirectoryW(0, nullptr);
+	wchar_t *dir_buf_wstr = gb_alloc_array(temporary_allocator(), wchar_t, sz_utf16);
+	if (dir_buf_wstr == nullptr) {
+		ReleaseSRWLockExclusive(&cwd_lock);
+		return {};
+	}
+
+	DWORD n = GetCurrentDirectoryW(sz_utf16, dir_buf_wstr);
+	GB_ASSERT(n+1 == sz_utf16);
+	ReleaseSRWLockExclusive(&cwd_lock);
+
+
+	isize buf_len = sz_utf16*4;
+	u8 *buf = gb_alloc_array(allocator, u8, buf_len);
+	gb_ucs2_to_utf8(buf, buf_len, cast(u16 *)dir_buf_wstr);
+
+	return make_string_c(cast(char const *)buf);
+}
+
+bool set_working_directory(String dir) {
+	bool ok = false;
+	TEMPORARY_ALLOCATOR_GUARD();
+
+	char const *cdir = alloc_cstring(temporary_allocator(), dir);
+	wchar_t *wstr = gb__alloc_utf8_to_ucs2(temporary_allocator(), cdir, nullptr);
 
-	return make_string_c(cwd);
+	AcquireSRWLockExclusive(&cwd_lock);
+
+	ok = SetCurrentDirectoryW(wstr);
+
+	ReleaseSRWLockExclusive(&cwd_lock);
+
+	return ok;
 }
 
 #else
-gb_internal String get_current_directory(void) {
-	gbAllocator a = heap_allocator();
 
-	wchar_t cwd[256];
-	GetCurrentDirectoryW(256, cwd);
+String get_working_directory(gbAllocator allocator) {
+	TEMPORARY_ALLOCATOR_GUARD();
 
-	String16 wstr = make_string16_c(cwd);
+	auto buf = array_make<char>(temporary_allocator());
+	size_t size = PATH_MAX;
 
-	return string16_to_string(a, wstr);
+	char const *cwd;
+	for (; cwd == nullptr; size *= 2) {
+		array_resize(&buf, size);
+
+		cwd = getcwd(buf.data, buf.count);
+		if (cwd == nullptr && errno != ERANGE) {
+			return {};
+		}
+	}
+
+	return copy_string(allocator, make_string_c(cwd));
+}
+
+bool set_working_directory(String dir) {
+	TEMPORARY_ALLOCATOR_GUARD();
+	char const *cdir = alloc_cstring(temporary_allocator(), dir);
+	return !chdir(cdir);
 }
+
 #endif
 
+
+
 gb_internal bool path_is_directory(String path);
 
 gb_internal String directory_from_path(String const &s) {

+ 9 - 0
src/string.cpp

@@ -273,6 +273,15 @@ gb_internal String path_extension(String const &str, bool include_dot = true) {
 	return substring(str, include_dot ? pos : pos + 1, str.len);
 }
 
+
+gb_internal String path_remove_extension(String const &str) {
+	isize pos = string_extension_position(str);
+	if (pos < 0) {
+		return str;
+	}
+	return substring(str, 0, pos);
+}
+
 gb_internal String string_trim_whitespace(String str) {
 	while (str.len > 0 && rune_is_whitespace(str[str.len-1])) {
 		str.len--;