public override BuiltNativeProgram DeployTo(NPath targetDirectory, Dictionary <IDeployable, IDeployable> alreadyDeployed = null)
        {
            // This is complementary target, library should be deployed to the corresponding folder of the main target
            // see comment in https://github.com/Unity-Technologies/dots/blob/master/TinySamples/Packages/com.unity.dots.runtime/bee%7E/BuildProgramSources/DotsConfigs.cs
            // DotsConfigs.MakeConfigs() method for details.
            var gradleProjectPath = AndroidApkToolchain.ExportProject ? targetDirectory.Combine(m_gameName) : Path.Parent.Parent.Combine("gradle");
            var libDirectory      = gradleProjectPath.Combine("src/main/jniLibs").Combine(m_libPath);

            // Deployables with type DeployableFile are deployed with main target
            Deployables = Deployables.Where(f => !(f is DeployableFile)).ToArray();
            var result = base.DeployTo(libDirectory, alreadyDeployed);

            // Required to make sure that main target Gradle project depends on this lib and this lib is deployed before packaging step
            Backend.Current.AddDependency(gradleProjectPath.Combine("build.gradle"), result.Path);
            return(result);
        }
        private NPath PackageApp(NPath buildPath, NPath mainLibPath)
        {
            var pbxPath = GenerateXCodeProject(mainLibPath);

            m_projectFiles.Add(mainLibPath);
            m_projectFiles.AddRange(Deployables.Select(d => d.Path));

            var deployedPath = buildPath.Combine($"{m_gameName}{(IOSAppToolchain.ExportProject ? "" : ".app")}");

            if (IOSAppToolchain.ExportProject)
            {
                Backend.Current.AddAction(
                    actionName: "Open Xcode project folder",
                    targetFiles: new[] { deployedPath },
                    inputs: m_projectFiles.ToArray(),
                    executableStringFor: $"open {deployedPath}",
                    commandLineArguments: Array.Empty <string>(),
                    allowUnexpectedOutput: true,
                    allowUnwrittenOutputFiles: true
                    );
            }
            else
            {
                var configuration  = m_config == DotsConfiguration.Release ? "Release" : "Debug";
                var xcodeprojPath  = pbxPath.Parent;
                var outputPath     = xcodeprojPath.Parent.Combine("app");
                var target         = IOSAppToolchain.Config.TargetSettings.SdkVersion == iOSSdkVersion.DeviceSDK ? "iphoneos" : "iphonesimulator";
                var appPath        = outputPath.Combine("Build", "Products", $"{configuration}-{target}", $"{TinyProjectName}.app");
                var appBinaryPath  = appPath.Combine("Tiny-iPhone");
                var destination    = IOSAppToolchain.Config.TargetSettings.SdkVersion == iOSSdkVersion.DeviceSDK ? "generic/platform=iOS": "platform=iOS Simulator,name=iPhone 11";
                var xcodeArguments = new List <string>
                {
                    $"-project {xcodeprojPath.InQuotes()}",
                    $"-configuration {configuration}",
                    $"-derivedDataPath {outputPath.InQuotes()}",
                    $"-destination \"{destination}\"",
                    $"-scheme \"{TinyProjectName}\"",
                    "-allowProvisioningUpdates"
                };
                if (!BuildConfiguration.HasComponent <iOSSigningSettings>())
                {
                    var devTeam = Environment.GetEnvironmentVariable("UNITY_TINY_IOS_DEVELOPMENT_TEAM");
                    if (devTeam != null)
                    {
                        xcodeArguments.Add($"DEVELOPMENT_TEAM={devTeam}");
                    }
                    var signIdentity = Environment.GetEnvironmentVariable("UNITY_TINY_IOS_SIGN_IDENTITY");
                    if (signIdentity != null)
                    {
                        xcodeArguments.Add($"CODE_SIGN_IDENTITY=\"{signIdentity}\"");
                    }
                    var provProfile = Environment.GetEnvironmentVariable("UNITY_TINY_IOS_PROVISIONING_PROFILE");
                    if (provProfile != null)
                    {
                        xcodeArguments.Add($"PROVISIONING_PROFILE_SPECIFIER={provProfile}");
                    }
                }

                Backend.Current.AddAction(
                    actionName: "Build Xcode project",
                    targetFiles: new[] { appBinaryPath },
                    inputs: m_projectFiles.ToArray(),
                    executableStringFor: IOSAppToolchain.XcodeBuildPath.InQuotes(),
                    commandLineArguments: xcodeArguments.ToArray(),
                    allowUnexpectedOutput: true,
                    allowUnwrittenOutputFiles: true
                    );

                m_projectFiles.Add(appBinaryPath);
                Backend.Current.AddAction(
                    actionName: "Copy application to output folder",
                    targetFiles: new[] { deployedPath },
                    inputs: m_projectFiles.ToArray(),
                    executableStringFor: $"rm -rf {deployedPath} && cp -R {appPath} {deployedPath}",
                    commandLineArguments: Array.Empty <string>(),
                    allowUnexpectedOutput: true,
                    allowUnwrittenOutputFiles: true
                    );
            }
            return(deployedPath);
        }
        private void GenerateGradleProject(NPath gradleProjectPath)
        {
            var gradleSrcPath = AsmDefConfigFile.AsmDefDescriptionFor("Unity.Build.Android.DotsRuntime").Path.Parent.Combine("AndroidProjectTemplate~/");

            var hasGradleDependencies = false;
            var gradleDependencies    = new StringBuilder();

            gradleDependencies.AppendLine("    dependencies {");
            var hasKotlin = false;

            foreach (var d in Deployables.Where(d => (d is DeployableFile)))
            {
                var f = d as DeployableFile;
                if (f.Path.Extension == "aar" || f.Path.Extension == "jar")
                {
                    gradleDependencies.AppendLine($"        compile(name:'{f.Path.FileNameWithoutExtension}', ext:'{f.Path.Extension}')");
                    hasGradleDependencies = true;
                }
                else if (f.Path.Extension == "kt")
                {
                    hasKotlin = true;
                }
            }
            if (hasGradleDependencies)
            {
                gradleDependencies.AppendLine("    }");
            }
            else
            {
                gradleDependencies.Clear();
            }

            var kotlinClassPath = hasKotlin ? "        classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.11'" : "";
            var kotlinPlugin    = hasKotlin ? "apply plugin: 'kotlin-android'" : "";

            var  loadLibraries = new StringBuilder();
            bool useStaticLib  = Deployables.FirstOrDefault(l => l.ToString().Contains("lib_unity_tiny_android.so")) == default(IDeployable);

            if (useStaticLib)
            {
                loadLibraries.AppendLine($"        System.loadLibrary(\"{m_gameName}\");");
            }
            else
            {
                var rx = new Regex(@".*lib([\w\d_]+)\.so", RegexOptions.Compiled);
                foreach (var l in Deployables)
                {
                    var match = rx.Match(l.ToString());
                    if (match.Success)
                    {
                        loadLibraries.AppendLine($"        System.loadLibrary(\"{match.Groups[1].Value}\");");
                    }
                }
            }

            String abiFilters = "";

            if (AndroidApkToolchain.Config.Architectures.Architectures == AndroidArchitecture.ARM64)
            {
                abiFilters = "'arm64-v8a'";
            }
            else if (AndroidApkToolchain.Config.Architectures.Architectures == AndroidArchitecture.ARMv7)
            {
                abiFilters = "'armeabi-v7a'";
            }
            else if (AndroidApkToolchain.IsFatApk)
            {
                abiFilters = "'armeabi-v7a', 'arm64-v8a'";
            }
            else // shouldn't happen
            {
                Console.WriteLine($"Tiny android toolchain doesn't support {AndroidApkToolchain.Config.Architectures.Architectures.ToString()} architectures");
            }

            // Android docs say "density" value was added in API level 17, but it doesn't compile with target SDK level lower than 24.
            string configChanges         = ((int)AndroidApkToolchain.Config.APILevels.ResolvedTargetAPILevel > 23) ? AndroidConfigChanges + "|density" : AndroidConfigChanges;
            var    useKeystore           = BuildConfiguration.HasComponent <AndroidKeystore>();
            var    renderOutsideSafeArea = BuildConfiguration.HasComponent <AndroidRenderOutsideSafeArea>();

            var icons                         = AndroidApkToolchain.Config.Icons;
            var hasBackground                 = icons.Icons.Any(i => !String.IsNullOrEmpty(i.Background));
            var hasCustomIcons                = hasBackground || icons.Icons.Any(i => !String.IsNullOrEmpty(i.Foreground) || !String.IsNullOrEmpty(i.Legacy));
            var version                       = AndroidApkToolchain.Config.Settings.Version;
            var versionFieldCount             = version.Revision > 0 ? 4 : 3;
            var maxRatio                      = AndroidApkToolchain.Config.AspectRatio.GetMaxAspectRatio(AndroidApkToolchain.Config.APILevels.ResolvedTargetAPILevel);
            var additionalApplicationMetadata = "";
            var additionalPermissions         = "";
            var additionalFeatures            = "";

            if (!String.IsNullOrEmpty(maxRatio))
            {
                additionalApplicationMetadata += GetMetaDataString("android.max_aspect", maxRatio);
            }
            if (BuildConfiguration.HasComponent <ARCoreSettings>())
            {
                additionalPermissions += GetPermissionString("android.permission.CAMERA");
                if (AndroidApkToolchain.Config.ARCore.Requirement == Requirement.Optional)
                {
                    additionalApplicationMetadata += "\n" + GetMetaDataString("com.google.ar.core", "optional");
                }
                else
                {
                    additionalApplicationMetadata += "\n" + GetMetaDataString("com.google.ar.core", "required");
                    additionalFeatures            += GetFeatureString("android.hardware.camera.ar", true);
                }
                if (AndroidApkToolchain.Config.ARCore.DepthSupport == Requirement.Required)
                {
                    additionalFeatures += "\n" + GetFeatureString("com.google.ar.core.depth", true);
                }
            }
            var templateStrings = new Dictionary <string, string>
            {
                { "**LOADLIBRARIES**", loadLibraries.ToString() },
                { "**PACKAGENAME**", AndroidApkToolchain.Config.Identifier.PackageName },
                { "**PRODUCTNAME**", AndroidApkToolchain.Config.Settings.ProductName },
                { "**VERSIONNAME**", version.ToString(versionFieldCount) },
                { "**VERSIONCODE**", AndroidApkToolchain.Config.VersionCode.VersionCode.ToString() },
                { "**ORIENTATION**", GetOrientationAttr() },
                { "**INSTALLLOCATION**", AndroidApkToolchain.Config.InstallLocation?.PreferredInstallLocationAsString() },
                { "**CUTOUTMODE**", AndroidRenderOutsideSafeArea.CutoutMode(renderOutsideSafeArea) },
                { "**NOTCHCONFIG**", AndroidRenderOutsideSafeArea.NotchConfig(renderOutsideSafeArea) },
                { "**NOTCHSUPPORT**", AndroidRenderOutsideSafeArea.NotchSupport(renderOutsideSafeArea) },
                { "**GAMENAME**", m_gameName },
                { "**MINSDKVERSION**", ((int)AndroidApkToolchain.Config.APILevels.MinAPILevel).ToString() },
                { "**TARGETSDKVERSION**", ((int)AndroidApkToolchain.Config.APILevels.ResolvedTargetAPILevel).ToString() },
                { "**CONFIGCHANGES**", configChanges },
                { "**ACTIVITY_ASPECT**", String.IsNullOrEmpty(maxRatio) ? "" : $"android:maxAspectRatio=\"{maxRatio}\"" },
                { "**ADDITIONAL_APPLICATION_METADATA**", additionalApplicationMetadata },
                { "**ADDITIONAL_PERMISSIONS**", additionalPermissions },
                { "**ADDITIONAL_FEATURES**", additionalFeatures },
                { "**ABIFILTERS**", abiFilters },
                { "**SIGN**", AndroidApkToolchain.Config.Keystore.GetSigningConfigs(useKeystore) },
                { "**SIGNCONFIG**", AndroidApkToolchain.Config.Keystore.GetSigningConfig(useKeystore) },
                { "**DEPENDENCIES**", gradleDependencies.ToString() },
                { "**KOTLINCLASSPATH**", kotlinClassPath },
                { "**KOTLINPLUGIN**", kotlinPlugin },
                { "**ALLOWED_PORTRAIT**", AndroidApkToolchain.AllowedOrientationPortrait ? "true" : "false" },
                { "**ALLOWED_REVERSE_PORTRAIT**", AndroidApkToolchain.AllowedOrientationReversePortrait ? "true" : "false" },
                { "**ALLOWED_LANDSCAPE**", AndroidApkToolchain.AllowedOrientationLandscape ? "true" : "false" },
                { "**ALLOWED_REVERSE_LANDSCAPE**", AndroidApkToolchain.AllowedOrientationReverseLandscape ? "true" : "false" },
                { "**BACKGROUND_PATH**", hasBackground ? "mipmap" : "drawable" }
            };

            // copy icon files
            if (hasCustomIcons)
            {
                for (int i = 0; i < icons.Icons.Length; ++i)
                {
                    var dpi = ((ScreenDPI)i).ToString().ToLower();
                    if (AndroidApkToolchain.Config.APILevels.TargetSDKSupportsAdaptiveIcons)
                    {
                        CopyIcon(gradleProjectPath, dpi, "ic_launcher_foreground.png", icons.Icons[i].Foreground);
                        CopyIcon(gradleProjectPath, dpi, "ic_launcher_background.png", icons.Icons[i].Background);
                    }
                    CopyIcon(gradleProjectPath, dpi, "app_icon.png", icons.Icons[i].Legacy);
                }
            }

            // copy and patch project files
            var apiRx = new Regex(@".+res[\\|\/].+-v([0-9]+)$", RegexOptions.Compiled);

            foreach (var r in gradleSrcPath.Files(true))
            {
                if ((hasCustomIcons && r.HasDirectory("mipmap-mdpi")) ||
                    (hasBackground && r.HasDirectory("drawable"))) // skipping icons files if there are custom ones
                {
                    continue;
                }
                if (!AndroidApkToolchain.Config.APILevels.TargetSDKSupportsAdaptiveIcons && r.FileName.StartsWith("ic_launcher_"))
                {
                    continue;
                }
                var match = apiRx.Match(r.Parent.ToString());
                if (match.Success)
                {
                    var api = Int32.Parse(match.Groups[1].Value);
                    if (api > (int)AndroidApkToolchain.Config.APILevels.ResolvedTargetAPILevel)
                    {
                        continue;
                    }
                }

                var destPath = gradleProjectPath.Combine(r.RelativeTo(gradleSrcPath));
                if (r.Extension == "template")
                {
                    destPath = destPath.ChangeExtension("");
                    var code = r.ReadAllText();
                    foreach (var t in templateStrings)
                    {
                        if (code.IndexOf(t.Key) != -1)
                        {
                            code = code.Replace(t.Key, t.Value);
                        }
                    }
                    Backend.Current.AddWriteTextAction(destPath, code);
                }
                else
                {
                    destPath = CopyTool.Instance().Setup(destPath, r);
                }
                m_projectFiles.Add(destPath);
            }

            var localProperties = new StringBuilder();

            localProperties.AppendLine($"sdk.dir={new NPath(AndroidApkToolchain.Config.ExternalTools.SdkPath).ToString()}");
            localProperties.AppendLine($"ndk.dir={new NPath(AndroidApkToolchain.Config.ExternalTools.NdkPath).ToString()}");
            var localPropertiesPath = gradleProjectPath.Combine("local.properties");

            Backend.Current.AddWriteTextAction(localPropertiesPath, localProperties.ToString());
            m_projectFiles.Add(localPropertiesPath);
        }