Part 3 - Create a Jenkins pipeline to deploy MSIX Desktop Apps: Packaging solutions outside VS
Published Feb 14 2022 06:38 AM 4,208 Views
Microsoft

 

logo.png

 

 

 

In this third part, it is described how to setup Jenkins pipeline to package a Desktop App to MSIX.

 

The idea is to package a Java GUI solution developed on Visual Studio Code and package it with the Windows Application Packaging Project available for Visual Studio. Yes, there is still a dependency with Visual Studio. In the next post, part 4, it is described how to package a Desktop application without Visual Studio.

 

Parts:

  1. Setup the Jenkins environment: install Jenkins and the required tools.
  2. Packaging a Visual Studio solution: for applications that use Visual Studio IDE, like Windows Forms and WPF.
  3. Packaging a solution developed outside Visual Studio: for applications developed outside VS, i.e., in others IDEs like Eclipse or Visual Studio Code, for Java GUI application.
  4. Packaging using the VB6RegistryTool: despite of the name, the tool can be used by any technology.

 

1. Java GUI Solution

 

In this section it will be demonstrated how to create the Java GUI solution with VS Code (you can use your preferred IDE) and then use the Windows Application Packaging Project on Visual Studio 2022 to generate the MSIX file.

 

In case you prefer, you can skip this step, since the solution is already available on jenkins_msix repo.

 

You can download VS Code here.

 

You need Java JDK installed. I am using OpenJDK 11 (Eclipse Temurin JDK more specifically). You can download it from the adoptium.net site or through this directly link.

 

More information about the steps describe here at Working with GUI applications in VS Code.

 

Create the Java GUI application

 

Before starting, make sure you have Maven installed. You can download Maven here.

 

On VS Code, click on Extensions (CTRL+Shift+X), search for extension pack for java and click on Install to install the extension:

 

p3vscode01.png

 

 

In VS Code, open the Command Palette (Ctrl+Shift+P) and then select the command Maven: Create Maven Project:

 

p3vscode02.png

 

 

Select the option maven-archetype-quickstart in the list:

 

p3vscode03.png

 

 

Select the version 1.4:

 

p3vscode02-1.png

 

 

Provide the group Id to your project and press Enter:

 

p3vscode04.png

 

 

Provide the artifact Id and press Enter to continue:

 

p3vscode05.png

 

 

Select the destination folder for the project.

 

p3vscode06.png

 

 

You will be prompted to confirm the properties, just press ENTER:

 

p3vscode07.png

 

 

To run the Java application, open the Explorer folders, navigate to demo\src\main\java\com\example\App.java file and press F5:

 

p3vscode08.png

 

 

Ensure that no errors appear and that the Hello World! appears in the terminal console:

 

p3vscode11.png

 

 

Replace the App.java content by the following code that I extracted from the Create your First Java Frame using Visual Studio Code | Create Java GUI Forms using VS Code video:

 

 

package com.example;


import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;

public class App extends JFrame{


    final private Font mainFont = new Font ("Segoe print", Font.BOLD, 18);
    JTextField tfFirstName, tfLastName;
    JLabel lbWelcome ;

    public void initialize(){

        //Form panel
        JLabel lbFirstName = new JLabel("First name");
        lbFirstName.setFont(mainFont);
        
        tfFirstName = new JTextField();
        tfFirstName.setFont(mainFont);

        JLabel lbLastName = new JLabel("Last name");
        lbLastName.setFont(mainFont);
        
        tfLastName = new JTextField();
        tfLastName.setFont(mainFont);

        JPanel formPanel = new JPanel();
        formPanel.setLayout(new GridLayout(4, 1, 5,5));
        formPanel.setOpaque(false);
        formPanel.add (lbFirstName);
        formPanel.add (tfFirstName);
        formPanel.add (lbLastName);
        formPanel.add (tfLastName);

        //button
        JButton btnOK = new JButton("OK");
        btnOK.setFont(mainFont);
        btnOK.addActionListener(new ActionListener(){

            @Override
            public void actionPerformed(ActionEvent e) {
               
                String firstName = tfFirstName.getText();
                String lastName = tfLastName.getText();
                lbWelcome.setText("Hello " + firstName + " " + lastName);
            }

        });

        JButton btnClear = new JButton("Clear");
        btnClear.setFont(mainFont);

        btnClear.addActionListener(new ActionListener(){

            @Override
            public void actionPerformed(ActionEvent e) {
              
                tfFirstName.setText("");
                tfLastName.setText("");
                lbWelcome.setText("");
                            }

        });

        JPanel buttonsPanel = new JPanel();
        buttonsPanel.setLayout(new GridLayout(1,2,5,5));
        buttonsPanel.setOpaque(false);
        buttonsPanel.add(btnOK);
        buttonsPanel.add(btnClear);
 
          //welcome
          lbWelcome = new JLabel();
          lbWelcome.setFont(mainFont);
  
          JPanel mainPanel = new JPanel();
          
          mainPanel.setLayout(new BorderLayout());
          mainPanel.setBackground(new Color(128, 128, 255));
          mainPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
          mainPanel.add(formPanel, BorderLayout.NORTH);
          mainPanel.add(lbWelcome, BorderLayout.CENTER);
          mainPanel.add(buttonsPanel, BorderLayout.SOUTH);

          add(mainPanel);
  
          setTitle("Welcome");
          setSize(500,600);
          setMinimumSize(new Dimension(300,400));
          setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
          setVisible(true);


    }

    public static void main(String[] args){

        App myFrame = new App();
        myFrame.initialize();
    }
}

 

Press F5 again and make sure that the following UI is displayed:

 

p3vscode12.png

 

 

Navigate to the pom.xml file and replace its content by the following one:

 

 

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.example</groupId>
  <artifactId>demo</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>demo</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

 <build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.0</version>
             <configuration>
                <release>11</release>
                <fork>true</fork>
                <executable>C:\Program Files\Eclipse Adoptium\jdk-11.0.14.9-hotspot\bin\javac</executable>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-shade-plugin</artifactId>
            <version>1.7.1</version>
            <executions>
                <execution>
                    <phase>package</phase>
                    <goals>
                        <goal>shade</goal>
                    </goals>
                </execution>
            </executions>
            <configuration>
                <shadedArtifactAttached>true</shadedArtifactAttached>
                <shadedClassifierName>shaded</shadedClassifierName>
                <transformers>
                    <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                        <mainClass>com.example.App</mainClass>
                    </transformer>
                </transformers>
            </configuration>
        </plugin>
        <plugin>
            <groupId>com.akathist.maven.plugins.launch4j</groupId>
            <artifactId>launch4j-maven-plugin</artifactId>
            <version>1.7.25</version>
            <executions>
                <execution>
                    <id>l4j-clui</id>
                    <phase>package</phase>
                    <goals>
                        <goal>launch4j</goal>
                    </goals>
                    <configuration>
                        <headerType>gui</headerType>
                        <jar>${project.build.directory}/${artifactId}-${version}-shaded.jar</jar>
                        <outfile>${project.build.directory}/myapp.exe</outfile>
                        <downloadUrl>http://java.com/download</downloadUrl>
                        <classPath>
                            <mainClass>com.example.App</mainClass>
                            <preCp>anything</preCp>
                        </classPath>
                        
                        <jre>
                            <minVersion>11.0.1</minVersion>
                            
                        </jre>
                        <versionInfo>
                            <fileVersion>1.0.0.0</fileVersion>
                            <txtFileVersion>${project.version}</txtFileVersion>
                            <fileDescription>${project.name}</fileDescription>
                            <copyright>2022 replaceme.com</copyright>
                            <productVersion>1.0.0.0</productVersion>
                            <txtProductVersion>1.0.0.0</txtProductVersion>
                            <productName>${project.name}</productName>
                            <companyName>replaceme.com</companyName>
                            <internalName>myapp</internalName>
                            <originalFilename>myapp.exe</originalFilename>
                        </versionInfo>
                    </configuration>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>
</project>

 

In the command line, navigate to the pom.xml level and run the following command:

 

mvn clean package

 

p3vscode14.png

 

 

Navigate to the target folder and run the app to make sure that it is working:

 

p3vscode15.png

 

 

Create the the Windows Application Packaging project

The next step is creating the Windows Application Packaging Project to package the Java GUI application.

 

Open Visual Studio, select Create a new project, select Windows Application Packaging Project and click Next:

 

p3wap01.png

 

 

Provide the project name MyJavaApp.Packaging and click Create:

 

p3wap02.png

 

 

We need to provide the Windows operating system target. Provide the build 19041 for the minimum and target version, as that is the Windows 10 SDK that is installed on our Jenkins environment, and click OK:

 

p3wap03.png

 

 

The solution will be created with the following structure:

 

p3wap04.png

 

 

Now, we need to include the Java application to the project.

 

Create the folder MyApp and add the myapp.exe:

 

p3wap05.png

 

 

Click on the Package.appxmanifest and press F7 to view the code:

 

p3wap06.png

 

 

Change the Executable value by MyApp\myapp.exe, as follows:

 

p3wap07.png

 

 

By now, if we can try to build our solution, we will receive the following exception, as the Windows Application Package Project requires a reference to an application:

 

p3wap09.png

 

 

Edit the MyJavaApp.Packaging project:

 

p3wap08.png

 

 

Include in the PropertyGroup element, which contains the TargetPlatformVersion, the EntryPointExe entry with the Java relative path to the executable:

 

p3wap10.png

 

 

Save the project and build the solution to make sure that there are not errors.

 

Build the application using MSBuild command line

Before creating a Jenkins pipeline, let's make sure that the MSBuild command line that will be used to build our application is working.

 

Open the Package Manager Console (you can press CTRL+Q and type package manager console):

 

p2vs13.png

 

 

Before running the following command, make sure to provide the MSBuild.exe PATH available in your environment:

 

 

&"C:\Program Files\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\MSBuild.exe" /p:AppxBundlePlatforms=X86 /p:AppxBundle=Never /p:UapAppxPackageBuildMode=Sideloading  /p:AppxPackageSigningEnabled=false

 

In my case, the msix package file was generated on:

 

 

C:\github\jenkins_msix\02_WAP\MyJavaApp.Packaging\MyJavaApp.Packaging\AppPackages\MyJavaApp.Packaging_1.0.0.0_AnyCPU_Debug_Test

 

p3wap11.png

 

In the next section, it will demonstrate how to build a Jenkins Pipeline for this project.

 

2. Jenkins Pipeline

Pipeline is a series of tasks required to build, test, and deploy an application.

 

Create a new job

In the Jenkins Dashboard, click on the New Item option:

 

p3pipeline01.png

 

 

Provide the name Java GUI WAP MSIX for the job, select the Pipeline type of job and click on OK to proceed.

 

p3pipeline02.png

 

 

In the pipeline configuration page, check the GitHub Project to specify that this is a GitHub project and provide a GitHub URL:

 

p2pipeline03.png

 

Scroll down under the Pipeline section and change the definition to Pipeline script from SCM

 

p2pipeline04.png

 

 

Provide the Repository URL as well. Because this is a public project, we can skip the credentials:

 

p2pipeline05.png

 

 

Scroll-down to the Branches to build section, change the branch name to */main, the Jenkins script path to Jenkinsfile02 and click on Save:

 

p3pipeline03.png

 

 

Those actions were needed as we want to use the Jenkins pipeline file available in the main branch of the following repo:

 

p3pipeline04.png

 

 

Jenkins Pipeline File

In the previous section it was demonstrated how to setup the Jenkins pipeline to use a Jenkins script file available on our GitHub repository.

Follows the Jenkinsfile02 content:

 

pipeline {
  
  agent any

  tools {
        maven "Maven 3.8"
        jdk "JDK 11"
  }

  environment {
    MSBUILD = "C:\\Program Files\\Microsoft Visual Studio\\2022\\Community\\Msbuild\\Current\\Bin\\MSBuild.exe"
    CONFIG = 'Release'
    PLATFORM = 'x86'

   
  }
  
  stages {
      stage('Initialize'){
            steps{
              
              echo "PATH = ${M2_HOME}\\bin:${PATH}"
              echo "M2_HOME = C:\\Program Files (x86)\\apache-maven"
                
            }
        }
        stage('Build') {
            steps {
                dir("\\02_WAP\\demo") {
                bat 'mvn clean package'
                }
            }
            post{
              always {
                bat 'copy 02_WAP\\demo\\target\\myapp.exe 02_WAP\\MyJavaApp.Packaging\\MyJavaApp.Packaging\\MyApp /y'
              }
            }
        }

    stage('Build MSIX package') {
      steps {
        
        bat "\"${MSBUILD}\" 02_WAP\\MyJavaApp.Packaging\\MyJavaApp.Packaging.sln /p:Configuration=${env.CONFIG} /p:AppxBundlePlatforms=${env.PLATFORM}  /p:AppxBundle=Never /p:UapAppxPackageBuildMode=Sideloading  /p:AppxPackageSigningEnabled=false"
        
      }
      post{
          always {
           archiveArtifacts artifacts: '**/*.msix', followSymlinks: false
          }
      }
    }
  }
}

 

The pipeline directive is the complete script from beginning to end.

 

The agent directive instructs Jenkins to allocate an executor and workspace for the entire Pipeline. In our case, we are justing saying it can run on any agent. For example, it could be specified that it could run in a Docker container or run on a specific node.

 

The environment directive specifies a sequence of key-value pairs which will be defined as environment variables for all steps, or stage-specific steps, depending on where the environment directive is located within the Pipeline.

 

In our case, it is defined the variables MSBUILD that contains the MSBUILD path, the CONFIG with the value Release and PLATFORM with the value x86. Those variables will be used in the command line used to build our application.

 

The stages block contains on or more stage block, and each stage is going to have one or more steps. In our case, we have two stages that are used to build the Java application and later to package to MSIX.

 

The post section defines the additional step needed to keep the msix file artifact available in our build, as workspace is a temporary directory.

 

You can find more details about the Jenkins pipeline syntax in the post Getting started with Pipeline.

 

Switch back to Jenkins, click on Dashboard and click on the Java GUI WAP MSIX pipeline:

 

p3pipeline05.png

 

 

Click on Build Now to start the build:

 

p3pipeline06.png

 

 

The job starts by checking out the source code to next restore and build our solution as defined in the Jenkinsfile02.

 

p3pipeline07.png

 

 

After the build is done, the build icon will be green and the msix artifact will be available:

 

p3pipeline08.png

 

 

The next post demonstrate how to package an application that uses the VB6VirtualRegistry tool to package the application to MSIX. Despite the name, the tool can be used to package any Desktop application.

 

 

Co-Authors
Version history
Last update:
‎Feb 14 2022 06:41 AM
Updated by: