Exemple #1
0
    public override bool Execute()
    {
        bool isDevice = (TargetOS == TargetNames.iOS || TargetOS == TargetNames.tvOS);

        if (!string.IsNullOrEmpty(MainLibraryFileName))
        {
            if (!File.Exists(Path.Combine(AppDir, MainLibraryFileName)))
            {
                throw new ArgumentException($"MainLibraryFileName='{MainLibraryFileName}' was not found in AppDir='{AppDir}'");
            }
        }

        if (ProjectName.Contains(' '))
        {
            throw new ArgumentException($"ProjectName='{ProjectName}' should not contain spaces");
        }

        string[] excludes = Array.Empty <string>();
        if (ExcludeFromAppDir != null)
        {
            excludes = ExcludeFromAppDir
                       .Where(i => !string.IsNullOrEmpty(i.ItemSpec))
                       .Select(i => i.ItemSpec)
                       .ToArray();
        }

        string binDir = Path.Combine(AppDir, $"bin-{ProjectName}-{Arch}");

        if (!string.IsNullOrEmpty(OutputDirectory))
        {
            binDir = OutputDirectory;
        }
        Directory.CreateDirectory(binDir);

        var assemblerFiles       = new List <string>();
        var assemblerFilesToLink = new List <string>();

        foreach (ITaskItem file in Assemblies)
        {
            // use AOT files if available
            var obj     = file.GetMetadata("AssemblerFile");
            var llvmObj = file.GetMetadata("LlvmObjectFile");
            if (!string.IsNullOrEmpty(obj))
            {
                assemblerFiles.Add(obj);
            }

            if (!string.IsNullOrEmpty(llvmObj))
            {
                assemblerFilesToLink.Add(llvmObj);
            }
        }

        if (((!ForceInterpreter && (isDevice || ForceAOT)) && !assemblerFiles.Any()))
        {
            throw new InvalidOperationException("Need list of AOT files for device builds.");
        }

        if (!string.IsNullOrEmpty(DiagnosticPorts))
        {
            bool validDiagnosticsConfig = false;

            if (string.IsNullOrEmpty(RuntimeComponents))
            {
                validDiagnosticsConfig = false;
            }
            else if (RuntimeComponents.Equals("*", StringComparison.OrdinalIgnoreCase))
            {
                validDiagnosticsConfig = true;
            }
            else if (RuntimeComponents.Contains("diagnostics_tracing", StringComparison.OrdinalIgnoreCase))
            {
                validDiagnosticsConfig = true;
            }

            if (!validDiagnosticsConfig)
            {
                throw new ArgumentException("Using DiagnosticPorts require diagnostics_tracing runtime component.");
            }
        }

        if (EnableAppSandbox && (string.IsNullOrEmpty(DevTeamProvisioning) || DevTeamProvisioning == "-"))
        {
            throw new ArgumentException("DevTeamProvisioning must be set to a valid value when App Sandbox is enabled, using '-' is not supported.");
        }

        var generator = new Xcode(Log, TargetOS, Arch);

        if (GenerateXcodeProject)
        {
            XcodeProjectPath = generator.GenerateXCode(ProjectName, MainLibraryFileName, assemblerFiles, assemblerFilesToLink,
                                                       AppDir, binDir, MonoRuntimeHeaders, !isDevice, UseConsoleUITemplate, ForceAOT, ForceInterpreter, InvariantGlobalization, Optimized, EnableRuntimeLogging, EnableAppSandbox, DiagnosticPorts, RuntimeComponents, NativeMainSource);

            if (BuildAppBundle)
            {
                if (isDevice && string.IsNullOrEmpty(DevTeamProvisioning))
                {
                    // DevTeamProvisioning shouldn't be empty for arm64 builds
                    Log.LogMessage(MessageImportance.High, "DevTeamProvisioning is not set, BuildAppBundle step is skipped.");
                }
                else
                {
                    AppBundlePath = generator.BuildAppBundle(XcodeProjectPath, Optimized, DevTeamProvisioning);
                }
            }
        }
        else if (GenerateCMakeProject)
        {
            generator.GenerateCMake(ProjectName, MainLibraryFileName, assemblerFiles, assemblerFilesToLink,
                                    AppDir, binDir, MonoRuntimeHeaders, !isDevice, UseConsoleUITemplate, ForceAOT, ForceInterpreter, InvariantGlobalization, Optimized, EnableRuntimeLogging, EnableAppSandbox, DiagnosticPorts, RuntimeComponents, NativeMainSource);
        }

        return(true);
    }
Exemple #2
0
    public (string apk, string packageId) BuildApk(
        string abi,
        string mainLibraryFileName,
        string monoRuntimeHeaders)
    {
        if (string.IsNullOrEmpty(AppDir) || !Directory.Exists(AppDir))
        {
            throw new ArgumentException($"AppDir='{AppDir}' is empty or doesn't exist");
        }

        if (!string.IsNullOrEmpty(mainLibraryFileName) && !File.Exists(Path.Combine(AppDir, mainLibraryFileName)))
        {
            throw new ArgumentException($"MainLibraryFileName='{mainLibraryFileName}' was not found in AppDir='{AppDir}'");
        }

        var networkSecurityConfigFilePath = Path.Combine(AppDir, "res", "xml", "network_security_config.xml");

        if (IncludeNetworkSecurityConfig && !File.Exists(networkSecurityConfigFilePath))
        {
            throw new ArgumentException($"IncludeNetworkSecurityConfig is set but the file '{networkSecurityConfigFilePath}' was not found");
        }

        if (string.IsNullOrEmpty(abi))
        {
            throw new ArgumentException("abi should not be empty (e.g. x86, x86_64, armeabi-v7a or arm64-v8a");
        }

        if (!string.IsNullOrEmpty(ProjectName) && ProjectName.Contains(' '))
        {
            throw new ArgumentException($"ProjectName='{ProjectName}' should not not contain spaces.");
        }

        if (string.IsNullOrEmpty(AndroidSdk))
        {
            AndroidSdk = Environment.GetEnvironmentVariable("ANDROID_SDK_ROOT");
        }

        if (string.IsNullOrEmpty(AndroidNdk))
        {
            AndroidNdk = Environment.GetEnvironmentVariable("ANDROID_NDK_ROOT");
        }

        if (string.IsNullOrEmpty(AndroidSdk) || !Directory.Exists(AndroidSdk))
        {
            throw new ArgumentException($"Android SDK='{AndroidSdk}' was not found or empty (can be set via ANDROID_SDK_ROOT envvar).");
        }

        if (string.IsNullOrEmpty(AndroidNdk) || !Directory.Exists(AndroidNdk))
        {
            throw new ArgumentException($"Android NDK='{AndroidNdk}' was not found or empty (can be set via ANDROID_NDK_ROOT envvar).");
        }

        if (ForceInterpreter && ForceAOT)
        {
            throw new InvalidOperationException("Interpreter and AOT cannot be enabled at the same time");
        }

        if (!string.IsNullOrEmpty(DiagnosticPorts))
        {
            bool validDiagnosticsConfig = false;

            if (string.IsNullOrEmpty(RuntimeComponents))
            {
                validDiagnosticsConfig = false;
            }
            else if (RuntimeComponents.Equals("*", StringComparison.OrdinalIgnoreCase))
            {
                validDiagnosticsConfig = true;
            }
            else if (RuntimeComponents.Contains("diagnostics_tracing", StringComparison.OrdinalIgnoreCase))
            {
                validDiagnosticsConfig = true;
            }

            if (!validDiagnosticsConfig)
            {
                throw new ArgumentException("Using DiagnosticPorts require diagnostics_tracing runtime component.");
            }
        }

        // Try to get the latest build-tools version if not specified
        if (string.IsNullOrEmpty(BuildToolsVersion))
        {
            BuildToolsVersion = GetLatestBuildTools(AndroidSdk);
        }

        // Try to get the latest API level if not specified
        if (string.IsNullOrEmpty(BuildApiLevel))
        {
            BuildApiLevel = GetLatestApiLevel(AndroidSdk);
        }

        if (string.IsNullOrEmpty(MinApiLevel))
        {
            MinApiLevel = DefaultMinApiLevel;
        }

        // make sure BuildApiLevel >= MinApiLevel
        // only if these api levels are not "preview" (not integers)
        if (int.TryParse(BuildApiLevel, out int intApi) &&
            int.TryParse(MinApiLevel, out int intMinApi) &&
            intApi < intMinApi)
        {
            throw new ArgumentException($"BuildApiLevel={BuildApiLevel} <= MinApiLevel={MinApiLevel}. " +
                                        "Make sure you've downloaded some recent build-tools in Android SDK");
        }

        string buildToolsFolder = Path.Combine(AndroidSdk, "build-tools", BuildToolsVersion);

        if (!Directory.Exists(buildToolsFolder))
        {
            throw new ArgumentException($"{buildToolsFolder} was not found.");
        }

        var assemblerFiles       = new StringBuilder();
        var assemblerFilesToLink = new StringBuilder();
        var aotLibraryFiles      = new List <string>();

        foreach (ITaskItem file in Assemblies)
        {
            // use AOT files if available
            var obj     = file.GetMetadata("AssemblerFile");
            var llvmObj = file.GetMetadata("LlvmObjectFile");
            var lib     = file.GetMetadata("LibraryFile");

            if (!string.IsNullOrEmpty(obj))
            {
                var name = Path.GetFileNameWithoutExtension(obj);
                assemblerFiles.AppendLine($"add_library({name} OBJECT {obj})");
                assemblerFilesToLink.AppendLine($"    {name}");
            }

            if (!string.IsNullOrEmpty(llvmObj))
            {
                var name = Path.GetFileNameWithoutExtension(llvmObj);
                assemblerFilesToLink.AppendLine($"    {llvmObj}");
            }

            if (!string.IsNullOrEmpty(lib))
            {
                aotLibraryFiles.Add(lib);
            }
        }

        if (ForceAOT && assemblerFiles.Length == 0 && aotLibraryFiles.Count == 0)
        {
            throw new InvalidOperationException("Need list of AOT files.");
        }

        Directory.CreateDirectory(OutputDir);
        Directory.CreateDirectory(Path.Combine(OutputDir, "bin"));
        Directory.CreateDirectory(Path.Combine(OutputDir, "obj"));
        Directory.CreateDirectory(Path.Combine(OutputDir, "assets-tozip"));
        Directory.CreateDirectory(Path.Combine(OutputDir, "assets"));
        Directory.CreateDirectory(Path.Combine(OutputDir, "res"));

        var extensionsToIgnore = new List <string> {
            ".so", ".a"
        };

        if (StripDebugSymbols)
        {
            extensionsToIgnore.Add(".pdb");
            extensionsToIgnore.Add(".dbg");
        }

        // Copy sourceDir to OutputDir/assets-tozip (ignore native files)
        // these files then will be zipped and copied to apk/assets/assets.zip
        var assetsToZipDirectory = Path.Combine(OutputDir, "assets-tozip");

        Utils.DirectoryCopy(AppDir, assetsToZipDirectory, file =>
        {
            string fileName  = Path.GetFileName(file);
            string extension = Path.GetExtension(file);

            if (extensionsToIgnore.Contains(extension))
            {
                // ignore native files, those go to lib/%abi%
                // also, aapt is not happy about zip files
                return(false);
            }
            if (fileName.StartsWith("."))
            {
                // aapt complains on such files
                return(false);
            }
            if (file.Contains("/res/"))
            {
                // exclude everything in the `res` folder
                return(false);
            }
            return(true);
        });

        // copy the res directory as is
        if (Directory.Exists(Path.Combine(AppDir, "res")))
        {
            Utils.DirectoryCopy(Path.Combine(AppDir, "res"), Path.Combine(OutputDir, "res"));
        }

        // add AOT .so libraries
        foreach (var aotlib in aotLibraryFiles)
        {
            File.Copy(aotlib, Path.Combine(assetsToZipDirectory, Path.GetFileName(aotlib)));
        }

        // tools:
        string dx               = Path.Combine(buildToolsFolder, "dx");
        string d8               = Path.Combine(buildToolsFolder, "d8");
        string aapt             = Path.Combine(buildToolsFolder, "aapt");
        string zipalign         = Path.Combine(buildToolsFolder, "zipalign");
        string apksigner        = Path.Combine(buildToolsFolder, "apksigner");
        string androidJar       = Path.Combine(AndroidSdk, "platforms", "android-" + BuildApiLevel, "android.jar");
        string androidToolchain = Path.Combine(AndroidNdk, "build", "cmake", "android.toolchain.cmake");
        string javac            = "javac";
        string cmake            = "cmake";
        string zip              = "zip";

        Utils.RunProcess(logger, zip, workingDir: assetsToZipDirectory, args: "-q -r ../assets/assets.zip .");
        Directory.Delete(assetsToZipDirectory, true);

        if (!File.Exists(androidJar))
        {
            throw new ArgumentException($"API level={BuildApiLevel} is not downloaded in Android SDK");
        }

        // 1. Build libmonodroid.so` via cmake

        string nativeLibraries = "";
        string monoRuntimeLib  = "";

        if (StaticLinkedRuntime)
        {
            monoRuntimeLib = Path.Combine(AppDir, "libmonosgen-2.0.a");
        }
        else
        {
            monoRuntimeLib = Path.Combine(AppDir, "libmonosgen-2.0.so");
        }

        if (!File.Exists(monoRuntimeLib))
        {
            throw new ArgumentException($"{monoRuntimeLib} was not found");
        }
        else
        {
            nativeLibraries += $"{monoRuntimeLib}{Environment.NewLine}";
        }

        if (StaticLinkedRuntime)
        {
            string[] staticComponentStubLibs = Directory.GetFiles(AppDir, "libmono-component-*-stub-static.a");
            bool     staticLinkAllComponents = false;
            string[] staticLinkedComponents  = Array.Empty <string>();

            if (!string.IsNullOrEmpty(RuntimeComponents) && RuntimeComponents.Equals("*", StringComparison.OrdinalIgnoreCase))
            {
                staticLinkAllComponents = true;
            }
            else if (!string.IsNullOrEmpty(RuntimeComponents))
            {
                staticLinkedComponents = RuntimeComponents.Split(";");
            }

            // by default, component stubs will be linked and depending on how mono runtime has been build,
            // stubs can disable or dynamic load components.
            foreach (string staticComponentStubLib in staticComponentStubLibs)
            {
                string componentLibToLink = staticComponentStubLib;
                if (staticLinkAllComponents)
                {
                    // static link component.
                    componentLibToLink = componentLibToLink.Replace("-stub-static.a", "-static.a", StringComparison.OrdinalIgnoreCase);
                }
                else
                {
                    foreach (string staticLinkedComponent in staticLinkedComponents)
                    {
                        if (componentLibToLink.Contains(staticLinkedComponent, StringComparison.OrdinalIgnoreCase))
                        {
                            // static link component.
                            componentLibToLink = componentLibToLink.Replace("-stub-static.a", "-static.a", StringComparison.OrdinalIgnoreCase);
                            break;
                        }
                    }
                }

                // if lib doesn't exist (primarily due to runtime build without static lib support), fallback linking stub lib.
                if (!File.Exists(componentLibToLink))
                {
                    logger.LogMessage(MessageImportance.High, $"\nCouldn't find static component library: {componentLibToLink}, linking static component stub library: {staticComponentStubLib}.\n");
                    componentLibToLink = staticComponentStubLib;
                }

                nativeLibraries += $"    {componentLibToLink}{Environment.NewLine}";
            }

            // There's a circular dependency between static mono runtime lib and static component libraries.
            // Adding mono runtime lib before and after component libs will resolve issues with undefined symbols
            // due to circular dependency.
            nativeLibraries += $"    {monoRuntimeLib}{Environment.NewLine}";
        }

        nativeLibraries += assemblerFilesToLink.ToString();

        string aotSources = assemblerFiles.ToString();

        string cmakeLists = Utils.GetEmbeddedResource("CMakeLists-android.txt")
                            .Replace("%MonoInclude%", monoRuntimeHeaders)
                            .Replace("%NativeLibrariesToLink%", nativeLibraries)
                            .Replace("%AotSources%", aotSources)
                            .Replace("%AotModulesSource%", string.IsNullOrEmpty(aotSources) ? "" : "modules.c");

        var defines = new StringBuilder();

        if (ForceInterpreter)
        {
            defines.AppendLine("add_definitions(-DFORCE_INTERPRETER=1)");
        }
        else if (ForceAOT)
        {
            defines.AppendLine("add_definitions(-DFORCE_AOT=1)");
            if (aotLibraryFiles.Count == 0)
            {
                defines.AppendLine("add_definitions(-DSTATIC_AOT=1)");
            }
        }

        if (ForceFullAOT)
        {
            defines.AppendLine("add_definitions(-DFULL_AOT=1)");
        }

        if (!string.IsNullOrEmpty(DiagnosticPorts))
        {
            defines.AppendLine("add_definitions(-DDIAGNOSTIC_PORTS=\"" + DiagnosticPorts + "\")");
        }

        cmakeLists = cmakeLists.Replace("%Defines%", defines.ToString());

        File.WriteAllText(Path.Combine(OutputDir, "CMakeLists.txt"), cmakeLists);

        File.WriteAllText(Path.Combine(OutputDir, "monodroid.c"), Utils.GetEmbeddedResource("monodroid.c"));

        string cmakeGenArgs = $"-DCMAKE_TOOLCHAIN_FILE={androidToolchain} -DANDROID_ABI=\"{abi}\" -DANDROID_STL=none " +
                              $"-DANDROID_PLATFORM=android-{MinApiLevel} -B monodroid";

        string cmakeBuildArgs = "--build monodroid";

        if (StripDebugSymbols)
        {
            // Use "-s" to strip debug symbols, it complains it's unused but it works
            cmakeGenArgs   += " -DCMAKE_BUILD_TYPE=MinSizeRel -DCMAKE_C_FLAGS=\"-s -Wno-unused-command-line-argument\"";
            cmakeBuildArgs += " --config MinSizeRel";
        }
        else
        {
            cmakeGenArgs   += " -DCMAKE_BUILD_TYPE=Debug";
            cmakeBuildArgs += " --config Debug";
        }

        Utils.RunProcess(logger, cmake, workingDir: OutputDir, args: cmakeGenArgs);
        Utils.RunProcess(logger, cmake, workingDir: OutputDir, args: cmakeBuildArgs);

        // 2. Compile Java files

        string javaSrcFolder = Path.Combine(OutputDir, "src", "net", "dot");

        Directory.CreateDirectory(javaSrcFolder);

        string javaActivityPath = Path.Combine(javaSrcFolder, "MainActivity.java");
        string monoRunnerPath   = Path.Combine(javaSrcFolder, "MonoRunner.java");

        Regex checkNumerics = new Regex(@"\.(\d)");

        if (!string.IsNullOrEmpty(ProjectName) && checkNumerics.IsMatch(ProjectName))
        {
            ProjectName = checkNumerics.Replace(ProjectName, @"_$1");
        }

        string packageId = $"net.dot.{ProjectName}";

        File.WriteAllText(javaActivityPath,
                          Utils.GetEmbeddedResource("MainActivity.java")
                          .Replace("%EntryPointLibName%", Path.GetFileName(mainLibraryFileName)));
        if (!string.IsNullOrEmpty(NativeMainSource))
        {
            File.Copy(NativeMainSource, javaActivityPath, true);
        }

        string networkSecurityConfigAttribute =
            IncludeNetworkSecurityConfig
                ? "a:networkSecurityConfig=\"@xml/network_security_config\""
                : string.Empty;

        string envVariables = "";

        foreach (ITaskItem item in EnvironmentVariables)
        {
            string name  = item.ItemSpec;
            string value = item.GetMetadata("Value");
            envVariables += $"\t\tsetEnv(\"{name}\", \"{value}\");\n";
        }

        string monoRunner = Utils.GetEmbeddedResource("MonoRunner.java")
                            .Replace("%EntryPointLibName%", Path.GetFileName(mainLibraryFileName))
                            .Replace("%EnvVariables%", envVariables);

        File.WriteAllText(monoRunnerPath, monoRunner);

        File.WriteAllText(Path.Combine(OutputDir, "AndroidManifest.xml"),
                          Utils.GetEmbeddedResource("AndroidManifest.xml")
                          .Replace("%PackageName%", packageId)
                          .Replace("%NetworkSecurityConfig%", networkSecurityConfigAttribute)
                          .Replace("%MinSdkLevel%", MinApiLevel));

        string javaCompilerArgs = $"-d obj -classpath src -bootclasspath {androidJar} -source 1.8 -target 1.8 ";

        Utils.RunProcess(logger, javac, javaCompilerArgs + javaActivityPath, workingDir: OutputDir);
        Utils.RunProcess(logger, javac, javaCompilerArgs + monoRunnerPath, workingDir: OutputDir);

        if (File.Exists(d8))
        {
            string[] classFiles = Directory.GetFiles(Path.Combine(OutputDir, "obj"), "*.class", SearchOption.AllDirectories);

            if (!classFiles.Any())
            {
                throw new InvalidOperationException("Didn't find any .class files");
            }

            Utils.RunProcess(logger, d8, $"--no-desugaring {string.Join(" ", classFiles)}", workingDir: OutputDir);
        }
        else
        {
            Utils.RunProcess(logger, dx, "--dex --output=classes.dex obj", workingDir: OutputDir);
        }

        // 3. Generate APK

        string debugModeArg = StripDebugSymbols ? string.Empty : "--debug-mode";
        string apkFile      = Path.Combine(OutputDir, "bin", $"{ProjectName}.unaligned.apk");
        string resources    = IncludeNetworkSecurityConfig ? "-S res" : string.Empty;

        Utils.RunProcess(logger, aapt, $"package -f -m -F {apkFile} -A assets {resources} -M AndroidManifest.xml -I {androidJar} {debugModeArg}", workingDir: OutputDir);

        var dynamicLibs = new List <string>();

        dynamicLibs.Add(Path.Combine(OutputDir, "monodroid", "libmonodroid.so"));
        dynamicLibs.AddRange(Directory.GetFiles(AppDir, "*.so").Where(file => Path.GetFileName(file) != "libmonodroid.so"));

        // add all *.so files to lib/%abi%/

        string[] dynamicLinkedComponents  = Array.Empty <string>();
        bool     dynamicLinkAllComponents = false;

        if (!StaticLinkedRuntime && !string.IsNullOrEmpty(RuntimeComponents) && RuntimeComponents.Equals("*", StringComparison.OrdinalIgnoreCase))
        {
            dynamicLinkAllComponents = true;
        }
        if (!string.IsNullOrEmpty(RuntimeComponents) && !StaticLinkedRuntime)
        {
            dynamicLinkedComponents = RuntimeComponents.Split(";");
        }

        Directory.CreateDirectory(Path.Combine(OutputDir, "lib", abi));
        foreach (var dynamicLib in dynamicLibs)
        {
            string dynamicLibName = Path.GetFileName(dynamicLib);
            string destRelative   = Path.Combine("lib", abi, dynamicLibName);

            if (dynamicLibName == "libmonosgen-2.0.so" && StaticLinkedRuntime)
            {
                // we link mono runtime statically into libmonodroid.so
                // make sure dynamic runtime is not included in package.
                if (File.Exists(destRelative))
                {
                    File.Delete(destRelative);
                }
                continue;
            }

            if (dynamicLibName.Contains("libmono-component-", StringComparison.OrdinalIgnoreCase))
            {
                bool includeComponent = dynamicLinkAllComponents;
                if (!StaticLinkedRuntime && !includeComponent)
                {
                    foreach (string dynamicLinkedComponent in dynamicLinkedComponents)
                    {
                        if (dynamicLibName.Contains(dynamicLinkedComponent, StringComparison.OrdinalIgnoreCase))
                        {
                            includeComponent = true;
                            break;
                        }
                    }
                }
                if (!includeComponent)
                {
                    // make sure dynamic component is not included in package.
                    if (File.Exists(destRelative))
                    {
                        File.Delete(destRelative);
                    }
                    continue;
                }
            }

            // NOTE: we can run android-strip tool from NDK to shrink native binaries here even more.

            File.Copy(dynamicLib, Path.Combine(OutputDir, destRelative), true);
            Utils.RunProcess(logger, aapt, $"add {apkFile} {destRelative}", workingDir: OutputDir);
        }
        Utils.RunProcess(logger, aapt, $"add {apkFile} classes.dex", workingDir: OutputDir);

        // 4. Align APK

        string alignedApk = Path.Combine(OutputDir, "bin", $"{ProjectName}.apk");

        AlignApk(apkFile, alignedApk, zipalign);
        // we don't need the unaligned one any more
        File.Delete(apkFile);

        // 5. Generate key (if needed) & sign the apk
        SignApk(alignedApk, apksigner);

        logger.LogMessage(MessageImportance.High, $"\nAPK size: {(new FileInfo(alignedApk).Length / 1000_000.0):0.#} Mb.\n");

        return(alignedApk, packageId);
    }