diegosasw
diegosasw

Reputation: 15684

How to configure GitLab CI/CD for .NET 7 Maui (Android)

I am a bit shocked Linux does not support building .NET MAUI apps unless they are

android                    .NET SDK Workload for building Android applications.
macos                      .NET SDK Workload for building macOS applications.  
maui-android               .NET MAUI SDK for Android                           
maui-tizen                 .NET MAUI SDK for Tizen                             
maui-windows               .NET MAUI SDK for Windows                           
runtimes-windows           Windows Runtime Packs                               
runtimes-windows-net6      Windows Runtime Packs                               
wasm-experimental          .NET WebAssembly experimental tooling               
wasm-tools                 .NET WebAssembly build tools                        
wasm-tools-net6            .NET WebAssembly build tools for net6.0  

In any case, I'd like to know how do .NET Maui developers automate CI/CD in GitLab. Is there any built-in mechanism like there is in GitHub or Azure Pipelines? Or does it have to be configured manually? Could I see an example?

This is what I've been trying so far, considering that the base image mcr.microsoft.com/dotnet/sdk:7.0 won't be able to be used for certain platforms unfortunately..

I have created a custom Docker image. Let's call it dotnet-maui

FROM mcr.microsoft.com/dotnet/sdk:7.0

# Install workload maui
RUN dotnet workload search
RUN dotnet workload install android maui-android --ignore-failed-sources

Is there any better alternative? Maybe an image that uses MacOs or windows where MAUI is supported?

And now I use that image as the GitLab base image for my MAUI apps. Specifically for one that targets android only, but I am still unable to even compile..

dotnet build -f:net7.0-android -c:Release

throws an error

 dotnet build -f:net7.0-android -c:Release
MSBuild version 17.4.0+18d5aef85 for .NET
  Determining projects to restore...
  Restored /builds/roundev/roundev-easy-qr/src/EasyQr/EasyQr.csproj (in 16.86 sec).
/usr/share/dotnet/packs/Microsoft.Android.Sdk.Linux/33.0.4/tools/Xamarin.Android.Tooling.targets(70,5): error XA5300: The Android SDK directory could not be found. Check that the Android SDK Manager in Visual Studio shows a valid installation. To use a custom SDK path for a command line build, set the 'AndroidSdkDirectory' MSBuild property to the custom path. [/builds/roundev/roundev-easy-qr/src/EasyQr/EasyQr.csproj::TargetFramework=net7.0-android]
Build FAILED.
/usr/share/dotnet/packs/Microsoft.Android.Sdk.Linux/33.0.4/tools/Xamarin.Android.Tooling.targets(70,5): error XA5300: The Android SDK directory could not be found. Check that the Android SDK Manager in Visual Studio shows a valid installation. To use a custom SDK path for a command line build, set the 'AndroidSdkDirectory' MSBuild property to the custom path. [/builds/roundev/roundev-easy-qr/src/EasyQr/EasyQr.csproj::TargetFramework=net7.0-android]
    0 Warning(s)
    1 Error(s)
Time Elapsed 00:00:00.58
Cleaning up project directory and file based variables 00:01
ERROR: Job failed: exit code 1

How should I install and configure this Android SDK?

PS: The fact that the errors show details about Visual Studio (which is a graphical user interface IDE) is a bit concerning.


UPDATE 1 (2023-01-02)

I've attempted to build in Ubuntu 22.04 with CLI and I am having problems. The goal is to have all this automated in a Docker image.

First I install maui-android

dotnet workload install maui-android --ignore-failed-sources

Then I install Android SDK. 2 Options:

sudo apt update && sudo apt install -y android-sdk

or

sudo snap install androidsdk

Either way, the following attempt to build, fails

dotnet build -f net7.0-android /p:AndroidSdkDirectory=/usr/lib/android-sdk

with error about API level 33 not installed

MSBuild version 17.4.0+18d5aef85 for .NET
  Determining projects to restore...
  All projects are up-to-date for restore.
/usr/share/dotnet/packs/Microsoft.Android.Sdk.Linux/33.0.4/tools/Xamarin.Android.Tooling.targets(100,5): error XA5207: Could not find android.jar for API level 33. This means the Android SDK platform for API level 33 is not installed. Either install it in the Android SDK Manager (Tools > Open Android SDK Manager...), or change the Xamarin.Android project to target an API version that is installed. (/usr/lib/android-sdk/platforms/android-33/android.jar missing.) [/media/diegosasw/data/src/roundev/roundev-easy-qr/src/EasyQr/EasyQr.csproj::TargetFramework=net7.0-android]

I have attempted to explicitly install API 33

yes | androidsdk "platform-tools" "platforms;android-33"

which creates a ~/AndroidSDK/platforms/android-33 with a jar inside

But

dotnet build -f net7.0-android /p:AndroidSdkDirectory=/home/diegosasw/AndroidSDK/platforms/android-33

also fails

MSBuild version 17.4.0+18d5aef85 for .NET
  Determining projects to restore...
  All projects are up-to-date for restore.
/usr/share/dotnet/packs/Microsoft.Android.Sdk.Linux/33.0.4/tools/Xamarin.Android.Tooling.targets(70,5): error XA5300: The Android SDK directory could not be found. Check that the Android SDK Manager in Visual Studio shows a valid installation. To use a custom SDK path for a command line build, set the 'AndroidSdkDirectory' MSBuild property to the custom path. [/media/diegosasw/data/src/roundev/roundev-easy-qr/src/EasyQr/EasyQr.csproj::TargetFramework=net7.0-android]

I would appreciate a working sample that I could use both locally in Linux Ubuntu, and in GitLab CI/CD


UPDATE 2 (2023-01-02)

I got something working locally, but still unable to find a good solution for GitLab CI/CD (too large base image is causing problems)

I have the following *.csproj

<Project Sdk="Microsoft.NET.Sdk.Razor">

    <PropertyGroup>
        <TargetFrameworks>net7.0-android</TargetFrameworks>
        <OutputType>Exe</OutputType>
        <RootNamespace>MySample</RootNamespace>
        <UseMaui>true</UseMaui>
        <SingleProject>true</SingleProject>
        <ImplicitUsings>enable</ImplicitUsings>
        <EnableDefaultCssItems>false</EnableDefaultCssItems>

        <!-- Display name -->
        <ApplicationTitle>MySample</ApplicationTitle>

        <!-- App Identifier -->
        <ApplicationId>com.roundev.mysample</ApplicationId>
        <ApplicationIdGuid>840AB3E3-0460-46FE-BDEA-94EEBB053DE2</ApplicationIdGuid>

        <!-- Versions -->
        <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
        <ApplicationVersion>1</ApplicationVersion>

        <SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">24.0</SupportedOSPlatformVersion>
    </PropertyGroup>

    <PropertyGroup Condition="$(TargetFramework.Contains('-android')) and '$(Configuration)' == 'Release'">
        <AndroidKeyStore>True</AndroidKeyStore>
        <AndroidSigningKeyStore>myapp.keystore</AndroidSigningKeyStore>
        <AndroidSigningKeyAlias>myalias</AndroidSigningKeyAlias>
        <AndroidSigningKeyPass></AndroidSigningKeyPass>
        <AndroidSigningStorePass></AndroidSigningStorePass>
    </PropertyGroup>


    <ItemGroup>
        <!-- App Icon -->
        <MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />

        <!-- Splash Screen -->
        <MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />

        <!-- Images -->
        <MauiImage Include="Resources\Images\*" />
        <MauiImage Update="Resources\Images\dotnet_bot.svg" BaseSize="168,208" />

        <!-- Custom Fonts -->
        <MauiFont Include="Resources\Fonts\*" />

        <!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
        <MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
    </ItemGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="7.0.0" />
    </ItemGroup>

</Project>

At localhost (Ubuntu 22.04)

At localhost, I managed to compile and publish signed APK/AAB with following steps:

Android SDK SDK Tools

Assuming SDK .NET 7 is already installed.

  1. Install workload maui-android on the top of SDK .NET
    dotnet workload install maui-android --ignore-failed-sources
    
  2. Installed Android SDK (Api level 33) through the Jetbrains Toolbox by installing Android Studio, with Android SDK build-tools also. Everything is under /home/diegosasw/Android/Sdk
  3. Created keystore
    keytool -genkey -v -keystore myapp.keystore -alias myalias -keyalg RSA -keysize 2048 -validity 36500
    
    and entered password and details. The generated myapp.keystore, I place it at the same level as the *.csproj (Ideally it should not be versioned)
  4. Compile
    dotnet build -f net7.0-android /p:AndroidSdkDirectory=/home/diegosasw/Android/Sdk
    
  5. Publish
    dotnet publish -f:net7.0-android -c:Release -o dist /p:AndroidSigningKeyPass=<the_password> /p:AndroidSigningStorePass=<the_password> /p:AndroidSdkDirectory=/home/diegosasw/Android/Sdk
    
    and the dist folder will contain the AAB, signed AAB and signed APK.

The process for CI/CD automation would have to be similar, but with automated Android SDK installation, plus other tools,

At GitLab CI/CD

  1. Firstly I create an image with the following Dockerfile. It's very heavy, and possibly it could be parametized, and greatly improved.

    ARG REPO=mcr.microsoft.com/dotnet/aspnet
    FROM $REPO:7.0.1-jammy-amd64 AS platform
    
    ENV \
        # Unset ASPNETCORE_URLS from aspnet base image
        ASPNETCORE_URLS= \
        # Do not generate certificate
        DOTNET_GENERATE_ASPNET_CERTIFICATE=false \
        # Do not show first run text
        DOTNET_NOLOGO=true \
        # SDK version
        DOTNET_SDK_VERSION=7.0.101 \
        # Enable correct mode for dotnet watch (only mode supported in a container)
        DOTNET_USE_POLLING_FILE_WATCHER=true \
        # Skip extraction of XML docs - generally not useful within an image/container - helps performance
        NUGET_XMLDOC_MODE=skip \
        # PowerShell telemetry for docker image usage
        POWERSHELL_DISTRIBUTION_CHANNEL=PSDocker-DotnetSDK-Ubuntu-22.04
    
    RUN apt-get update \
        && apt-get install -y --no-install-recommends \
            curl \
            git \
            wget \
        && rm -rf /var/lib/apt/lists/*
    
    # Install .NET SDK
    RUN curl -fSL --output dotnet.tar.gz https://dotnetcli.azureedge.net/dotnet/Sdk/$DOTNET_SDK_VERSION/dotnet-sdk-$DOTNET_SDK_VERSION-linux-x64.tar.gz \
        && dotnet_sha512='cf289ad0e661c38dcda7f415b3078a224e8347528448429d62c0f354ee951f4e7bef9cceaf3db02fb52b5dd7be987b7a4327ca33fb9239b667dc1c41c678095c' \
        && echo "$dotnet_sha512  dotnet.tar.gz" | sha512sum -c - \
        && mkdir -p /usr/share/dotnet \
        && tar -oxzf dotnet.tar.gz -C /usr/share/dotnet ./packs ./sdk ./sdk-manifests ./templates ./LICENSE.txt ./ThirdPartyNotices.txt \
        && rm dotnet.tar.gz \
        # Trigger first run experience by running arbitrary cmd
        && dotnet help
    
    # Install PowerShell global tool
    RUN powershell_version=7.3.0 \
        && curl -fSL --output PowerShell.Linux.x64.$powershell_version.nupkg https://pwshtool.blob.core.windows.net/tool/$powershell_version/PowerShell.Linux.x64.$powershell_version.nupkg \
        && powershell_sha512='c4a72142e2bfae0c2a64a662f1baa27940f1db8a09384c90843163e339581d8d41824145fb9f79c680f9b7906043365e870d48d751ab8809c15a590f47562ae6' \
        && echo "$powershell_sha512  PowerShell.Linux.x64.$powershell_version.nupkg" | sha512sum -c - \
        && mkdir -p /usr/share/powershell \
        && dotnet tool install --add-source / --tool-path /usr/share/powershell --version $powershell_version PowerShell.Linux.x64 \
        && dotnet nuget locals all --clear \
        && rm PowerShell.Linux.x64.$powershell_version.nupkg \
        && ln -s /usr/share/powershell/pwsh /usr/bin/pwsh \
        && chmod 755 /usr/share/powershell/pwsh \
        # To reduce image size, remove the copy nupkg that nuget keeps.
        && find /usr/share/powershell -print | grep -i '.*[.]nupkg$' | xargs rm
    
    # JAVA
    RUN apt-get update && \
        apt-get install -y openjdk-11-jdk && \
        rm -rf /var/lib/apt/lists/*
    
    ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/
    
    # Install workload maui
    RUN dotnet workload install maui-android --ignore-failed-sources
    
    # Utils
    RUN apt-get update && apt-get install -y \
        unzip \
        jq \
        bzip2 \
        libzip4 \
        libzip-dev && \
        rm -rf /var/lib/apt/lists/*
    
    # Install Android SDK
    RUN mkdir -p /usr/lib/android-sdk/cmdline-tools/latest && \
        curl -k "https://dl.google.com/android/repository/commandlinetools-linux-9123335_latest.zip" -o commandlinetools-linux.zip && \
        unzip -q commandlinetools-linux.zip -d /usr/lib/android-sdk/tmp && \
        mv  /usr/lib/android-sdk/tmp/cmdline-tools/* /usr/lib/android-sdk/cmdline-tools/latest && \
        rm -rf /usr/lib/android-sdk/tmp/ && \
        rm commandlinetools-linux.zip 
    
    ENV ANDROID_SDK_ROOT=/usr/lib/android-sdk
    ENV PATH=$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$PATH
    
    RUN yes | sdkmanager --licenses && \
        sdkmanager "platform-tools" && \
        sdkmanager "ndk-bundle" && \
        sdkmanager "build-tools;33.0.0" "platforms;android-33"
    
  2. At the repo in GitLab, I create a secure file and upload the myapp.keystore (and I remove it from the filesystem).

  3. At the repo in GitLab, I create a CI/CD environment variable KEYSTORE_PASSWORD with the password value

  4. The idea is to have a .gitlab-ci.yml similar to this

    image: registry.gitlab.com/your_image_generated_out_of_Dockerfile:latest
    
    variables:
      PUBLISH_OUTPUT_DIR: dist
      ANDROID_SIGNING_KEY_PASS: $KEYSTORE_PASSWORD
      ANDROID_SIGNING_STORE_PASS: $KEYSTORE_PASSWORD
    
    stages:
      - build
      - publish
      - delivery
    
    build:
      stage: build
      script:
        - dotnet build -f net7.0-android /p:AndroidSdkDirectory=/usr/lib/android-sdk 
      artifacts:
        paths:
          - test
        expire_in: 8 hour
      rules:
        - if: '$CI_COMMIT_TAG == null'
    
    publish_android:
      stage: publish
      variables:
        SECURE_FILES_DOWNLOAD_PATH: './'
      before_script:
        - APP_VERSION=$(cat ./version/semver)
        - curl --silent "https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer" | bash
      script:
        - dotnet publish 
          -f:net7.0-android 
          -c:Release 
          -o $PUBLISH_OUTPUT_DIR 
          /p:AndroidSigningKeyPass=$ANDROID_SIGNING_KEY_PASS 
          /p:AndroidSigningStorePass=$ANDROID_SIGNING_STORE_PASS 
          /p:AndroidSdkDirectory=/usr/lib/android-sdk 
          -p:Version=$APP_VERSION
      allow_failure: false
      artifacts:
        paths: 
          - $PUBLISH_OUTPUT_DIR/
        expire_in: 8 hour
      rules:
        - if: $CI_COMMIT_BRANCH == "main"
          when: on_success
        - if: '$CI_COMMIT_TAG == null'   
    

    but unfortunately I cannot even generate the Docker image to use as base image because I run out of space due to the huge image size I'm attempting to build.

The question remains:

Upvotes: 10

Views: 3858

Answers (1)

diegosasw
diegosasw

Reputation: 15684

I got it working and with a smaller custom image built on the top of the "official" Microsoft's SDK .NET 7, without adding Android NDK (which I don't think it was needed).

This is my docker image

FROM mcr.microsoft.com/dotnet/sdk:7.0
MAINTAINER roundev

# JAVA
RUN apt-get update && \
    apt-get install -y openjdk-11-jdk && \
    rm -rf /var/lib/apt/lists/*

ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/

# Install workload maui
RUN dotnet workload install maui-android --ignore-failed-sources

# Utils
RUN apt-get update && apt-get install -y \
    unzip && \
    rm -rf /var/lib/apt/lists/*

# Install Android SDK
RUN mkdir -p /usr/lib/android-sdk/cmdline-tools/latest && \
    curl -k "https://dl.google.com/android/repository/commandlinetools-linux-9123335_latest.zip" -o commandlinetools-linux.zip && \
    unzip -q commandlinetools-linux.zip -d /usr/lib/android-sdk/tmp && \
    mv  /usr/lib/android-sdk/tmp/cmdline-tools/* /usr/lib/android-sdk/cmdline-tools/latest && \
    rm -rf /usr/lib/android-sdk/tmp/ && \
    rm commandlinetools-linux.zip 

ENV ANDROID_SDK_ROOT=/usr/lib/android-sdk
ENV PATH=$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$PATH

RUN yes | sdkmanager --licenses && \
    sdkmanager "platform-tools" && \
    #sdkmanager "ndk-bundle" && \ # I Don't think this was needed at all
    sdkmanager "build-tools;33.0.0" "platforms;android-33"

This, generated and stored in GitLab container registry, takes "only" 2.5GB.

So now the pipeline is successful with this .gitlab-ci.yml

image: registry.gitlab.com/roundev/devops/dotnet-maui:latest

variables:
  PUBLISH_OUTPUT_DIR: dist
  ANDROID_SIGNING_KEY_PASS: $KEYSTORE_PASSWORD
  ANDROID_SIGNING_STORE_PASS: $KEYSTORE_PASSWORD

stages:
  - build
  - publish
  - delivery

build:
  stage: build
  script:
    - dotnet build -f net7.0-android /p:AndroidSdkDirectory=/usr/lib/android-sdk 
  rules:
    - if: '$CI_COMMIT_TAG == null'

publish_android:
  stage: publish
  variables:
    SECURE_FILES_DOWNLOAD_PATH: './'
  before_script:
    - curl --silent "https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/download-secure-files/-/raw/main/installer" | bash
  script:
    - dotnet publish 
      -f:net7.0-android 
      -c:Release 
      -o $PUBLISH_OUTPUT_DIR 
      /p:AndroidSigningKeyPass=$ANDROID_SIGNING_KEY_PASS 
      /p:AndroidSigningStorePass=$ANDROID_SIGNING_STORE_PASS 
      /p:AndroidSdkDirectory=/usr/lib/android-sdk
  allow_failure: false
  artifacts:
    paths: 
      - $PUBLISH_OUTPUT_DIR/
    expire_in: 8 hour
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: on_success
    - if: '$CI_COMMIT_TAG == null'

Notice it is using a secure file where I upload my myapp.keystore and a CI/CD variable KEYSTORE_PASSWORD with the signing key pass and store pass (it's the same in my case).

Upvotes: 11

Related Questions