Skip to content

Commit

Permalink
feat: Do not ship Mapeo for ICCAs assets with Mapeo (#447)
Browse files Browse the repository at this point in the history
* feat: Create ICCAs build scripts & exclude ICCA screens from main build

Use Metro build config to conditionally include ICCA screens so that
ICCA resouces are not included in the main Mapeo build. This avoids the
images used in the ICCAs intro screens increasing the file size of the
main Mapeo APK.

* feat: Ship apk variants with only the presets they need

Choose the presets that are included in the apk at build time instead of
runtime. This reduces the apk size and removes runtime code for
conditionally running code based on app variant.

* chore: Patch react-native so gradle tasks package ICCA variant

Adds the app variant name as an environment variable in the gradle build
script so that metro bundler can package JS files according to variant
by using the `sourceExts` option.
  • Loading branch information
gmaclennan committed Aug 6, 2020
1 parent 08fe2d6 commit c7df1fd
Show file tree
Hide file tree
Showing 16 changed files with 259 additions and 141 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,8 @@ buck-out/
# Private env variables
.env
.bitrise.secrets.yml

# nodejs-mobile assets generated by build script
# TODO: somehow modify gradle build scripts so that these can be stored in
# a build folder rather than in these app/src folders
/android/app/src/*/assets/nodejs-assets
16 changes: 16 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,22 @@ alongside existing copies of the app on their phone. It has a red logo.
This is the main variant of the app for our partners and the public. It has a
dark blue logo and Application ID `com.mapeo`.

### Mapeo for ICCAs Variant

This is a special variant created for the WCMC-UNEP for using Mapeo to map ICCAs. To build the Mapeo for ICCAs Variant, run:

```sh
npm run build:release-icca
```

To develop with the debug build of Mapeo for ICCAs:

```sh
npm run start-icca
# In another tab
npm run android-icca
```

## Releases & Builds

Mapeo is built using the CI service
Expand Down
3 changes: 3 additions & 0 deletions android/app/src/icca/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Mapeo for ICCAs</string>
</resources>
3 changes: 3 additions & 0 deletions android/app/src/iccaDebug/res/values/strings.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">Mapeo ICCAs Dev</string>
</resources>
28 changes: 23 additions & 5 deletions metro.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,35 @@
* @format
*/
const blacklist = require("metro-config/src/defaults/blacklist");
const defaultSourceExts = require("metro-config/src/defaults/defaults")
.sourceExts;

const customSourceExts = [];

if (process.env.APP_VARIANT) {
const match = process.env.APP_VARIANT.match(/^[^A-Z]*/);
if (match) {
customSourceExts.push(match[0] + ".js");
}
}

if (process.env.RN_SRC_EXT) {
process.env.RN_SRC_EXT.split(",").forEach(ext => {
customSourceExts.push(ext);
});
}

module.exports = {
transformer: {
getTransformOptions: async () => ({
transform: {
experimentalImportSupport: false,
inlineRequires: false
}
})
inlineRequires: false,
},
}),
},
resolver: {
blacklistRE: blacklist([/nodejs-assets\/.*/, /android\/.*/, /ios\/.*/])
}
blacklistRE: blacklist([/nodejs-assets\/.*/, /android\/.*/, /ios\/.*/]),
sourceExts: customSourceExts.concat(defaultSourceExts),
},
};
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@
},
"scripts": {
"start": "react-native start",
"start-icca": "RN_SRC_EXT=icca.js react-native start",
"android": "npm run build:backend && npm run android-no-backend-rebuild",
"android-no-backend-rebuild": "react-native run-android --variant=appDebug --appIdSuffix=debug",
"android-storybook": "react-native run-android --variant=storybookDebug --appIdSuffix=storybook.debug",
"android-icca": "react-native run-android --variant=iccaDebug --appIdSuffix=debug",
"android-icca": "RN_SRC_EXT=icca.js react-native run-android --variant=iccaDebug --appIdSuffix=icca.debug",
"build:backend": "./scripts/build-backend.sh",
"build:translations": "node ./scripts/build-translations.js",
"build:release": "npm run build:translations && npm run build:backend && ./scripts/build-release-android.sh",
"build:release-icca": "npm run build:translations && npm run build:backend && RN_SRC_EXT=icca.js ./scripts/build-release-android.sh",
"build:storybook": "./node_modules/@storybook/react/bin/build.js -c ./storybook-web -s ./public -o ./build --no-dll",
"test": "jest",
"lint": "eslint *.js \"src/**/*.js\"",
Expand Down
17 changes: 17 additions & 0 deletions patches/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,20 @@

These patches use [`patch-package`](https://www.npmjs.com/package/patch-package)
to update dependencies which have unpublished fixes.

## `nodejs-mobile-react-native`

This patch additionally extracts files in the folder `assets/nodejs-assets` so
that they can be accessed by the nodejs process. This is necessary so that
different variants (Mapeo for ICCAs vs normal Mapeo) can each ship their own
assets (e.g. presets)

## `react-native`

This patch adds an environment variable `APP_VARIANT` with the app variant name
when calling `react-native bundle` in the react gradle script. This is used to
define custom `sourceExts` for Metro bundler, so we can package specific code
based on application variant. It is necessary to patch this file rather than
just add an environment variable as part of the build script because it allows
gradle tasks like `./gradlew assembleRelease` to work as expected — each variant
will be built correctly with the correct bundled files.
38 changes: 38 additions & 0 deletions patches/nodejs-mobile-react-native+0.6.1.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
diff --git a/node_modules/nodejs-mobile-react-native/android/src/main/java/com/janeasystems/rn_nodejs_mobile/RNNodeJsMobileModule.java b/node_modules/nodejs-mobile-react-native/android/src/main/java/com/janeasystems/rn_nodejs_mobile/RNNodeJsMobileModule.java
index e882a0c..02616d0 100644
--- a/node_modules/nodejs-mobile-react-native/android/src/main/java/com/janeasystems/rn_nodejs_mobile/RNNodeJsMobileModule.java
+++ b/node_modules/nodejs-mobile-react-native/android/src/main/java/com/janeasystems/rn_nodejs_mobile/RNNodeJsMobileModule.java
@@ -32,6 +32,7 @@ public class RNNodeJsMobileModule extends ReactContextBaseJavaModule implements
private final ReactApplicationContext reactContext;
private static final String TAG = "NODEJS-RN";
private static final String NODEJS_PROJECT_DIR = "nodejs-project";
+ private static final String NODEJS_ASSETS_DIR = "nodejs-assets";
private static final String NODEJS_BUILTIN_MODULES = "nodejs-builtin_modules";
private static final String TRASH_DIR = "nodejs-project-trash";
private static final String SHARED_PREFS = "NODEJS_MOBILE_PREFS";
@@ -42,6 +43,7 @@ public class RNNodeJsMobileModule extends ReactContextBaseJavaModule implements
private static String trashDirPath;
private static String filesDirPath;
private static String nodeJsProjectPath;
+ private static String nodeJsAssetsPath;
private static String builtinModulesPath;
private static String nativeAssetsPath;

@@ -74,6 +76,7 @@ public class RNNodeJsMobileModule extends ReactContextBaseJavaModule implements

// The paths where we expect the node project assets to be at runtime.
nodeJsProjectPath = filesDirPath + "/" + NODEJS_PROJECT_DIR;
+ nodeJsAssetsPath = filesDirPath + "/" + NODEJS_ASSETS_DIR;
builtinModulesPath = filesDirPath + "/" + NODEJS_BUILTIN_MODULES;
trashDirPath = filesDirPath + "/" + TRASH_DIR;
nativeAssetsPath = BUILTIN_NATIVE_ASSETS_PREFIX + getCurrentABIName();
@@ -387,6 +390,9 @@ public class RNNodeJsMobileModule extends ReactContextBaseJavaModule implements
// Copy the nodejs built-in modules to the application's data path.
copyAssetFolder("builtin_modules", builtinModulesPath);

+ // Copy nodejs assets (e.g. presets) which can vary between variants
+ copyAssetFolder("nodejs-assets", nodeJsAssetsPath);
+
saveLastUpdateTime();
Log.d(TAG, "Node assets copy completed successfully");
}
13 changes: 13 additions & 0 deletions patches/react-native+0.62.2.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
diff --git a/node_modules/react-native/react.gradle b/node_modules/react-native/react.gradle
index d886a9b..444c345 100644
--- a/node_modules/react-native/react.gradle
+++ b/node_modules/react-native/react.gradle
@@ -153,6 +153,8 @@ afterEvaluate {
extraArgs.add(bundleConfig);
}

+ environment("APP_VARIANT", variant.name);
+
commandLine(*execCommand, bundleCommand, "--platform", "android", "--dev", "${devEnabled}",
"--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir,
"--sourcemap-output", enableHermes ? jsPackagerSourceMapFile : jsOutputSourceMapFile, *extraArgs)
15 changes: 11 additions & 4 deletions scripts/build-backend.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,18 @@ cd ../..
echo -en " done.\n"

echo -en "Create presets fallback folders..."
ANDROID_SRC_DIR="$(pwd)/android/app/src"
cd ./nodejs-assets/nodejs-project
mkdir -p presets
mkdir -p presets-icca
mv ./node_modules/mapeo-default-settings/dist ./presets/default
mv ./node_modules/mapeo-config-icca/dist ./presets-icca/default
# We use a patched version of nodejs-mobile-react-native that extracts files in
# the assets/nodejs-assets folder, so that they are accessible to Node. Here we
# copy default presets into assets folders by variant, so that the icca variant
# has its own custom presets shipped with the app.
rm -rf "${ANDROID_SRC_DIR}/main/assets/nodejs-assets/presets"
rm -rf "${ANDROID_SRC_DIR}/icca/assets/nodejs-assets/presets"
mkdir -p "${ANDROID_SRC_DIR}/main/assets/nodejs-assets/presets"
mkdir -p "${ANDROID_SRC_DIR}/icca/assets/nodejs-assets/presets"
mv ./node_modules/mapeo-default-settings/dist "${ANDROID_SRC_DIR}/main/assets/nodejs-assets/presets/default"
mv ./node_modules/mapeo-config-icca/dist "${ANDROID_SRC_DIR}/icca/assets/nodejs-assets/presets/default"
cd ../..
echo -en " done.\n"

Expand Down
10 changes: 2 additions & 8 deletions src/backend/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ const log = debug("mapeo-core:index");
const PORT = 9081;
const status = new ServerStatus();
let storagePath;
let flavor;
let server;

// This is nastily circular: we need an instance of status for the constructor
Expand Down Expand Up @@ -75,23 +74,18 @@ status.startHeartbeat();
*/
rnBridge.channel.on("config", config => {
log("storagePath", config.storagePath);
log("flavor", config.flavor);
if (config.storagePath === storagePath && config.flavor === flavor) return;
if (config.storagePath === storagePath) return;
const prevStoragePath = storagePath;
const prevFlavor = flavor;
if (server)
stopServer(() => {
log(`closed server with:
storagePath: ${prevStoragePath}
flavor: ${prevFlavor}`);
storagePath: ${prevStoragePath}`);
});
storagePath = config.storagePath;
flavor = config.flavor;
try {
server = createServer({
privateStorage: rnBridge.app.datadir(),
sharedStorage: storagePath,
flavor: flavor,
});
} catch (error) {
status.setState(constants.ERROR, { error, context: "createServer" });
Expand Down
7 changes: 2 additions & 5 deletions src/backend/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,11 @@ module.exports = createServer;

function createServer({ privateStorage, sharedStorage, flavor }) {
const defaultConfigPath = path.join(sharedStorage, "presets/default");
log("Creating server", { flavor });
log("Creating server");

// Folder with default (built-in) presets to server when the user has not
// added any presets
const fallbackPresetsDir = path.join(
process.cwd(),
flavor === "icca" ? "presets-icca" : "presets"
);
const fallbackPresetsDir = path.join(privateStorage, "nodejs-assets/presets");

// create folders for presets & styles
mkdirp.sync(defaultConfigPath);
Expand Down
17 changes: 17 additions & 0 deletions src/frontend/AppContainer.icca.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// @flow
import AppStack from "./AppStack";
import IntroStack from "./screens/Intro";
import { createAppContainer, createSwitchNavigator } from "react-navigation";

const RootStack = createSwitchNavigator(
{
Intro: IntroStack,
App: AppStack,
},
{
initialRouteName: "Intro",
}
);

// $FlowFixMe
export default createAppContainer(RootStack);
119 changes: 3 additions & 116 deletions src/frontend/AppContainer.js
Original file line number Diff line number Diff line change
@@ -1,119 +1,6 @@
// @flow
import React from "react";
import { createAppContainer, createSwitchNavigator } from "react-navigation";
import { createBottomTabNavigator } from "react-navigation-tabs";
import { createStackNavigator } from "react-navigation-stack";
import MaterialIcons from "react-native-vector-icons/MaterialIcons";
import BuildConfig from "react-native-build-config";

import MapScreen from "./screens/MapScreen";
import CameraScreen from "./screens/CameraScreen";
import ObservationList from "./screens/ObservationsList";
import Observation from "./screens/Observation";
import ObservationEdit from "./screens/ObservationEdit";
import AddPhoto from "./screens/AddPhoto";
import ObservationDetails from "./screens/ObservationDetails";
import CategoryChooser from "./screens/CategoryChooser";
import GpsModal from "./screens/GpsModal";
import SyncModal from "./screens/SyncModal";
import Settings from "./screens/Settings";
import PhotosModal from "./screens/PhotosModal";
import ManualGpsScreen from "./screens/ManualGpsScreen";
import CustomHeaderLeft from "./sharedComponents/CustomHeaderLeft";
import ProjectConfig from "./screens/Settings/ProjectConfig";
import LanguageSettings from "./screens/Settings/LanguageSettings";
import IntroStack from "./screens/Intro";
import HomeHeader from "./sharedComponents/HomeHeader";

const HomeTabs = createBottomTabNavigator(
{
Map: MapScreen,
Camera: CameraScreen,
},
// $FlowFixMe
{
navigationOptions: () => ({
header: props => <HomeHeader {...props} />,
headerTransparent: true,
}),
defaultNavigationOptions: ({ navigation }) => ({
initialRouteName: "Map",
backBehavior: "initialRoute",
tabBarOptions: {
showLabel: false,
},
tabBarIcon: ({ focused, horizontal, tintColor }) => {
const { routeName } = navigation.state;
let iconName;
if (routeName === "Map") iconName = "map";
else iconName = "photo-camera";
return <MaterialIcons name={iconName} size={30} color={tintColor} />;
},
}),
}
);

const AppStack = createStackNavigator(
// $FlowFixMe - flow definitions don't recognize static props on function components
{
Home: HomeTabs,
// $FlowFixMe
GpsModal: GpsModal,
// $FlowFixMe
SyncModal: SyncModal,
Settings: Settings,
// $FlowFixMe
ProjectConfig: ProjectConfig,
// $FlowFixMe
LanguageSettings,
// $FlowFixMe
PhotosModal: PhotosModal,
// $FlowFixMe
CategoryChooser: CategoryChooser,
// $FlowFixMe
AddPhoto: AddPhoto,
// $FlowFixMe
ObservationList: ObservationList,
// $FlowFixMe
Observation: Observation,
// $FlowFixMe
ObservationEdit: ObservationEdit,
ManualGpsScreen: ManualGpsScreen,
ObservationDetails: ObservationDetails,
},
{
initialRouteName: "Home",
// TODO iOS: Dynamically set transition mode to modal for modals
mode: "card",
headerMode: "screen",
defaultNavigationOptions: {
headerStyle: {
height: 60,
},
// We use a slightly larger back icon, to improve accessibility
// TODO iOS: This should probably be a chevron not an arrow
headerLeft: props => <CustomHeaderLeft {...props} />,
headerTitleStyle: {
marginHorizontal: 0,
},
cardStyle: {
backgroundColor: "#ffffff",
},
},
}
);

const RootStack = createSwitchNavigator(
{
Intro: IntroStack,
App: AppStack,
},
{
initialRouteName: "Intro",
}
);
import AppStack from "./AppStack";
import { createAppContainer } from "react-navigation";

// $FlowFixMe
export default createAppContainer(
BuildConfig.FLAVOR === "icca" ? RootStack : AppStack
);
export default createAppContainer(AppStack);
Loading

0 comments on commit c7df1fd

Please sign in to comment.