private NPath PackageApp(NPath buildPath, NPath mainLibPath)
        {
            var deployedPath = buildPath.Combine(m_gameName + ".apk");

            if (m_apkToolchain == null)
            {
                Console.WriteLine($"Error: not Android APK toolchain");
                return(deployedPath);
            }

            var gradleProjectPath = mainLibPath.Parent.Parent.Parent.Parent.Parent;
            var pathToRoot        = new NPath(string.Concat(Enumerable.Repeat("../", gradleProjectPath.Depth)));
            var apkSrcPath        = AsmDefConfigFile.AsmDefDescriptionFor("Unity.Platforms.Android").Path.Parent.Combine("AndroidProjectTemplate~/");

            var javaLaunchPath         = m_apkToolchain.JavaPath.Combine("bin").Combine("java");
            var gradleLaunchPath       = m_apkToolchain.GetGradleLaunchJarPath();
            var releaseApk             = m_codeGen == CodeGen.Release;
            var gradleCommand          = releaseApk ? "assembleRelease" : "assembleDebug";
            var deleteCommand          = Unity.BuildTools.HostPlatform.IsWindows ? $"del /f /q {deployedPath.InQuotes(SlashMode.Native)} 2> nul" : $"rm -f {deployedPath.InQuotes(SlashMode.Native)}";
            var gradleExecutableString = $"{deleteCommand} && cd {gradleProjectPath.InQuotes()} && {javaLaunchPath.InQuotes()} -classpath {gradleLaunchPath.InQuotes()} org.gradle.launcher.GradleMain {gradleCommand} && cd {pathToRoot.InQuotes()}";

            var apkPath = gradleProjectPath.Combine("build/outputs/apk").Combine(releaseApk ? "release/gradle-release.apk" : "debug/gradle-debug.apk");

            Backend.Current.AddAction(
                actionName: "Build Gradle project",
                targetFiles: new[] { apkPath },
                inputs: m_apkToolchain.RequiredArtifacts.Append(mainLibPath).Concat(m_supportFiles.Select(d => d.Path)).ToArray(),
                executableStringFor: gradleExecutableString,
                commandLineArguments: Array.Empty <string>(),
                allowUnexpectedOutput: false,
                allowedOutputSubstrings: new[] { ":*", "BUILD SUCCESSFUL in *" }
                );

            var templateStrings = new Dictionary <string, string>
            {
                { "**TINYNAME**", m_gameName.Replace("-", "").ToLower() },
                { "**GAMENAME**", m_gameName },
            };

            // copy and patch project files
            foreach (var r in apkSrcPath.Files(true))
            {
                var destPath = gradleProjectPath.Combine(r.RelativeTo(apkSrcPath));
                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);
                }
                Backend.Current.AddDependency(apkPath, destPath);
            }

            var localProperties = new StringBuilder();

            localProperties.AppendLine($"sdk.dir={m_apkToolchain.SdkPath}");
            localProperties.AppendLine($"ndk.dir={m_apkToolchain.Sdk.Path.MakeAbsolute()}");
            var localPropertiesPath = gradleProjectPath.Combine("local.properties");

            Backend.Current.AddWriteTextAction(localPropertiesPath, localProperties.ToString());
            Backend.Current.AddDependency(apkPath, localPropertiesPath);

            // copy additional resources and Data files
            // TODO: better to use move from main lib directory
            foreach (var r in m_supportFiles)
            {
                var targetAssetPath = gradleProjectPath.Combine("src/main/assets");
                if (r.Path.FileName == "testconfig.json")
                {
                    targetAssetPath = buildPath.Combine(r.Path.FileName);
                }
                else if (r is DeployableFile && (r as DeployableFile).RelativeDeployPath != null)
                {
                    targetAssetPath = targetAssetPath.Combine((r as DeployableFile).RelativeDeployPath);
                }
                else
                {
                    targetAssetPath = targetAssetPath.Combine(r.Path.FileName);
                }
                Backend.Current.AddDependency(apkPath, CopyTool.Instance().Setup(targetAssetPath, r.Path));
            }

            return(CopyTool.Instance().Setup(deployedPath, apkPath));
        }
        private NPath PackageApp(NPath buildPath, BuiltNativeProgram mainProgram)
        {
            var mainLibPath = mainProgram.Path;

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

            if (m_apkToolchain == null)
            {
                Console.WriteLine($"Error: not Android APK toolchain");
                return(buildPath);
            }
            if (AndroidApkToolchain.ExportProject)
            {
                var deployedPath = buildPath.Combine(m_gameName);
                GenerateGradleProject(deployedPath);

                // stub action to have deployedPath in build tree and set correct dependencies
                Backend.Current.AddAction(
                    actionName: "Gradle project folder",
                    targetFiles: new[] { deployedPath },
                    inputs: m_projectFiles.ToArray(),
                    executableStringFor: $"echo created",
                    commandLineArguments: Array.Empty <string>(),
                    allowUnexpectedOutput: true,
                    allowUnwrittenOutputFiles: true
                    );
                return(deployedPath);
            }
            else
            {
                var deployedPath      = buildPath.Combine(m_gameName + "." + m_apkToolchain.ExecutableFormat.Extension);
                var gradleProjectPath = mainLibPath.Parent.Parent.Parent.Parent.Parent;
                GenerateGradleProject(gradleProjectPath);
                var pathToRoot = new NPath(string.Concat(Enumerable.Repeat("../", gradleProjectPath.Depth)));

                var javaLaunchPath   = new NPath(AndroidApkToolchain.Config.ExternalTools.JavaPath).Combine("bin").Combine("java");
                var gradleLaunchPath = AndroidApkToolchain.GetGradleLaunchJarPath();
                var releaseBuild     = m_config == DotsConfiguration.Release;
                var gradleCommand    = AndroidApkToolchain.BuildAppBundle ?
                                       (releaseBuild ? "bundleRelease" : "bundleDebug") :
                                       (releaseBuild ? "assembleRelease" : "assembleDebug");
                var gradleExecutableString = $"cd {gradleProjectPath.InQuotes()} && {javaLaunchPath.InQuotes()} -classpath {gradleLaunchPath.InQuotes()} org.gradle.launcher.GradleMain {gradleCommand} && cd {pathToRoot.InQuotes()}";

                var config          = releaseBuild ? "release" : "debug";
                var gradleBuildPath = gradleProjectPath.Combine("build/outputs").
                                      Combine(AndroidApkToolchain.BuildAppBundle ? "bundle" : "apk").
                                      Combine($"{config}/gradle-{config}.{(AndroidApkToolchain.BuildAppBundle ? "aab" : "apk")}");

                Backend.Current.AddAction(
                    actionName: "Build Gradle project",
                    targetFiles: new[] { gradleBuildPath },
                    inputs: m_projectFiles.ToArray(),
                    executableStringFor: gradleExecutableString,
                    commandLineArguments: Array.Empty <string>(),
                    allowUnexpectedOutput: false,
                    allowedOutputSubstrings: new[] { ":*", "BUILD SUCCESSFUL in *" }
                    );

                return(CopyTool.Instance().Setup(deployedPath, gradleBuildPath));
            }
        }