diff --git a/core/osutils/file.py b/core/osutils/file.py index ba2e23e8..01aa75e8 100644 --- a/core/osutils/file.py +++ b/core/osutils/file.py @@ -7,6 +7,7 @@ import shutil import time import tarfile +import zipfile from core.osutils.os_type import OSType from core.osutils.process import Process @@ -193,3 +194,12 @@ def unpack_tar(file_path, dest_dir): tarFile.extractall(dest_dir) except: print "Failed to unpack .tar file {0}".format(file_path) + + @staticmethod + def unzip(file_path, dest_dir): + try: + zipFile = zipfile.ZipFile(file_path, 'r') + zipFile.extractall(dest_dir) + zipFile.close() + except: + print "Failed to unzip file {0}".format(file_path) diff --git a/data/abdoid-app-bundle/app.gradle b/data/abdoid-app-bundle/app.gradle new file mode 100644 index 00000000..5cfdda33 --- /dev/null +++ b/data/abdoid-app-bundle/app.gradle @@ -0,0 +1,23 @@ +// Add your native dependencies here: + +// Uncomment to add recyclerview-v7 dependency +//dependencies { +// implementation 'com.android.support:recyclerview-v7:+' +//} + +android { + defaultConfig { + generatedDensities = [] + ndk { + abiFilters.clear() + } + } + aaptOptions { + additionalParameters "--no-version-vectors" + } + sourceSets { + main { + jniLibs.srcDirs = ["$projectDir/libs/jni", "$projectDir/snapshot-build/build/ndk-build/libs"] + } + } +} diff --git a/data/abdoid-app-bundle/webpack.config.js b/data/abdoid-app-bundle/webpack.config.js new file mode 100644 index 00000000..d13248cb --- /dev/null +++ b/data/abdoid-app-bundle/webpack.config.js @@ -0,0 +1,259 @@ +const { join, relative, resolve, sep } = require("path"); + +const webpack = require("webpack"); +const nsWebpack = require("nativescript-dev-webpack"); +const nativescriptTarget = require("nativescript-dev-webpack/nativescript-target"); +const CleanWebpackPlugin = require("clean-webpack-plugin"); +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const { BundleAnalyzerPlugin } = require("webpack-bundle-analyzer"); +const { NativeScriptWorkerPlugin } = require("nativescript-worker-loader/NativeScriptWorkerPlugin"); +const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); + +module.exports = env => { + // Add your custom Activities, Services and other android app components here. + const appComponents = [ + "tns-core-modules/ui/frame", + "tns-core-modules/ui/frame/activity", + ]; + + const platform = env && (env.android && "android" || env.ios && "ios"); + if (!platform) { + throw new Error("You need to provide a target platform!"); + } + + const platforms = ["ios", "android"]; + const projectRoot = __dirname; + + // Default destination inside platforms//... + const dist = resolve(projectRoot, nsWebpack.getAppPath(platform, projectRoot)); + const appResourcesPlatformDir = platform === "android" ? "Android" : "iOS"; + + const { + // The 'appPath' and 'appResourcesPath' values are fetched from + // the nsconfig.json configuration file + // when bundling with `tns run android|ios --bundle`. + appPath = "app", + appResourcesPath = "app/App_Resources", + + // You can provide the following flags when running 'tns run android|ios' + snapshot, // --env.snapshot + uglify, // --env.uglify + report, // --env.report + sourceMap, // --env.sourceMap + hmr, // --env.hmr, + } = env; + const externals = (env.externals || []).map((e) => { // --env.externals + return new RegExp(e + ".*"); + }); + + const appFullPath = resolve(projectRoot, appPath); + const appResourcesFullPath = resolve(projectRoot, appResourcesPath); + + const entryModule = nsWebpack.getEntryModule(appFullPath); + const entryPath = `.${sep}${entryModule}.js`; + + const config = { + mode: uglify ? "production" : "development", + context: appFullPath, + externals, + watchOptions: { + ignored: [ + appResourcesFullPath, + // Don't watch hidden files + "**/.*", + ] + }, + target: nativescriptTarget, + entry: { + bundle: entryPath, + }, + output: { + pathinfo: false, + path: dist, + libraryTarget: "commonjs2", + filename: "[name].js", + globalObject: "global", + }, + resolve: { + extensions: [".js", ".scss", ".css"], + // Resolve {N} system modules from tns-core-modules + modules: [ + "node_modules/tns-core-modules", + "node_modules", + ], + alias: { + '~': appFullPath + }, + // don't resolve symlinks to symlinked modules + symlinks: false + }, + resolveLoader: { + // don't resolve symlinks to symlinked loaders + symlinks: false + }, + node: { + // Disable node shims that conflict with NativeScript + "http": false, + "timers": false, + "setImmediate": false, + "fs": "empty", + "__dirname": false, + }, + devtool: sourceMap ? "inline-source-map" : "none", + optimization: { + splitChunks: { + cacheGroups: { + vendor: { + name: "vendor", + chunks: "all", + test: (module, chunks) => { + const moduleName = module.nameForCondition ? module.nameForCondition() : ''; + return /[\\/]node_modules[\\/]/.test(moduleName) || + appComponents.some(comp => comp === moduleName); + + }, + enforce: true, + }, + } + }, + minimize: !!uglify, + minimizer: [ + new UglifyJsPlugin({ + parallel: true, + cache: true, + uglifyOptions: { + output: { + comments: false, + }, + compress: { + // The Android SBG has problems parsing the output + // when these options are enabled + 'collapse_vars': platform !== "android", + sequences: platform !== "android", + } + } + }) + ], + }, + module: { + rules: [ + { + test: new RegExp(entryPath), + use: [ + // Require all Android app components + platform === "android" && { + loader: "nativescript-dev-webpack/android-app-components-loader", + options: { modules: appComponents } + }, + + { + loader: "nativescript-dev-webpack/bundle-config-loader", + options: { + loadCss: !snapshot, // load the application css if in debug mode + } + }, + ].filter(loader => !!loader) + }, + + { + test: /-page\.js$/, + use: "nativescript-dev-webpack/script-hot-loader" + }, + + { + test: /\.(css|scss)$/, + use: "nativescript-dev-webpack/style-hot-loader" + }, + + { + test: /\.(html|xml)$/, + use: "nativescript-dev-webpack/markup-hot-loader" + }, + + { test: /\.(html|xml)$/, use: "nativescript-dev-webpack/xml-namespace-loader"}, + + { + test: /\.css$/, + use: { loader: "css-loader", options: { minimize: false, url: false } } + }, + + { + test: /\.scss$/, + use: [ + { loader: "css-loader", options: { minimize: false, url: false } }, + "sass-loader" + ] + }, + ] + }, + plugins: [ + // Define useful constants like TNS_WEBPACK + new webpack.DefinePlugin({ + "global.TNS_WEBPACK": "true", + "process": undefined, + }), + // Remove all files from the out dir. + new CleanWebpackPlugin([ `${dist}/**/*` ]), + // Copy native app resources to out dir. + new CopyWebpackPlugin([ + { + from: `${appResourcesFullPath}/${appResourcesPlatformDir}`, + to: `${dist}/App_Resources/${appResourcesPlatformDir}`, + context: projectRoot + }, + ]), + // Copy assets to out dir. Add your own globs as needed. + new CopyWebpackPlugin([ + { from: { glob: "fonts/**" } }, + { from: { glob: "**/*.jpg" } }, + { from: { glob: "**/*.png" } }, + ], { ignore: [`${relative(appPath, appResourcesFullPath)}/**`] }), + // Generate a bundle starter script and activate it in package.json + new nsWebpack.GenerateBundleStarterPlugin([ + "./vendor", + "./bundle", + ]), + // For instructions on how to set up workers with webpack + // check out https://github.com/nativescript/worker-loader + new NativeScriptWorkerPlugin(), + new nsWebpack.PlatformFSPlugin({ + platform, + platforms, + }), + // Does IPC communication with the {N} CLI to notify events when running in watch mode. + new nsWebpack.WatchStateLoggerPlugin(), + ], + }; + + if (report) { + // Generate report files for bundles content + config.plugins.push(new BundleAnalyzerPlugin({ + analyzerMode: "static", + openAnalyzer: false, + generateStatsFile: true, + reportFilename: resolve(projectRoot, "report", `report.html`), + statsFilename: resolve(projectRoot, "report", `stats.json`), + })); + } + + if (snapshot) { + config.plugins.push(new nsWebpack.NativeScriptSnapshotPlugin({ + chunk: "vendor", + requireModules: [ + "tns-core-modules/bundle-entry-points", + ], + projectRoot, + webpackConfig: config, + targetArchs: ["arm", "arm64", "ia32"], + useLibs: true, + androidNdkPath: "$ANDROID_NDK_HOME" + })); + } + + if (hmr) { + config.plugins.push(new webpack.HotModuleReplacementPlugin()); + } + + + return config; +}; diff --git a/tests/build/android/android_app_bundle_tests.py b/tests/build/android/android_app_bundle_tests.py new file mode 100644 index 00000000..8949d6c7 --- /dev/null +++ b/tests/build/android/android_app_bundle_tests.py @@ -0,0 +1,156 @@ +import os + +import urllib + +import unittest + +from core.settings.settings import SUT_FOLDER, EMULATOR_NAME, EMULATOR_ID, \ + ANDROID_KEYSTORE_PASS, ANDROID_KEYSTORE_ALIAS, ANDROID_KEYSTORE_PATH,\ + ANDROID_KEYSTORE_ALIAS_PASS, CURRENT_OS, OSType, ANDROID_PACKAGE, TEST_RUN_HOME + +from core.device.helpers.adb import Adb + +from core.base_class.BaseClass import BaseClass + +from core.osutils.file import File + +from core.device.device import Device + +from core.osutils.folder import Folder + +from core.osutils.command import run + +from core.osutils.command_log_level import CommandLogLevel + +from core.device.emulator import Emulator, EMULATOR_ID, EMULATOR_NAME + +from core.tns.tns import Tns +class AndroidAppBundleTests(BaseClass): + + bundletool_path = os.path.join(SUT_FOLDER,"bundletool.jar") + path_to_apks = os.path.join(BaseClass.app_name,'app.apks') + + @classmethod + def setUpClass(cls): + BaseClass.setUpClass(cls.__name__) + Emulator.stop() + Emulator.ensure_available() + + Tns.create_app(BaseClass.app_name) + Tns.platform_add_android(attributes={"--path": BaseClass.app_name, "--frameworkPath": ANDROID_PACKAGE}) + Folder.copy(TEST_RUN_HOME + "/" + cls.app_name, TEST_RUN_HOME + "/data/TestApp") + + #Download bundletool + url = 'https://github.com/google/bundletool/releases/download/0.8.0/bundletool-all-0.8.0.jar' + urllib.urlretrieve(url, os.path.join(SUT_FOLDER, 'bundletool.jar')) + + @classmethod + def tearDownClass(cls): + BaseClass.tearDownClass() + Folder.cleanup(TEST_RUN_HOME + "/data/TestApp") + + def setUp(self): + BaseClass.setUp(self) + # Ensure app is in initial state + Folder.navigate_to(folder=TEST_RUN_HOME, relative_from_current_folder=False) + Folder.cleanup(self.app_name) + Folder.copy(TEST_RUN_HOME + "/data/TestApp", TEST_RUN_HOME + "/TestApp") + + def tearDown(self): + BaseClass.tearDown(self) + Tns.kill() + + @staticmethod + def bundletool_build(bundletool_path, path_to_aab, path_to_apks): + build_command = ('java -jar {0} build-apks --bundle="{1}" --output="{2}" --ks="{3}" --ks-pass=pass:"{4}" --ks-key-alias="{5}"' \ + ' --key-pass=pass:"{6}"').format(bundletool_path, path_to_aab, path_to_apks, ANDROID_KEYSTORE_PATH, + ANDROID_KEYSTORE_PASS, ANDROID_KEYSTORE_ALIAS, ANDROID_KEYSTORE_ALIAS_PASS) + output = run(build_command, log_level=CommandLogLevel.FULL) + assert not "Error" in output, "create of .apks file failed" + + @staticmethod + def bundletool_deploy(bundletool_path, path_to_apks, device_id): + deploy_command = ('java -jar {0} install-apks --apks="{1}" --device-id={2}').format(bundletool_path, path_to_apks, EMULATOR_ID) + output = run(deploy_command, log_level=CommandLogLevel.FULL) + assert not "Error" in output, "deploy of app failed" + assert "The APKs have been extracted in the directory:" in output, "deploy of app failed" + + def test_001_build_android_app_bundle(self): + """Build app with android app bundle option. Verify the output(app.aab) and use bundletool to deploy on device""" + path_to_aab = os.path.join(self.app_name, "platforms", "android", "app", "build", "outputs", "bundle", "debug", "app.aab") + + output = Tns.build_android(attributes={"--path": self.app_name, "--aab":""}, assert_success=False) + assert "The build result is located at:" in output + assert path_to_aab in output + assert File.exists(path_to_aab) + + #Verify app can be deployed on emulator via bundletool + # Use bundletool to create the .apks file + self.bundletool_build(self.bundletool_path, path_to_aab, self.path_to_apks) + assert File.exists(self.path_to_apks) + + # Deploy on device + self.bundletool_deploy(self.bundletool_path, self.path_to_apks, device_id=EMULATOR_ID) + + # Start the app on device + Adb.start_app(EMULATOR_ID, "org.nativescript.TestApp") + + # Verify app looks correct inside emulator + app_started = Device.wait_for_text(device_id=EMULATOR_ID, text='TAP') + assert app_started, 'App is not started on device' + + @unittest.skipIf(CURRENT_OS == OSType.WINDOWS, "Skip on Windows") + def test_002_build_android_app_bundle_env_snapshot(self): + """Build app with android app bundle option with --bundle and optimisations for snapshot. + Verify the output(app.aab) and use bundletool to deploy on device.""" + # This test will not run on windows because env.snapshot option is not available on that OS + + path_to_aab = os.path.join(self.app_name, "platforms", "android", "app", "build", "outputs", "bundle", "release", "app.aab") + + #Configure app with snapshot optimisations + source = os.path.join('data', 'abdoid-app-bundle', 'app.gradle') + target = os.path.join(self.app_name, 'app', 'App_Resources', 'Android', 'app.gradle' ) + File.copy(src=source, dest=target) + + source = os.path.join('data', 'abdoid-app-bundle', 'webpack.config.js') + target = os.path.join(self.app_name, 'webpack.config.js' ) + File.copy(src=source, dest=target) + + #env.snapshot is applicable only in release build + output = Tns.build_android(attributes={"--path": self.app_name, + "--keyStorePath": ANDROID_KEYSTORE_PATH, + "--keyStorePassword": ANDROID_KEYSTORE_PASS, + "--keyStoreAlias": ANDROID_KEYSTORE_ALIAS, + "--keyStoreAliasPassword": ANDROID_KEYSTORE_ALIAS_PASS, + "--release": "", + "--aab": "", + "--env.uglify": "", + "--env.snapshot": "", + "--bundle": "" + }, assert_success=False) + assert "The build result is located at:" in output + assert path_to_aab in output + assert File.exists(path_to_aab) + + #Verify app can be deployed on emulator via bundletool + # Use bundletool to create the .apks file + self.bundletool_build(self.bundletool_path, path_to_aab, self.path_to_apks) + assert File.exists(self.path_to_apks) + + # Verify that the correct .so file is included in the package + File.unzip(self.path_to_apks, os.path.join(self.app_name, 'apks')) + File.unzip(os.path.join(self.app_name, 'apks', 'splits', 'base-x86.apk'), os.path.join(self.app_name,'base_apk')) + assert File.exists(os.path.join(self.app_name, 'base_apk', 'lib', 'x86', 'libNativeScript.so')) + + # Deploy on device + self.bundletool_deploy(self.bundletool_path, self.path_to_apks, device_id=EMULATOR_ID) + + # Start the app on device + Adb.start_app(EMULATOR_ID, "org.nativescript.TestApp") + + # Verify app looks correct inside emulator + app_started = Device.wait_for_text(device_id=EMULATOR_ID, text='TAP') + assert app_started, 'App is not started on device' + + + diff --git a/tests/build/android/build_android_tests.py b/tests/build/android/build_android_tests.py index c440c4bf..14c691d7 100644 --- a/tests/build/android/build_android_tests.py +++ b/tests/build/android/build_android_tests.py @@ -366,4 +366,4 @@ def test_451_resources_update(self): assert File.exists(self.app_name + "/app/App_Resources/Android/src/main/assets") assert File.exists(self.app_name + "/app/App_Resources/Android/src/main/java") assert File.exists(self.app_name + "/app/App_Resources/Android/src/main/res/values") - Tns.build_android(attributes={"--path": self.app_name}) + Tns.build_android(attributes={"--path": self.app_name}) \ No newline at end of file