diff --git a/.gitignore b/.gitignore index 3ff4ead..e4f94cc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target/ .idea -*.iml \ No newline at end of file +*.iml +CMakeFiles diff --git a/Dockerfile b/Dockerfile index 6ca9774..db65e1f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ -FROM rust:latest +FROM rust:stretch RUN apt-get update -RUN apt-get install -yq openjdk-8-jre unzip wget +RUN apt-get install -yq openjdk-8-jre unzip wget cmake RUN rustup target add armv7-linux-androideabi RUN rustup target add aarch64-linux-android diff --git a/README.md b/README.md index b86b7bb..6da706e 100644 --- a/README.md +++ b/README.md @@ -84,15 +84,16 @@ the stdlib. The build process works by: -- Using rustc to always compile your crate as a static library by: +- Using rustc to always compile your crate as a shared library by: + - Creating a custom CMake toolchain file and setting environment variables which expose the appropriate NDK provided build tools for use with the `cc` and `cmake` crates. - Creating a temporary file in the same directory as your crate root. This temporary file serves as the crate root of the static library. It contains the contents of the original crate root along with an `android_main` implementation. - Injecting some glue libraries in rust, which is used by `android_main` to perform initialization required by the `android_glue` crate and to call the `main` function of your crate. -- Using `ndk-build` provided by the NDK to to build a shared library. -- Linking to the `android_native_app_glue` library provided by the Android NDK. `android_native_app_glue` provides the entrypoint used by Android's `NativeActivity` that calls `android_main`. + - Compiling a forked version of `android_native_app_glue`. `android_native_app_glue` is originally provided by the NDK. It provides the entrypoint used by Android's `NativeActivity` that calls `android_main`. + - Linking using the NDK provided linker. This first step outputs a shared library, and is run once per target architecture. -The command then builds the APK using the shared library, generated manifest, and tools from the Android SDK. +The command then builds the APK using the shared libraries, generated manifest, and tools from the Android SDK. If the C++ standard library is used, it adds the appropriate shared library to the APK. It signs the APK with the default debug keystore used by Android development tools. If the keystore doesn't exist, it creates it using the keytool from the JRE or JDK. # Supported `[package.metadata.android]` entries @@ -199,3 +200,17 @@ max_sdk_version = 18 [[package.metadata.android.permission]] name = "android.permission.CAMERA" ``` + +# Environment Variables +Cargo-apk sets environment variables which are used to expose the appropriate C and C++ build tools to build scripts. The primary intent is to support building crates which have build scripts which use the `cc` and `cmake` crates. + +- CC : path to NDK provided `clang` wrapper for the appropriate target and android platform. +- CXX : path to NDK provided `clang++` wrapper for the appropriate target and android platform. +- AR : path to NDK provided `ar` +- CXXSTDLIB : `c++` to use the full featured C++ standard library provided by the NDK. +- CMAKE_TOOLCHAIN_FILE : the path to the generated CMake toolchain. This toolchain sets the ABI, overrides any target specified, and includes the toolchain provided by the NDK. +- CMAKE_GENERATOR : `Unix Makefiles` to default to `Unix Makefiles` as opposed to using the CMake default which may not be appropriate depending on platform. +- CMAKE_MAKE_PROGRAM: Path to NDK provided make. + +# C++ Standard Library Compatibility Issues +When a crate links to the C++ standard library, the shared library version provided by the NDK is used. Unfortunately, dependency loading issues will cause the application to crash on older versions of android. Once `lld` linker issues are resolved on all platforms, cargo apk will be updated to link to the static C++ library. This should resolve the compatibility issues. diff --git a/cargo-apk/Cargo.lock b/cargo-apk/Cargo.lock index 02ce304..4cda62c 100644 --- a/cargo-apk/Cargo.lock +++ b/cargo-apk/Cargo.lock @@ -200,6 +200,7 @@ dependencies = [ "dirs 2.0.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "multimap 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -769,6 +770,14 @@ dependencies = [ "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "multimap" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.97 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "nodrop" version = "0.1.13" @@ -1434,6 +1443,7 @@ dependencies = [ "checksum miniz_oxide 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c061edee74a88eb35d876ce88b94d77a0448a201de111c244b70d047f5820516" "checksum miniz_oxide_c_api 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6c675792957b0d19933816c4e1d56663c341dd9bfa31cb2140ff2267c1d8ecf4" "checksum miow 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "396aa0f2003d7df8395cb93e09871561ccc3e785f0acb369170e8cc74ddf9226" +"checksum multimap 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1838fe05e64c53b9a62579020e4a12ec5947af5d703c56962d0e3b7b923bf3ba" "checksum nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "2f9667ddcc6cc8a43afc9b7917599d7216aa09c463919ea32c59ed6cac8bc945" "checksum num_cpus 1.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bcef43580c035376c0705c42792c294b66974abbfd2789b511784023f71f3273" "checksum opener 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "998c59e83d9474c01127a96e023b7a04bb061dd286bf8bb939d31dc8d31a7448" diff --git a/cargo-apk/Cargo.toml b/cargo-apk/Cargo.toml index 9321a01..098a34e 100644 --- a/cargo-apk/Cargo.toml +++ b/cargo-apk/Cargo.toml @@ -13,6 +13,7 @@ clap = "2.33.0" itertools = "0.8.0" dirs = "2.0.1" failure = "0.1.5" +multimap = "0.5.0" serde = "1.0.97" toml = "0.5.1" diff --git a/cargo-apk/native_app_glue/NOTICE b/cargo-apk/native_app_glue/NOTICE new file mode 100644 index 0000000..d6c0922 --- /dev/null +++ b/cargo-apk/native_app_glue/NOTICE @@ -0,0 +1,13 @@ +Copyright (C) 2016 The Android Open Source Project + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/cargo-apk/native_app_glue/android_native_app_glue.c b/cargo-apk/native_app_glue/android_native_app_glue.c new file mode 100644 index 0000000..7eada08 --- /dev/null +++ b/cargo-apk/native_app_glue/android_native_app_glue.c @@ -0,0 +1,442 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include + +#include +#include +#include +#include +#include + +#include "android_native_app_glue.h" +#include + +#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "threaded_app", __VA_ARGS__)) +#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "threaded_app", __VA_ARGS__)) + +/* For debug builds, always enable the debug traces in this library */ +#ifndef NDEBUG +# define LOGV(...) ((void)__android_log_print(ANDROID_LOG_VERBOSE, "threaded_app", __VA_ARGS__)) +#else +# define LOGV(...) ((void)0) +#endif + +static void free_saved_state(struct android_app* android_app) { + pthread_mutex_lock(&android_app->mutex); + if (android_app->savedState != NULL) { + free(android_app->savedState); + android_app->savedState = NULL; + android_app->savedStateSize = 0; + } + pthread_mutex_unlock(&android_app->mutex); +} + +int8_t android_app_read_cmd(struct android_app* android_app) { + int8_t cmd; + if (read(android_app->msgread, &cmd, sizeof(cmd)) == sizeof(cmd)) { + switch (cmd) { + case APP_CMD_SAVE_STATE: + free_saved_state(android_app); + break; + } + return cmd; + } else { + LOGE("No data on command pipe!"); + } + return -1; +} + +static void print_cur_config(struct android_app* android_app) { + char lang[2], country[2]; + AConfiguration_getLanguage(android_app->config, lang); + AConfiguration_getCountry(android_app->config, country); + + LOGV("Config: mcc=%d mnc=%d lang=%c%c cnt=%c%c orien=%d touch=%d dens=%d " + "keys=%d nav=%d keysHid=%d navHid=%d sdk=%d size=%d long=%d " + "modetype=%d modenight=%d", + AConfiguration_getMcc(android_app->config), + AConfiguration_getMnc(android_app->config), + lang[0], lang[1], country[0], country[1], + AConfiguration_getOrientation(android_app->config), + AConfiguration_getTouchscreen(android_app->config), + AConfiguration_getDensity(android_app->config), + AConfiguration_getKeyboard(android_app->config), + AConfiguration_getNavigation(android_app->config), + AConfiguration_getKeysHidden(android_app->config), + AConfiguration_getNavHidden(android_app->config), + AConfiguration_getSdkVersion(android_app->config), + AConfiguration_getScreenSize(android_app->config), + AConfiguration_getScreenLong(android_app->config), + AConfiguration_getUiModeType(android_app->config), + AConfiguration_getUiModeNight(android_app->config)); +} + +void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd) { + switch (cmd) { + case APP_CMD_INPUT_CHANGED: + LOGV("APP_CMD_INPUT_CHANGED\n"); + pthread_mutex_lock(&android_app->mutex); + if (android_app->inputQueue != NULL) { + AInputQueue_detachLooper(android_app->inputQueue); + } + android_app->inputQueue = android_app->pendingInputQueue; + if (android_app->inputQueue != NULL) { + LOGV("Attaching input queue to looper"); + AInputQueue_attachLooper(android_app->inputQueue, + android_app->looper, LOOPER_ID_INPUT, NULL, + &android_app->inputPollSource); + } + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + break; + + case APP_CMD_INIT_WINDOW: + LOGV("APP_CMD_INIT_WINDOW\n"); + pthread_mutex_lock(&android_app->mutex); + android_app->window = android_app->pendingWindow; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + break; + + case APP_CMD_TERM_WINDOW: + LOGV("APP_CMD_TERM_WINDOW\n"); + pthread_cond_broadcast(&android_app->cond); + break; + + case APP_CMD_RESUME: + case APP_CMD_START: + case APP_CMD_PAUSE: + case APP_CMD_STOP: + LOGV("activityState=%d\n", cmd); + pthread_mutex_lock(&android_app->mutex); + android_app->activityState = cmd; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + break; + + case APP_CMD_CONFIG_CHANGED: + LOGV("APP_CMD_CONFIG_CHANGED\n"); + AConfiguration_fromAssetManager(android_app->config, + android_app->activity->assetManager); + print_cur_config(android_app); + break; + + case APP_CMD_DESTROY: + LOGV("APP_CMD_DESTROY\n"); + android_app->destroyRequested = 1; + break; + } +} + +void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd) { + switch (cmd) { + case APP_CMD_TERM_WINDOW: + LOGV("APP_CMD_TERM_WINDOW\n"); + pthread_mutex_lock(&android_app->mutex); + android_app->window = NULL; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + break; + + case APP_CMD_SAVE_STATE: + LOGV("APP_CMD_SAVE_STATE\n"); + pthread_mutex_lock(&android_app->mutex); + android_app->stateSaved = 1; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + break; + + case APP_CMD_RESUME: + free_saved_state(android_app); + break; + } +} + +void app_dummy() { + +} + +static void android_app_destroy(struct android_app* android_app) { + LOGV("android_app_destroy!"); + free_saved_state(android_app); + pthread_mutex_lock(&android_app->mutex); + if (android_app->inputQueue != NULL) { + AInputQueue_detachLooper(android_app->inputQueue); + } + AConfiguration_delete(android_app->config); + android_app->destroyed = 1; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + // Can't touch android_app object after this. +} + +static void process_input(struct android_app* app, struct android_poll_source* source) { + AInputEvent* event = NULL; + while (AInputQueue_getEvent(app->inputQueue, &event) >= 0) { + LOGV("New input event: type=%d\n", AInputEvent_getType(event)); + if (AInputQueue_preDispatchEvent(app->inputQueue, event)) { + continue; + } + int32_t handled = 0; + if (app->onInputEvent != NULL) handled = app->onInputEvent(app, event); + AInputQueue_finishEvent(app->inputQueue, event, handled); + } +} + +static void process_cmd(struct android_app* app, struct android_poll_source* source) { + int8_t cmd = android_app_read_cmd(app); + android_app_pre_exec_cmd(app, cmd); + if (app->onAppCmd != NULL) app->onAppCmd(app, cmd); + android_app_post_exec_cmd(app, cmd); +} + +static void* android_app_entry(void* param) { + struct android_app* android_app = (struct android_app*)param; + + android_app->config = AConfiguration_new(); + AConfiguration_fromAssetManager(android_app->config, android_app->activity->assetManager); + + print_cur_config(android_app); + + android_app->cmdPollSource.id = LOOPER_ID_MAIN; + android_app->cmdPollSource.app = android_app; + android_app->cmdPollSource.process = process_cmd; + android_app->inputPollSource.id = LOOPER_ID_INPUT; + android_app->inputPollSource.app = android_app; + android_app->inputPollSource.process = process_input; + + ALooper* looper = ALooper_prepare(ALOOPER_PREPARE_ALLOW_NON_CALLBACKS); + ALooper_addFd(looper, android_app->msgread, LOOPER_ID_MAIN, ALOOPER_EVENT_INPUT, NULL, + &android_app->cmdPollSource); + android_app->looper = looper; + + pthread_mutex_lock(&android_app->mutex); + android_app->running = 1; + pthread_cond_broadcast(&android_app->cond); + pthread_mutex_unlock(&android_app->mutex); + + android_main(android_app); + + android_app_destroy(android_app); + return NULL; +} + +// -------------------------------------------------------------------- +// Native activity interaction (called from main thread) +// -------------------------------------------------------------------- + +static struct android_app* android_app_create(ANativeActivity* activity, + void* savedState, size_t savedStateSize) { + struct android_app* android_app = (struct android_app*)malloc(sizeof(struct android_app)); + memset(android_app, 0, sizeof(struct android_app)); + android_app->activity = activity; + + pthread_mutex_init(&android_app->mutex, NULL); + pthread_cond_init(&android_app->cond, NULL); + + if (savedState != NULL) { + android_app->savedState = malloc(savedStateSize); + android_app->savedStateSize = savedStateSize; + memcpy(android_app->savedState, savedState, savedStateSize); + } + + int msgpipe[2]; + if (pipe(msgpipe)) { + LOGE("could not create pipe: %s", strerror(errno)); + return NULL; + } + android_app->msgread = msgpipe[0]; + android_app->msgwrite = msgpipe[1]; + + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); + pthread_create(&android_app->thread, &attr, android_app_entry, android_app); + + // Wait for thread to start. + pthread_mutex_lock(&android_app->mutex); + while (!android_app->running) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + pthread_mutex_unlock(&android_app->mutex); + + return android_app; +} + +static void android_app_write_cmd(struct android_app* android_app, int8_t cmd) { + if (write(android_app->msgwrite, &cmd, sizeof(cmd)) != sizeof(cmd)) { + LOGE("Failure writing android_app cmd: %s\n", strerror(errno)); + } +} + +static void android_app_set_input(struct android_app* android_app, AInputQueue* inputQueue) { + pthread_mutex_lock(&android_app->mutex); + android_app->pendingInputQueue = inputQueue; + android_app_write_cmd(android_app, APP_CMD_INPUT_CHANGED); + while (android_app->inputQueue != android_app->pendingInputQueue) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + pthread_mutex_unlock(&android_app->mutex); +} + +static void android_app_set_window(struct android_app* android_app, ANativeWindow* window) { + pthread_mutex_lock(&android_app->mutex); + if (android_app->pendingWindow != NULL) { + android_app_write_cmd(android_app, APP_CMD_TERM_WINDOW); + } + android_app->pendingWindow = window; + if (window != NULL) { + android_app_write_cmd(android_app, APP_CMD_INIT_WINDOW); + } + while (android_app->window != android_app->pendingWindow) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + pthread_mutex_unlock(&android_app->mutex); +} + +static void android_app_set_activity_state(struct android_app* android_app, int8_t cmd) { + pthread_mutex_lock(&android_app->mutex); + android_app_write_cmd(android_app, cmd); + while (android_app->activityState != cmd) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + pthread_mutex_unlock(&android_app->mutex); +} + +static void android_app_free(struct android_app* android_app) { + pthread_mutex_lock(&android_app->mutex); + android_app_write_cmd(android_app, APP_CMD_DESTROY); + while (!android_app->destroyed) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + pthread_mutex_unlock(&android_app->mutex); + + close(android_app->msgread); + close(android_app->msgwrite); + pthread_cond_destroy(&android_app->cond); + pthread_mutex_destroy(&android_app->mutex); + free(android_app); +} + +static void onDestroy(ANativeActivity* activity) { + LOGV("Destroy: %p\n", activity); + android_app_free((struct android_app*)activity->instance); +} + +static void onStart(ANativeActivity* activity) { + LOGV("Start: %p\n", activity); + android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_START); +} + +static void onResume(ANativeActivity* activity) { + LOGV("Resume: %p\n", activity); + android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_RESUME); +} + +static void* onSaveInstanceState(ANativeActivity* activity, size_t* outLen) { + struct android_app* android_app = (struct android_app*)activity->instance; + void* savedState = NULL; + + LOGV("SaveInstanceState: %p\n", activity); + pthread_mutex_lock(&android_app->mutex); + android_app->stateSaved = 0; + android_app_write_cmd(android_app, APP_CMD_SAVE_STATE); + while (!android_app->stateSaved) { + pthread_cond_wait(&android_app->cond, &android_app->mutex); + } + + if (android_app->savedState != NULL) { + savedState = android_app->savedState; + *outLen = android_app->savedStateSize; + android_app->savedState = NULL; + android_app->savedStateSize = 0; + } + + pthread_mutex_unlock(&android_app->mutex); + + return savedState; +} + +static void onPause(ANativeActivity* activity) { + LOGV("Pause: %p\n", activity); + android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_PAUSE); +} + +static void onStop(ANativeActivity* activity) { + LOGV("Stop: %p\n", activity); + android_app_set_activity_state((struct android_app*)activity->instance, APP_CMD_STOP); +} + +static void onConfigurationChanged(ANativeActivity* activity) { + struct android_app* android_app = (struct android_app*)activity->instance; + LOGV("ConfigurationChanged: %p\n", activity); + android_app_write_cmd(android_app, APP_CMD_CONFIG_CHANGED); +} + +static void onLowMemory(ANativeActivity* activity) { + struct android_app* android_app = (struct android_app*)activity->instance; + LOGV("LowMemory: %p\n", activity); + android_app_write_cmd(android_app, APP_CMD_LOW_MEMORY); +} + +static void onWindowFocusChanged(ANativeActivity* activity, int focused) { + LOGV("WindowFocusChanged: %p -- %d\n", activity, focused); + android_app_write_cmd((struct android_app*)activity->instance, + focused ? APP_CMD_GAINED_FOCUS : APP_CMD_LOST_FOCUS); +} + +static void onNativeWindowCreated(ANativeActivity* activity, ANativeWindow* window) { + LOGV("NativeWindowCreated: %p -- %p\n", activity, window); + android_app_set_window((struct android_app*)activity->instance, window); +} + +static void onNativeWindowDestroyed(ANativeActivity* activity, ANativeWindow* window) { + LOGV("NativeWindowDestroyed: %p -- %p\n", activity, window); + android_app_set_window((struct android_app*)activity->instance, NULL); +} + +static void onInputQueueCreated(ANativeActivity* activity, AInputQueue* queue) { + LOGV("InputQueueCreated: %p -- %p\n", activity, queue); + android_app_set_input((struct android_app*)activity->instance, queue); +} + +static void onInputQueueDestroyed(ANativeActivity* activity, AInputQueue* queue) { + LOGV("InputQueueDestroyed: %p -- %p\n", activity, queue); + android_app_set_input((struct android_app*)activity->instance, NULL); +} + +JNIEXPORT +void ANativeActivity_onCreate(ANativeActivity* activity, void* savedState, + size_t savedStateSize) { + LOGV("Creating: %p\n", activity); + activity->callbacks->onDestroy = onDestroy; + activity->callbacks->onStart = onStart; + activity->callbacks->onResume = onResume; + activity->callbacks->onSaveInstanceState = onSaveInstanceState; + activity->callbacks->onPause = onPause; + activity->callbacks->onStop = onStop; + activity->callbacks->onConfigurationChanged = onConfigurationChanged; + activity->callbacks->onLowMemory = onLowMemory; + activity->callbacks->onWindowFocusChanged = onWindowFocusChanged; + activity->callbacks->onNativeWindowCreated = onNativeWindowCreated; + activity->callbacks->onNativeWindowDestroyed = onNativeWindowDestroyed; + activity->callbacks->onInputQueueCreated = onInputQueueCreated; + activity->callbacks->onInputQueueDestroyed = onInputQueueDestroyed; + + activity->instance = android_app_create(activity, savedState, savedStateSize); +} diff --git a/cargo-apk/native_app_glue/android_native_app_glue.h b/cargo-apk/native_app_glue/android_native_app_glue.h new file mode 100644 index 0000000..c99d6e1 --- /dev/null +++ b/cargo-apk/native_app_glue/android_native_app_glue.h @@ -0,0 +1,354 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#ifndef _ANDROID_NATIVE_APP_GLUE_H +#define _ANDROID_NATIVE_APP_GLUE_H + +#include +#include +#include + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * The native activity interface provided by + * is based on a set of application-provided callbacks that will be called + * by the Activity's main thread when certain events occur. + * + * This means that each one of this callbacks _should_ _not_ block, or they + * risk having the system force-close the application. This programming + * model is direct, lightweight, but constraining. + * + * The 'android_native_app_glue' static library is used to provide a different + * execution model where the application can implement its own main event + * loop in a different thread instead. Here's how it works: + * + * 1/ The application must provide a function named "android_main()" that + * will be called when the activity is created, in a new thread that is + * distinct from the activity's main thread. + * + * 2/ android_main() receives a pointer to a valid "android_app" structure + * that contains references to other important objects, e.g. the + * ANativeActivity obejct instance the application is running in. + * + * 3/ the "android_app" object holds an ALooper instance that already + * listens to two important things: + * + * - activity lifecycle events (e.g. "pause", "resume"). See APP_CMD_XXX + * declarations below. + * + * - input events coming from the AInputQueue attached to the activity. + * + * Each of these correspond to an ALooper identifier returned by + * ALooper_pollOnce with values of LOOPER_ID_MAIN and LOOPER_ID_INPUT, + * respectively. + * + * Your application can use the same ALooper to listen to additional + * file-descriptors. They can either be callback based, or with return + * identifiers starting with LOOPER_ID_USER. + * + * 4/ Whenever you receive a LOOPER_ID_MAIN or LOOPER_ID_INPUT event, + * the returned data will point to an android_poll_source structure. You + * can call the process() function on it, and fill in android_app->onAppCmd + * and android_app->onInputEvent to be called for your own processing + * of the event. + * + * Alternatively, you can call the low-level functions to read and process + * the data directly... look at the process_cmd() and process_input() + * implementations in the glue to see how to do this. + * + * See the sample named "native-activity" that comes with the NDK with a + * full usage example. Also look at the JavaDoc of NativeActivity. + */ + +struct android_app; + +/** + * Data associated with an ALooper fd that will be returned as the "outData" + * when that source has data ready. + */ +struct android_poll_source { + // The identifier of this source. May be LOOPER_ID_MAIN or + // LOOPER_ID_INPUT. + int32_t id; + + // The android_app this ident is associated with. + struct android_app* app; + + // Function to call to perform the standard processing of data from + // this source. + void (*process)(struct android_app* app, struct android_poll_source* source); +}; + +/** + * This is the interface for the standard glue code of a threaded + * application. In this model, the application's code is running + * in its own thread separate from the main thread of the process. + * It is not required that this thread be associated with the Java + * VM, although it will need to be in order to make JNI calls any + * Java objects. + */ +struct android_app { + // The application can place a pointer to its own state object + // here if it likes. + void* userData; + + // Fill this in with the function to process main app commands (APP_CMD_*) + void (*onAppCmd)(struct android_app* app, int32_t cmd); + + // Fill this in with the function to process input events. At this point + // the event has already been pre-dispatched, and it will be finished upon + // return. Return 1 if you have handled the event, 0 for any default + // dispatching. + int32_t (*onInputEvent)(struct android_app* app, AInputEvent* event); + + // The ANativeActivity object instance that this app is running in. + ANativeActivity* activity; + + // The current configuration the app is running in. + AConfiguration* config; + + // This is the last instance's saved state, as provided at creation time. + // It is NULL if there was no state. You can use this as you need; the + // memory will remain around until you call android_app_exec_cmd() for + // APP_CMD_RESUME, at which point it will be freed and savedState set to NULL. + // These variables should only be changed when processing a APP_CMD_SAVE_STATE, + // at which point they will be initialized to NULL and you can malloc your + // state and place the information here. In that case the memory will be + // freed for you later. + void* savedState; + size_t savedStateSize; + + // The ALooper associated with the app's thread. + ALooper* looper; + + // When non-NULL, this is the input queue from which the app will + // receive user input events. + AInputQueue* inputQueue; + + // When non-NULL, this is the window surface that the app can draw in. + ANativeWindow* window; + + // Current content rectangle of the window; this is the area where the + // window's content should be placed to be seen by the user. + ARect contentRect; + + // Current state of the app's activity. May be either APP_CMD_START, + // APP_CMD_RESUME, APP_CMD_PAUSE, or APP_CMD_STOP; see below. + int activityState; + + // This is non-zero when the application's NativeActivity is being + // destroyed and waiting for the app thread to complete. + int destroyRequested; + + // ------------------------------------------------- + // Below are "private" implementation of the glue code. + + pthread_mutex_t mutex; + pthread_cond_t cond; + + int msgread; + int msgwrite; + + pthread_t thread; + + struct android_poll_source cmdPollSource; + struct android_poll_source inputPollSource; + + int running; + int stateSaved; + int destroyed; + int redrawNeeded; + AInputQueue* pendingInputQueue; + ANativeWindow* pendingWindow; + ARect pendingContentRect; +}; + +enum { + /** + * Looper data ID of commands coming from the app's main thread, which + * is returned as an identifier from ALooper_pollOnce(). The data for this + * identifier is a pointer to an android_poll_source structure. + * These can be retrieved and processed with android_app_read_cmd() + * and android_app_exec_cmd(). + */ + LOOPER_ID_MAIN = 1, + + /** + * Looper data ID of events coming from the AInputQueue of the + * application's window, which is returned as an identifier from + * ALooper_pollOnce(). The data for this identifier is a pointer to an + * android_poll_source structure. These can be read via the inputQueue + * object of android_app. + */ + LOOPER_ID_INPUT = 2, + + /** + * Start of user-defined ALooper identifiers. + */ + LOOPER_ID_USER = 3, +}; + +enum { + /** + * Command from main thread: the AInputQueue has changed. Upon processing + * this command, android_app->inputQueue will be updated to the new queue + * (or NULL). + */ + APP_CMD_INPUT_CHANGED, + + /** + * Command from main thread: a new ANativeWindow is ready for use. Upon + * receiving this command, android_app->window will contain the new window + * surface. + */ + APP_CMD_INIT_WINDOW, + + /** + * Command from main thread: the existing ANativeWindow needs to be + * terminated. Upon receiving this command, android_app->window still + * contains the existing window; after calling android_app_exec_cmd + * it will be set to NULL. + */ + APP_CMD_TERM_WINDOW, + + /** + * Command from main thread: the current ANativeWindow has been resized. + * Please redraw with its new size. + */ + APP_CMD_WINDOW_RESIZED, + + /** + * Command from main thread: the system needs that the current ANativeWindow + * be redrawn. You should redraw the window before handing this to + * android_app_exec_cmd() in order to avoid transient drawing glitches. + */ + APP_CMD_WINDOW_REDRAW_NEEDED, + + /** + * Command from main thread: the content area of the window has changed, + * such as from the soft input window being shown or hidden. You can + * find the new content rect in android_app::contentRect. + */ + APP_CMD_CONTENT_RECT_CHANGED, + + /** + * Command from main thread: the app's activity window has gained + * input focus. + */ + APP_CMD_GAINED_FOCUS, + + /** + * Command from main thread: the app's activity window has lost + * input focus. + */ + APP_CMD_LOST_FOCUS, + + /** + * Command from main thread: the current device configuration has changed. + */ + APP_CMD_CONFIG_CHANGED, + + /** + * Command from main thread: the system is running low on memory. + * Try to reduce your memory use. + */ + APP_CMD_LOW_MEMORY, + + /** + * Command from main thread: the app's activity has been started. + */ + APP_CMD_START, + + /** + * Command from main thread: the app's activity has been resumed. + */ + APP_CMD_RESUME, + + /** + * Command from main thread: the app should generate a new saved state + * for itself, to restore from later if needed. If you have saved state, + * allocate it with malloc and place it in android_app.savedState with + * the size in android_app.savedStateSize. The will be freed for you + * later. + */ + APP_CMD_SAVE_STATE, + + /** + * Command from main thread: the app's activity has been paused. + */ + APP_CMD_PAUSE, + + /** + * Command from main thread: the app's activity has been stopped. + */ + APP_CMD_STOP, + + /** + * Command from main thread: the app's activity is being destroyed, + * and waiting for the app thread to clean up and exit before proceeding. + */ + APP_CMD_DESTROY, +}; + +/** + * Call when ALooper_pollAll() returns LOOPER_ID_MAIN, reading the next + * app command message. + */ +int8_t android_app_read_cmd(struct android_app* android_app); + +/** + * Call with the command returned by android_app_read_cmd() to do the + * initial pre-processing of the given command. You can perform your own + * actions for the command after calling this function. + */ +void android_app_pre_exec_cmd(struct android_app* android_app, int8_t cmd); + +/** + * Call with the command returned by android_app_read_cmd() to do the + * final post-processing of the given command. You must have done your own + * actions for the command before calling this function. + */ +void android_app_post_exec_cmd(struct android_app* android_app, int8_t cmd); + +/** + * Dummy function that used to be used to prevent the linker from stripping app + * glue code. No longer necessary, since __attribute__((visibility("default"))) + * does this for us. + */ +__attribute__(( + deprecated("Calls to app_dummy are no longer necessary. See " + "https://github.com/android-ndk/ndk/issues/381."))) void +app_dummy(); + +/** + * This is the function that application code must implement, representing + * the main entry to the app. + */ +extern void android_main(struct android_app* app); + +#ifdef __cplusplus +} +#endif + +#endif /* _ANDROID_NATIVE_APP_GLUE_H */ diff --git a/cargo-apk/src/config.rs b/cargo-apk/src/config.rs index c75ea3c..97e348c 100644 --- a/cargo-apk/src/config.rs +++ b/cargo-apk/src/config.rs @@ -15,6 +15,7 @@ use std::path::Path; use std::path::PathBuf; use toml; +#[derive(Clone)] pub struct AndroidConfig { /// Name of the cargo package pub cargo_package_name: String, @@ -30,7 +31,7 @@ pub struct AndroidConfig { pub ndk_path: PathBuf, /// List of targets to build the app for. Eg. `armv7-linux-androideabi`. - pub build_targets: Vec, + pub build_targets: Vec, /// Path to the android.jar for the selected android platform pub android_jar_path: PathBuf, @@ -145,6 +146,19 @@ impl AndroidConfig { } } +/// Build targets supported by NDK +#[derive(Debug, Copy, Clone, Deserialize)] +pub enum AndroidBuildTarget { + #[serde(rename(deserialize = "armv7-linux-androideabi"))] + ArmV7a, + #[serde(rename(deserialize = "aarch64-linux-android"))] + Arm64V8a, + #[serde(rename(deserialize = "i686-linux-android"))] + X86, + #[serde(rename(deserialize = "x86_64-linux-android"))] + X86_64, +} + #[derive(Clone)] pub struct AndroidFeature { pub name: String, @@ -388,9 +402,9 @@ pub fn load( .and_then(|a| a.build_targets.clone()) .unwrap_or_else(|| { vec![ - "armv7-linux-androideabi".to_owned(), - "aarch64-linux-android".to_owned(), - "i686-linux-android".to_owned(), + AndroidBuildTarget::ArmV7a, + AndroidBuildTarget::Arm64V8a, + AndroidBuildTarget::X86, ] }), default_target_config, @@ -427,7 +441,7 @@ struct TomlAndroid { android_version: Option, target_sdk_version: Option, min_sdk_version: Option, - build_targets: Option>, + build_targets: Option>, #[serde(flatten)] default_target_config: TomlAndroidTarget, diff --git a/cargo-apk/src/ops/build.rs b/cargo-apk/src/ops/build.rs index 3b87961..5790fda 100644 --- a/cargo-apk/src/ops/build.rs +++ b/cargo-apk/src/ops/build.rs @@ -1,19 +1,22 @@ mod compile; +mod targets; +pub mod tempfile; +mod util; + +use self::compile::SharedLibraries; use crate::config::{AndroidConfig, AndroidTargetConfig}; use cargo::core::{Target, TargetKind, Workspace}; use cargo::util::process_builder::process; use cargo::util::CargoResult; use clap::ArgMatches; use failure::format_err; -use std::collections::{BTreeMap, HashSet}; +use std::collections::BTreeMap; use std::fs::File; use std::io::Write; use std::path::Path; use std::path::PathBuf; use std::{env, fs}; -pub use compile::AndroidAbi; - #[derive(Debug)] pub struct BuildResult { /// Mapping from target kind and target name to the built APK @@ -25,35 +28,17 @@ pub fn build( config: &AndroidConfig, options: &ArgMatches, ) -> CargoResult { - let root_build_dir = get_root_build_directory(workspace, config); - let (targets, abis) = - compile::build_static_libraries(workspace, config, options, &root_build_dir)?; - build_apks(config, &root_build_dir, &targets, &abis) -} - -/// Returns the directory in which all cargo apk artifacts for the current -/// debug/release configuration should be produced. -fn get_root_build_directory(workspace: &Workspace, config: &AndroidConfig) -> PathBuf { - let android_artifacts_dir = workspace - .target_dir() - .join("android-artifacts") - .into_path_unlocked(); - - if config.release { - android_artifacts_dir.join("release") - } else { - android_artifacts_dir.join("debug") - } + let root_build_dir = util::get_root_build_directory(workspace, config); + let shared_libraries = + compile::build_shared_libraries(workspace, config, options, &root_build_dir)?; + build_apks(config, &root_build_dir, shared_libraries) } fn build_apks( config: &AndroidConfig, root_build_dir: &PathBuf, - targets: &HashSet, - abis: &[AndroidAbi], + shared_libraries: SharedLibraries, ) -> CargoResult { - let abis_str = abis.join(" "); - // Create directory to hold final APKs which are signed using the debug key let final_apk_dir = root_build_dir.join("apk"); fs::create_dir_all(&final_apk_dir)?; @@ -62,34 +47,10 @@ fn build_apks( let mut target_to_apk_map = BTreeMap::new(); // Build an APK for each cargo target - for target in targets.iter() { - let target_directory = match target.kind() { - TargetKind::Bin => root_build_dir.join("bin"), - TargetKind::ExampleBin => root_build_dir.join("examples"), - _ => unreachable!("Unexpected target kind"), - }; - - let target_directory = target_directory.join(target.name()); + for (target, shared_libraries) in shared_libraries.shared_libraries.iter_all() { + let target_directory = util::get_target_directory(root_build_dir, target)?; fs::create_dir_all(&target_directory)?; - // Run ndk-build - build_makefiles(&target_directory, target, &abis_str, config)?; - - let mut ndk_build_cmd = if cfg!(target_os = "windows") { - let mut pb = process("cmd"); - let ndk_build_path = config.ndk_path.join("build/ndk-build.cmd"); - pb.arg("/C").arg(ndk_build_path); - pb - } else { - let ndk_build_path = config.ndk_path.join("build/ndk-build"); - process(ndk_build_path) - }; - - ndk_build_cmd - .arg("NDK_LIBS_OUT=./lib") - .cwd(&target_directory) - .exec()?; - // Determine Target Configuration let target_config = config.resolve((target.kind().to_owned(), target.name().to_owned()))?; @@ -142,9 +103,22 @@ fn build_apks( aapt2_link_cmd.cwd(&target_directory).exec()?; - // Add binaries - for abi in abis { - let so_path = format!("lib/{}/lib{}.so", abi, target.name()); + // Add shared libraries to the APK + for shared_library in shared_libraries { + // Copy the shared library to the appropriate location in the target directory and with the appropriate name + // Note: that the type of slash used matters. This path is passed to aapt and the shared library + // will not load if backslashes are used. + let so_path = format!( + "lib/{}/{}", + &shared_library.abi.android_abi(), + shared_library.filename + ); + + let target_shared_object_path = target_directory.join(&so_path); + fs::create_dir_all(target_shared_object_path.parent().unwrap())?; + fs::copy(&shared_library.path, target_shared_object_path)?; + + // Add to the APK process(&aapt_path) .arg("add") .arg(&unaligned_apk_name) @@ -211,25 +185,17 @@ fn build_apks( } // Sign the APK with the development certificate - let mut apksigner_cmd = if cfg!(target_os = "windows") { - let mut pb = process("cmd"); - let apksigner_path = build_tools_path.join("apksigner.bat"); - pb.arg("/C").arg(apksigner_path); - pb - } else { - let apksigner_path = build_tools_path.join("apksigner"); - process(apksigner_path) - }; - - apksigner_cmd - .arg("sign") - .arg("--ks") - .arg(keystore_path) - .arg("--ks-pass") - .arg("pass:android") - .arg(&final_apk_path) - .cwd(&target_directory) - .exec()?; + util::script_process( + build_tools_path.join(format!("apksigner{}", util::EXECUTABLE_SUFFIX_BAT)), + ) + .arg("sign") + .arg("--ks") + .arg(keystore_path) + .arg("--ks-pass") + .arg("pass:android") + .arg(&final_apk_path) + .cwd(&target_directory) + .exec()?; target_to_apk_map.insert( (target.kind().to_owned(), target.name().to_owned()), @@ -387,59 +353,3 @@ fn build_manifest( Ok(()) } - -fn build_makefiles( - target_directory: &Path, - target: &Target, - abis: &str, - config: &AndroidConfig, -) -> CargoResult<()> { - let output_directory = target_directory.join("jni"); - fs::create_dir_all(&output_directory)?; - - // Write Android.mk - let file = output_directory.join("Android.mk"); - let mut file = File::create(&file)?; - - writeln!( - file, - r#"LOCAL_PATH := $(call my-dir) - -# Define module for static library built by rustc -include $(CLEAR_VARS) -LOCAL_MODULE := rustlib -LOCAL_SRC_FILES := ../../../$(TARGET_ARCH_ABI)/build/lib{target_library_name}.a -include $(PREBUILT_STATIC_LIBRARY) - -# Build the application -include $(CLEAR_VARS) - -LOCAL_MODULE := {target_name} -LOCAL_SRC_FILES := -LOCAL_LDLIBS := -llog -landroid -LOCAL_STATIC_LIBRARIES := android_native_app_glue rustlib -NDK_LIBS_OUT := ./lib - -include $(BUILD_SHARED_LIBRARY) - -$(call import-module,android/native_app_glue)"#, - target_library_name = target.name().replace("-", "_"), - target_name = target.name() - )?; - - // Write Application.mk - let file = output_directory.join("Application.mk"); - let mut file = File::create(&file)?; - - let app_optim = if config.release { "release" } else { "debug" }; - - write!( - file, - r#"APP_ABI := {} -APP_PLATFORM := android-{} -APP_OPTIM := {}"#, - abis, config.min_sdk_version, app_optim - )?; - - Ok(()) -} diff --git a/cargo-apk/src/ops/build/compile.rs b/cargo-apk/src/ops/build/compile.rs index 9632efa..17ed599 100644 --- a/cargo-apk/src/ops/build/compile.rs +++ b/cargo-apk/src/ops/build/compile.rs @@ -1,3 +1,6 @@ +use super::tempfile::TempFile; +use super::util; +use crate::config::AndroidBuildTarget; use crate::config::AndroidConfig; use cargo::core::compiler::CompileMode; use cargo::core::compiler::Executor; @@ -7,9 +10,8 @@ use cargo::util::command_prelude::ArgMatchesExt; use cargo::util::{CargoResult, ProcessBuilder}; use clap::ArgMatches; use failure::format_err; -use std::collections::HashSet; -use std::ffi::OsString; -use std::fmt; +use multimap::MultiMap; +use std::ffi::{OsStr, OsString}; use std::fs; use std::fs::File; use std::io::Write; @@ -17,26 +19,50 @@ use std::path::Path; use std::path::PathBuf; use std::sync::{Arc, Mutex}; -pub type AndroidAbi = String; +pub struct SharedLibrary { + pub abi: AndroidBuildTarget, + pub path: PathBuf, + pub filename: String, +} + +pub struct SharedLibraries { + pub shared_libraries: MultiMap, +} -/// For each build target and cargo binary or example target, produce a static library which is named based on the cargo target -pub fn build_static_libraries( +/// For each build target and cargo binary or example target, produce a shared library +pub fn build_shared_libraries( workspace: &Workspace, config: &AndroidConfig, options: &ArgMatches, root_build_dir: &PathBuf, -) -> CargoResult<(HashSet, Vec)> { +) -> CargoResult { let injected_glue_src_path = write_injected_glue_src(&root_build_dir)?; + let android_native_glue_src_path = write_native_app_glue_src(&root_build_dir)?; - let mut abis = Vec::new(); - let targets: Arc>> = Arc::new(Mutex::new(HashSet::new())); // Set of all example and bin cargo targets built + let shared_libraries: Arc>> = + Arc::new(Mutex::new(MultiMap::new())); for build_target in config.build_targets.iter() { - // Determine the android ABI - let abi = get_abi(build_target)?; - abis.push(abi.to_owned()); + let build_target = *build_target; + + // Directory that will contain files specific to this build target + let build_target_dir = root_build_dir.join(build_target.android_abi()); + fs::create_dir_all(&build_target_dir).unwrap(); - let build_target_dir = root_build_dir.join(abi); + // Set environment variables needed for use with the cc crate + std::env::set_var("CC", util::find_clang(config, build_target)?); + std::env::set_var("CXX", util::find_clang_cpp(config, build_target)?); + std::env::set_var("AR", util::find_ar(config, build_target)?); + // Use libc++. It is current default C++ runtime + std::env::set_var("CXXSTDLIB", "c++"); + + // Generate cmake toolchain and set environment variables to allow projects which use the cmake crate to build correctly + let cmake_toolchain_path = write_cmake_toolchain(config, &build_target_dir, build_target)?; + std::env::set_var("CMAKE_TOOLCHAIN_FILE", cmake_toolchain_path); + std::env::set_var("CMAKE_GENERATOR", r#"Unix Makefiles"#); + std::env::set_var("CMAKE_MAKE_PROGRAM", util::make_path(config)); + + // Build android_native_glue and injected-glue let injected_glue_lib = build_injected_glue( workspace, config, @@ -45,46 +71,53 @@ pub fn build_static_libraries( build_target, )?; + let android_native_glue_object = build_android_native_glue( + config, + &android_native_glue_src_path, + &build_target_dir, + build_target, + )?; + // Configure compilation options so that we will build the desired build_target let mut opts = options.compile_options(workspace.config(), CompileMode::Build, Some(&workspace))?; - opts.build_config.requested_target = Some((*build_target).clone()); + opts.build_config.requested_target = Some(build_target.rust_triple().to_owned()); - // Create - let executor: Arc = Arc::new(StaticLibraryExecutor { + // Create executor + let config = Arc::new(config.clone()); + let executor: Arc = Arc::new(SharedLibraryExecutor { + config: Arc::clone(&config), build_target_dir: build_target_dir.clone(), injected_glue_lib, - targets: targets.clone(), + android_native_glue_object, + build_target, + shared_libraries: shared_libraries.clone(), }); // Compile all targets for the requested build target - // Hack to ignore expected error caused by the executor changing the targetkind and other settings. - // "error: failed to stat ... - let compilation_result = cargo::ops::compile_with_exec(workspace, &opts, &executor); - if let Err(err) = &compilation_result { - let mut output = String::new(); - fmt::write(&mut output, format_args!("{}", err))?; - if !output.contains(".fingerprint") { - compilation_result?; - } - } + cargo::ops::compile_with_exec(workspace, &opts, &executor)?; } // Remove the set of targets from the reference counted mutex - let mut targets = targets.lock().unwrap(); - let targets = std::mem::replace(&mut *targets, HashSet::new()); + let mut shared_libraries = shared_libraries.lock().unwrap(); + let shared_libraries = std::mem::replace(&mut *shared_libraries, MultiMap::new()); - Ok((targets, abis)) + Ok(SharedLibraries { shared_libraries }) } /// Executor which builds binary and example targets as static libraries -struct StaticLibraryExecutor { +struct SharedLibraryExecutor { + config: Arc, build_target_dir: PathBuf, injected_glue_lib: PathBuf, - targets: Arc>>, + android_native_glue_object: PathBuf, + build_target: AndroidBuildTarget, + + // Shared libraries built by the executor are added to this multimap + shared_libraries: Arc>>, } -impl<'a> Executor for StaticLibraryExecutor { +impl<'a> Executor for SharedLibraryExecutor { fn exec( &self, cmd: ProcessBuilder, @@ -160,61 +193,110 @@ pub extern "C" fn android_main(app: *mut ()) {{ }); if let Some(source_arg) = source_arg { - *source_arg = tmp_file.path.clone().into(); + // Build a new relative path to the temporary source file and use it as the source argument + // Using an absolute path causes compatibility issues in some cases under windows + // If a UNC path is used then relative paths used in "include* macros" may not work if + // the relative path includes "/" instead of "\" + let path_arg = Path::new(&source_arg); + let mut path_arg = path_arg.to_path_buf(); + path_arg.set_file_name(tmp_file.path.file_name().unwrap()); + *source_arg = path_arg.into_os_string(); } else { return Err(format_err!( - "Unable to replace source argument when buildin target '{}'", + "Unable to replace source argument when building target '{}'", target.name() )); } // - // Change target from bin to staticlib - // - for arg in &mut new_args { - if arg == "bin" { - *arg = "staticlib".into(); - } - } - - // - // Replace output directory with one inside the build target directory + // Create output directory inside the build target directory // let build_path = self.build_target_dir.join("build"); fs::create_dir_all(&build_path).unwrap(); + // + // Change crate-type from bin to dylib + // Replace output directory with the directory we created + // let mut iter = new_args.iter_mut().rev().peekable(); while let Some(arg) = iter.next() { if let Some(prev_arg) = iter.peek() { - if *prev_arg == "--out-dir" { + if *prev_arg == "--crate-type" && arg == "bin" { + *arg = "dylib".into(); + } else if *prev_arg == "--out-dir" { *arg = build_path.clone().into(); } } } - // Remove -C extra-filename argument - { - let mut extra_filename_index = None; - for (i, value) in new_args.iter().enumerate() { - if value.to_string_lossy().starts_with("extra-filename=") { - extra_filename_index = Some(i); - } - } - - if let Some(index) = extra_filename_index { - new_args.remove(index - 1); - new_args.remove(index - 1); - } + // Helper function to build arguments composed of concatenating two strings + fn build_arg(start: &str, end: impl AsRef) -> OsString { + let mut new_arg = OsString::new(); + new_arg.push(start); + new_arg.push(end.as_ref()); + new_arg } // // Inject crate dependency for injected glue // new_args.push("--extern".into()); - let mut arg = OsString::new(); - arg.push("cargo_apk_injected_glue="); - arg.push(&self.injected_glue_lib); - new_args.push(arg); + new_args.push(build_arg( + "cargo_apk_injected_glue=", + self.injected_glue_lib.as_os_str(), + )); + + // Determine paths + let tool_root = util::llvm_toolchain_root(&self.config); + let linker_path = tool_root + .join("bin") + .join(format!("{}-ld", &self.build_target.ndk_triple())); + let sysroot = tool_root.join("sysroot"); + let version_independent_libraries_path = sysroot + .join("usr") + .join("lib") + .join(&self.build_target.ndk_triple()); + let version_specific_libraries_path = + util::find_ndk_path(self.config.min_sdk_version, |platform| { + version_independent_libraries_path.join(platform.to_string()) + })?; + let gcc_lib_path = tool_root + .join("lib/gcc") + .join(&self.build_target.ndk_triple()) + .join("4.9.x"); + + // Add linker arguments + // Specify linker + new_args.push(build_arg("-Clinker=", linker_path)); + + // Set linker flavor + new_args.push("-Clinker-flavor=ld".into()); + + // Set system root + new_args.push(build_arg("-Clink-arg=--sysroot=", sysroot)); + + // Add version specific libraries directory to search path + new_args.push(build_arg("-Clink-arg=-L", version_specific_libraries_path)); + + // Add version independent libraries directory to search path + new_args.push(build_arg( + "-Clink-arg=-L", + &version_independent_libraries_path, + )); + + // Add path to folder containing libgcc.a to search path + new_args.push(build_arg("-Clink-arg=-L", gcc_lib_path)); + + // Add android native glue + new_args.push(build_arg("-Clink-arg=", &self.android_native_glue_object)); + + // Strip symbols for release builds + if self.config.release { + new_args.push("-Clink-arg=-strip-all".into()); + } + + // Require position independent code + new_args.push("-Crelocation-model=pic".into()); // Create new command let mut cmd = cmd.clone(); @@ -226,11 +308,44 @@ pub extern "C" fn android_main(app: *mut ()) {{ cmd.exec_with_streaming(on_stdout_line, on_stderr_line, false) .map(drop)?; - // Add target to target set - let mut targets = self.targets.lock().unwrap(); + // Execute the command again with the print flag to determine the name of the produced shared library and then add it to the list of shared librares to be added to the APK + let stdout = cmd.arg("--print").arg("file-names").exec_with_output()?; + let stdout = String::from_utf8(stdout.stdout).unwrap(); + let library_path = build_path.join(stdout.lines().next().unwrap()); + + let mut shared_libraries = self.shared_libraries.lock().unwrap(); + shared_libraries.insert( + target.clone(), + SharedLibrary { + abi: self.build_target, + path: library_path, + filename: format!("lib{}.so", target.name()), + }, + ); + + // If the target uses the C++ standard library, add the appropriate shared library + // to the list of shared libraries to be added to the APK + let mut iter = new_args.iter().peekable(); + let mut uses_cpp_standard_library = false; + while let Some(arg) = iter.next() { + if let Some(next_arg) = iter.peek() { + if arg == "-l" && *next_arg == "c++" { + uses_cpp_standard_library = true; + } + } + } - // Track the cargo targets that are built - targets.insert(target.clone()); + if uses_cpp_standard_library { + let cpp_library_path = version_independent_libraries_path.join("libc++_shared.so"); + shared_libraries.insert( + target.clone(), + SharedLibrary { + abi: self.build_target, + path: cpp_library_path, + filename: "libc++_shared.so".into(), + }, + ); + } } else if mode == CompileMode::Test { // This occurs when --all-targets is specified eprintln!("Ignoring CompileMode::Test for target: {}", target.name()); @@ -264,7 +379,7 @@ fn build_injected_glue( config: &AndroidConfig, injected_glue_src_path: &PathBuf, build_target_dir: &PathBuf, - build_target: &str, + build_target: AndroidBuildTarget, ) -> CargoResult { let rustc = workspace.config().load_global_rustc(Some(&workspace))?; let injected_glue_build_path = build_target_dir.join("injected-glue"); @@ -273,76 +388,99 @@ fn build_injected_glue( drop(writeln!( workspace.config().shell().err(), "Compiling injected-glue for {}", - build_target + build_target.rust_triple() )); let mut cmd = rustc.process(); cmd.arg(injected_glue_src_path) .arg("--edition") .arg("2018") .arg("--crate-type") - .arg("rlib"); + .arg("rlib") + .arg("-C") + .arg("relocation-model=pic"); if config.release { cmd.arg("-C").arg("opt-level=3"); } cmd.arg("--crate-name") .arg("cargo_apk_injected_glue") .arg("--target") - .arg(build_target) + .arg(build_target.rust_triple()) .arg("--out-dir") .arg(&injected_glue_build_path); cmd.exec()?; + // Run the compiler again with the print flag to determine the name of the produced rlib file let stdout = cmd.arg("--print").arg("file-names").exec_with_output()?; let stdout = String::from_utf8(stdout.stdout).unwrap(); Ok(injected_glue_build_path.join(stdout.lines().next().unwrap())) } -fn get_abi(build_target: &str) -> CargoResult<&str> { - Ok(if build_target == "armv7-linux-androideabi" { - "armeabi-v7a" - } else if build_target == "aarch64-linux-android" { - "arm64-v8a" - } else if build_target == "i686-linux-android" { - "x86" - } else if build_target == "x86_64-linux-android" { - "x86_64" - } else { - return Err(format_err!( - "Unknown or incompatible build target: {}", - build_target - )); - }) -} +/// Returns the path to the ".c" file for the android native app glue +fn write_native_app_glue_src(android_artifacts_dir: &Path) -> CargoResult { + let output_dir = android_artifacts_dir.join("native_app_glue"); + fs::create_dir_all(&output_dir).unwrap(); + + let mut h_file = File::create(output_dir.join("android_native_app_glue.h"))?; + h_file.write_all(&include_bytes!("../../../native_app_glue/android_native_app_glue.h")[..])?; -/// Temporary file implementation that allows creating a file with a specified path which -/// will be deleted when dropped. -struct TempFile { - path: PathBuf, + let c_path = output_dir.join("android_native_app_glue.c"); + let mut c_file = File::create(&c_path)?; + c_file.write_all(&include_bytes!("../../../native_app_glue/android_native_app_glue.c")[..])?; + + Ok(c_path) } -impl TempFile { - /// Create a new `TempFile` using the contents provided by a closure. - /// If the file already exists, it will be overwritten and then deleted when the instance - /// is dropped. - fn new(path: PathBuf, write_contents: F) -> CargoResult - where - F: FnOnce(&mut File) -> CargoResult<()>, - { - let tmp_file = TempFile { path }; - - // Write the contents to the the temp file - let mut file = File::create(&tmp_file.path)?; - write_contents(&mut file)?; - - Ok(tmp_file) - } +/// Returns the path to the built object file for the android native glue +fn build_android_native_glue( + config: &AndroidConfig, + android_native_glue_src_path: &PathBuf, + build_target_dir: &PathBuf, + build_target: AndroidBuildTarget, +) -> CargoResult { + let clang = util::find_clang(config, build_target)?; + + let android_native_glue_build_path = build_target_dir.join("android_native_glue"); + fs::create_dir_all(&android_native_glue_build_path)?; + let android_native_glue_object_path = + android_native_glue_build_path.join("android_native_glue.o"); + + // Will produce warnings when bulding on linux? Create constants for extensions that can be used.. Or have separate functions? + util::script_process(clang) + .arg(android_native_glue_src_path) + .arg("-c") + .arg("-o") + .arg(&android_native_glue_object_path) + .exec()?; + + Ok(android_native_glue_object_path) } -impl Drop for TempFile { - fn drop(&mut self) { - // Ignore failure to remove file - let _ = fs::remove_file(&self.path); - } +/// Write a CMake toolchain which will remove references to the rustc build target before including +/// the NDK provided toolchain. The NDK provided android toolchain will set the target appropriately +/// Returns the path to the generated toolchain file +fn write_cmake_toolchain( + config: &AndroidConfig, + build_target_dir: &PathBuf, + build_target: AndroidBuildTarget, +) -> CargoResult { + let toolchain_path = build_target_dir.join("cargo-apk.toolchain.cmake"); + let mut toolchain_file = File::create(&toolchain_path).unwrap(); + writeln!( + toolchain_file, + r#"set(ANDROID_PLATFORM android-{min_sdk_version}) +set(ANDROID_ABI {abi}) +string(REPLACE "--target={build_target}" "" CMAKE_C_FLAGS "${{CMAKE_C_FLAGS}}") +string(REPLACE "--target={build_target}" "" CMAKE_CXX_FLAGS "${{CMAKE_CXX_FLAGS}}") +unset(CMAKE_C_COMPILER CACHE) +unset(CMAKE_CXX_COMPILER CACHE) +include("{ndk_path}/build/cmake/android.toolchain.cmake")"#, + min_sdk_version = config.min_sdk_version, + ndk_path = config.ndk_path.to_string_lossy().replace("\\", "/"), // Use forward slashes even on windows to avoid path escaping issues. + build_target = build_target.rust_triple(), + abi = build_target.android_abi(), + )?; + + Ok(toolchain_path) } diff --git a/cargo-apk/src/ops/build/targets.rs b/cargo-apk/src/ops/build/targets.rs new file mode 100644 index 0000000..32de70c --- /dev/null +++ b/cargo-apk/src/ops/build/targets.rs @@ -0,0 +1,43 @@ +use crate::config::AndroidBuildTarget; + +impl AndroidBuildTarget { + /// Identifier used in the NDK to refer to the ABI + pub fn android_abi(self) -> &'static str { + match self { + AndroidBuildTarget::ArmV7a => "armeabi-v7a", + AndroidBuildTarget::Arm64V8a => "arm64-v8a", + AndroidBuildTarget::X86 => "x86", + AndroidBuildTarget::X86_64 => "x86_64", + } + } + + /// Returns the triple used by the rust build tools + pub fn rust_triple(self) -> &'static str { + match self { + AndroidBuildTarget::ArmV7a => "armv7-linux-androideabi", + AndroidBuildTarget::Arm64V8a => "aarch64-linux-android", + AndroidBuildTarget::X86 => "i686-linux-android", + AndroidBuildTarget::X86_64 => "x86_64-linux-android", + } + } + + // Returns the triple NDK provided LLVM + pub fn ndk_llvm_triple(self) -> &'static str { + match self { + AndroidBuildTarget::ArmV7a => "armv7a-linux-androideabi", + AndroidBuildTarget::Arm64V8a => "aarch64-linux-android", + AndroidBuildTarget::X86 => "i686-linux-android", + AndroidBuildTarget::X86_64 => "x86_64-linux-android", + } + } + + /// Returns the triple used by the non-LLVM parts of the NDK + pub fn ndk_triple(self) -> &'static str { + match self { + AndroidBuildTarget::ArmV7a => "arm-linux-androideabi", + AndroidBuildTarget::Arm64V8a => "aarch64-linux-android", + AndroidBuildTarget::X86 => "i686-linux-android", + AndroidBuildTarget::X86_64 => "x86_64-linux-android", + } + } +} diff --git a/cargo-apk/src/ops/build/tempfile.rs b/cargo-apk/src/ops/build/tempfile.rs new file mode 100644 index 0000000..3501f92 --- /dev/null +++ b/cargo-apk/src/ops/build/tempfile.rs @@ -0,0 +1,39 @@ +use cargo::util::CargoResult; +use std::fs::{self, File}; +use std::path::PathBuf; + +/// Temporary file implementation that allows creating a file with a specified path which +/// will be deleted when dropped. +pub struct TempFile { + pub path: PathBuf, +} + +impl TempFile { + /// Create a new `TempFile` using the contents provided by a closure. + /// If the file already exists, it will be overwritten and then deleted when the instance + /// is dropped. + pub fn new(path: PathBuf, write_contents: F) -> CargoResult + where + F: FnOnce(&mut File) -> CargoResult<()>, + { + let tmp_file = TempFile { path }; + + // Write the contents to the the temp file + let mut file = File::create(&tmp_file.path)?; + write_contents(&mut file)?; + + Ok(tmp_file) + } +} + +impl Drop for TempFile { + fn drop(&mut self) { + fs::remove_file(&self.path).unwrap_or_else(|e| { + eprintln!( + "Unable to remove temporary file: {}. {}", + &self.path.to_string_lossy(), + &e + ); + }) + } +} diff --git a/cargo-apk/src/ops/build/util.rs b/cargo-apk/src/ops/build/util.rs new file mode 100644 index 0000000..96454d4 --- /dev/null +++ b/cargo-apk/src/ops/build/util.rs @@ -0,0 +1,183 @@ +use crate::config::{AndroidBuildTarget, AndroidConfig}; +use cargo::core::{Target, TargetKind, Workspace}; +use cargo::util::{process, CargoResult, ProcessBuilder}; +use failure::format_err; +use std::ffi::OsStr; +use std::path::PathBuf; + +/// Returns the directory in which all cargo apk artifacts for the current +/// debug/release configuration should be produced. +pub fn get_root_build_directory(workspace: &Workspace, config: &AndroidConfig) -> PathBuf { + let android_artifacts_dir = workspace + .target_dir() + .join("android-artifacts") + .into_path_unlocked(); + + if config.release { + android_artifacts_dir.join("release") + } else { + android_artifacts_dir.join("debug") + } +} + +/// Returns the sub directory within the root build directory for the specified target. +pub fn get_target_directory(root_build_dir: &PathBuf, target: &Target) -> CargoResult { + let target_directory = match target.kind() { + TargetKind::Bin => root_build_dir.join("bin"), + TargetKind::ExampleBin => root_build_dir.join("examples"), + _ => unreachable!("Unexpected target kind"), + }; + + let target_directory = target_directory.join(target.name()); + Ok(target_directory) +} + +/// Returns path to NDK provided make +pub fn make_path(config: &AndroidConfig) -> PathBuf { + config.ndk_path.join("prebuild").join(HOST_TAG).join("make") +} + +/// Returns the path to the LLVM toolchain provided by the NDK +pub fn llvm_toolchain_root(config: &AndroidConfig) -> PathBuf { + config + .ndk_path + .join("toolchains") + .join("llvm") + .join("prebuilt") + .join(HOST_TAG) +} + +// Helper function for looking for a path based on the platform version +// Calls a closure for each attempt and then return the PathBuf for the first file that exists. +// Uses approach that NDK build tools use which is described at: +// https://developer.android.com/ndk/guides/application_mk +// " - The platform version matching APP_PLATFORM. +// - The next available API level below APP_PLATFORM. For example, android-19 will be used when +// APP_PLATFORM is android-20, since there were no new native APIs in android-20. +// - The minimum API level supported by the NDK." +pub fn find_ndk_path(platform: u32, path_builder: F) -> CargoResult +where + F: Fn(u32) -> PathBuf, +{ + let mut tmp_platform = platform; + + // Look for the file which matches the specified platform + // If that doesn't exist, look for a lower version + while tmp_platform > 1 { + let path = path_builder(tmp_platform); + if path.exists() { + return Ok(path); + } + + tmp_platform -= 1; + } + + // If that doesn't exist... Look for a higher one. This would be the minimum API level supported by the NDK + tmp_platform = platform; + while tmp_platform < 100 { + let path = path_builder(tmp_platform); + if path.exists() { + return Ok(path); + } + + tmp_platform += 1; + } + + Err(format_err!("Unable to find NDK file")) +} + +// Returns path to clang executable/script that should be used to build the target +pub fn find_clang( + config: &AndroidConfig, + build_target: AndroidBuildTarget, +) -> CargoResult { + let bin_folder = llvm_toolchain_root(config).join("bin"); + find_ndk_path(config.min_sdk_version, |platform| { + bin_folder.join(format!( + "{}{}-clang{}", + build_target.ndk_llvm_triple(), + platform, + EXECUTABLE_SUFFIX_CMD + )) + }) + .map_err(|_| format_err!("Unable to find NDK clang")) +} + +// Returns path to clang++ executable/script that should be used to build the target +pub fn find_clang_cpp( + config: &AndroidConfig, + build_target: AndroidBuildTarget, +) -> CargoResult { + let bin_folder = llvm_toolchain_root(config).join("bin"); + find_ndk_path(config.min_sdk_version, |platform| { + bin_folder.join(format!( + "{}{}-clang++{}", + build_target.ndk_llvm_triple(), + platform, + EXECUTABLE_SUFFIX_CMD + )) + }) + .map_err(|_| format_err!("Unable to find NDK clang++")) +} + +// Returns path toe ar tool. +pub fn find_ar(config: &AndroidConfig, build_target: AndroidBuildTarget) -> CargoResult { + let ar_path = llvm_toolchain_root(config).join("bin").join(format!( + "{}-ar{}", + build_target.ndk_triple(), + EXECUTABLE_SUFFIX_EXE + )); + if ar_path.exists() { + Ok(ar_path) + } else { + Err(format_err!( + "Unable to find AR file at `{}`", + ar_path.to_string_lossy() + )) + } +} + +/// Returns a ProcessBuilder which runs the specified command. Uses "cmd" on windows in order to +/// allow execution of batch files. +pub fn script_process(cmd: impl AsRef) -> ProcessBuilder { + if cfg!(target_os = "windows") { + let mut pb = process("cmd"); + pb.arg("/C").arg(cmd); + pb + } else { + process(cmd) + } +} + +#[cfg(all(target_os = "windows", target_pointer_width = "64"))] +const HOST_TAG: &str = "windows-x86_64"; + +#[cfg(all(target_os = "windows", target_pointer_width = "32"))] +const HOST_TAG: &str = "windows"; + +#[cfg(target_os = "linux")] +const HOST_TAG: &str = "linux-x86_64"; + +#[cfg(target_os = "macos")] +const HOST_TAG: &str = "darwin-x86_64"; + +// These are executable suffixes used to simplify building commands. +// On non-windows platforms they are empty. + +#[cfg(target_os = "windows")] +const EXECUTABLE_SUFFIX_EXE: &str = ".exe"; + +#[cfg(not(target_os = "windows"))] +const EXECUTABLE_SUFFIX_EXE: &str = ""; + +#[cfg(target_os = "windows")] +const EXECUTABLE_SUFFIX_CMD: &str = ".cmd"; + +#[cfg(not(target_os = "windows"))] +const EXECUTABLE_SUFFIX_CMD: &str = ""; + +#[cfg(target_os = "windows")] +pub const EXECUTABLE_SUFFIX_BAT: &str = ".bat"; + +#[cfg(not(target_os = "windows"))] +pub const EXECUTABLE_SUFFIX_BAT: &str = ""; diff --git a/cargo-apk/src/ops/install.rs b/cargo-apk/src/ops/install.rs index 55cd232..aca7fde 100644 --- a/cargo-apk/src/ops/install.rs +++ b/cargo-apk/src/ops/install.rs @@ -24,7 +24,7 @@ pub fn install( process(&adb) .arg("install") - .arg("-r") // TODO: let user choose + .arg("-r") .arg(apk_path) .exec()?; } diff --git a/cargo-apk/tests/cc/Cargo.lock b/cargo-apk/tests/cc/Cargo.lock new file mode 100644 index 0000000..43bdcd9 --- /dev/null +++ b/cargo-apk/tests/cc/Cargo.lock @@ -0,0 +1,16 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "cc" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "test_cc-rs" +version = "0.1.0" +dependencies = [ + "cc 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum cc 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "ce400c638d48ee0e9ab75aef7997609ec57367ccfe1463f21bf53c3eca67bf46" diff --git a/cargo-apk/tests/cc/Cargo.toml b/cargo-apk/tests/cc/Cargo.toml new file mode 100644 index 0000000..da7885e --- /dev/null +++ b/cargo-apk/tests/cc/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "test_cc-rs" +version = "0.1.0" +authors = ["Philip Alldredge "] +description = "Simple test to ensure cargo-apk works with crates that use cc crate to build C and C++ static libraries." +edition = "2018" +publish = false + +[package.metadata.android] +build_targets = [ "armv7-linux-androideabi", "aarch64-linux-android", "i686-linux-android", "x86_64-linux-android" ] + +[build-dependencies] +cc = "1.0" diff --git a/cargo-apk/tests/cc/build.rs b/cargo-apk/tests/cc/build.rs new file mode 100644 index 0000000..7b2c113 --- /dev/null +++ b/cargo-apk/tests/cc/build.rs @@ -0,0 +1,10 @@ +fn main() { + cc::Build::new() + .file("./cc_src/ctest.c") + .compile("ctest"); + + cc::Build::new() + .cpp(true) + .file("./cc_src/cpptest.cpp") + .compile("cpptest"); +} diff --git a/cargo-apk/tests/cc/cc_src/cpptest.cpp b/cargo-apk/tests/cc/cc_src/cpptest.cpp new file mode 100644 index 0000000..6318690 --- /dev/null +++ b/cargo-apk/tests/cc/cc_src/cpptest.cpp @@ -0,0 +1,11 @@ +#include +#include + +extern "C" int32_t multiply_by_four(int32_t value) { + return value * 4; +} + +// Print using std::cout to verify C++ standard library is working properly. +extern "C" void print_value(int32_t value) { + std::cout << "Value printed from cout: " << value << std::endl; +} \ No newline at end of file diff --git a/cargo-apk/tests/cc/cc_src/ctest.c b/cargo-apk/tests/cc/cc_src/ctest.c new file mode 100644 index 0000000..b6523c3 --- /dev/null +++ b/cargo-apk/tests/cc/cc_src/ctest.c @@ -0,0 +1,5 @@ +#include + +int32_t add_two(int32_t value) { + return value + 2; +} \ No newline at end of file diff --git a/cargo-apk/tests/cc/src/main.rs b/cargo-apk/tests/cc/src/main.rs new file mode 100644 index 0000000..c03c4d7 --- /dev/null +++ b/cargo-apk/tests/cc/src/main.rs @@ -0,0 +1,25 @@ +fn main() { + println!("Android cc test"); + let result = unsafe { + multiply_by_four(add_two(4)) + }; + + println!("multiply_by_four(add_two(4)): {}", result); + + println!("Printing value using c++ library:"); + unsafe { + print_value(result); + } +} + +#[link(name = "ctest")] +extern "C" { + fn add_two(value : i32) -> i32; +} + +#[link(name = "cpptest")] +extern "C" { + fn multiply_by_four(value : i32) -> i32; + fn print_value(value : i32) -> std::ffi::c_void; +} + diff --git a/cargo-apk/tests/cli.rs b/cargo-apk/tests/cli.rs index 0b876d8..5c01b81 100644 --- a/cargo-apk/tests/cli.rs +++ b/cargo-apk/tests/cli.rs @@ -31,6 +31,21 @@ fn check_inner_attributes_test() { build_test("inner_attributes"); } +#[test] +fn check_native_library_test() { + build_test("native-library"); +} + +#[test] +fn check_cc_test() { + build_test("cc"); +} + +#[test] +fn check_cmake_test() { + build_test("cmake"); +} + fn build_example(directory_name: &str) { build_package(&format!("../examples/{}/", directory_name)); } diff --git a/cargo-apk/tests/cmake/Cargo.lock b/cargo-apk/tests/cmake/Cargo.lock new file mode 100644 index 0000000..f96faa1 --- /dev/null +++ b/cargo-apk/tests/cmake/Cargo.lock @@ -0,0 +1,25 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "cc" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cmake" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cc 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "test_cmake-rs" +version = "0.1.0" +dependencies = [ + "cmake 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum cc 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "ce400c638d48ee0e9ab75aef7997609ec57367ccfe1463f21bf53c3eca67bf46" +"checksum cmake 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)" = "2ca4386c8954b76a8415b63959337d940d724b336cabd3afe189c2b51a7e1ff0" diff --git a/cargo-apk/tests/cmake/Cargo.toml b/cargo-apk/tests/cmake/Cargo.toml new file mode 100644 index 0000000..ec62a11 --- /dev/null +++ b/cargo-apk/tests/cmake/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "test_cmake-rs" +version = "0.1.0" +authors = ["Philip Alldredge "] +description = "Simple test to ensure cargo-apk works with crates that use cmake crate" +edition = "2018" +publish = false + +[package.metadata.android] +build_targets = [ "armv7-linux-androideabi", "aarch64-linux-android", "i686-linux-android", "x86_64-linux-android" ] + +[build-dependencies] +cmake = "0.1" diff --git a/cargo-apk/tests/cmake/build.rs b/cargo-apk/tests/cmake/build.rs new file mode 100644 index 0000000..a1b4b34 --- /dev/null +++ b/cargo-apk/tests/cmake/build.rs @@ -0,0 +1,8 @@ +use cmake::Config; + +fn main() { + let dst = Config::new("libcmaketest") + .build(); + println!("cargo:rustc-link-search=native={}", dst.display()); + println!("cargo:rustc-link-lib=static=cmaketest"); +} diff --git a/cargo-apk/tests/cmake/libcmaketest/CMakeLists.txt b/cargo-apk/tests/cmake/libcmaketest/CMakeLists.txt new file mode 100644 index 0000000..b79cdbb --- /dev/null +++ b/cargo-apk/tests/cmake/libcmaketest/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 2.8.9) +project(cmaketest) + +set(CMAKE_BUILD_TYPE Release) +ADD_LIBRARY(cmaketest STATIC cmaketest.c ) + +install(TARGETS cmaketest DESTINATION .) \ No newline at end of file diff --git a/cargo-apk/tests/cmake/libcmaketest/cmaketest.c b/cargo-apk/tests/cmake/libcmaketest/cmaketest.c new file mode 100644 index 0000000..9df2cea --- /dev/null +++ b/cargo-apk/tests/cmake/libcmaketest/cmaketest.c @@ -0,0 +1,3 @@ +int multiply_by_10(int value) { + return value * 10; +} \ No newline at end of file diff --git a/cargo-apk/tests/cmake/src/main.rs b/cargo-apk/tests/cmake/src/main.rs new file mode 100644 index 0000000..4f98da1 --- /dev/null +++ b/cargo-apk/tests/cmake/src/main.rs @@ -0,0 +1,14 @@ +fn main() { + println!("Android cmake test"); + let result = unsafe { + multiply_by_10(2) + }; + + println!("multiply_by_10(2): {}", result); +} + + +#[link(name = "cmaketest")] +extern "C" { + fn multiply_by_10(value : i32) -> i32; +} diff --git a/cargo-apk/tests/inner_attributes/Cargo.lock b/cargo-apk/tests/inner_attributes/Cargo.lock index 6320465..022ef0b 100644 --- a/cargo-apk/tests/inner_attributes/Cargo.lock +++ b/cargo-apk/tests/inner_attributes/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. [[package]] -name = "android_glue_test_inner_attributes" +name = "test_inner_attributes" version = "0.1.0" diff --git a/cargo-apk/tests/inner_attributes/Cargo.toml b/cargo-apk/tests/inner_attributes/Cargo.toml index f93c95b..29a4b90 100644 --- a/cargo-apk/tests/inner_attributes/Cargo.toml +++ b/cargo-apk/tests/inner_attributes/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "android_glue_test_inner_attributes" +name = "test_inner_attributes" version = "0.1.0" authors = ["Philip Alldredge "] edition = "2018" diff --git a/cargo-apk/tests/inner_attributes/src/main.rs b/cargo-apk/tests/inner_attributes/src/main.rs index 65dc9c8..f8d5ee9 100644 --- a/cargo-apk/tests/inner_attributes/src/main.rs +++ b/cargo-apk/tests/inner_attributes/src/main.rs @@ -3,3 +3,4 @@ fn main() { println!("Inner attributes test"); } + diff --git a/cargo-apk/tests/native-library/Cargo.lock b/cargo-apk/tests/native-library/Cargo.lock new file mode 100644 index 0000000..854d623 --- /dev/null +++ b/cargo-apk/tests/native-library/Cargo.lock @@ -0,0 +1,6 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "test_native-library" +version = "0.1.0" + diff --git a/cargo-apk/tests/native-library/Cargo.toml b/cargo-apk/tests/native-library/Cargo.toml new file mode 100644 index 0000000..894004d --- /dev/null +++ b/cargo-apk/tests/native-library/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "test_native-library" +version = "0.1.0" +authors = ["Philip Alldredge "] +edition = "2018" +publish = false diff --git a/cargo-apk/tests/native-library/src/main.rs b/cargo-apk/tests/native-library/src/main.rs new file mode 100644 index 0000000..1822545 --- /dev/null +++ b/cargo-apk/tests/native-library/src/main.rs @@ -0,0 +1,15 @@ +#![cfg(target_os = "android")] +use std::ffi::c_void; +use std::ptr; + +fn main() { + println!("Android native library test"); + let display = unsafe { eglGetDisplay(ptr::null()) }; + println!("eglGetDisplay(0) result: {:?}", display); +} + +// Link to the EGL to ensure that additional libraries can be linked +#[link(name = "EGL")] +extern "C" { + fn eglGetDisplay(native_display : *const c_void) -> *mut c_void; +} \ No newline at end of file