Publish Visual Studio for Mac extensions using Github Actions

In my previous article on Build Visual Studio for Mac Extensions using Github Actions, I went through how to build a Visual Studio for Mac extension using Github Actions. In this article, I would like to go into further detail of the same theme but now focusing on how to publish a Visual Studio for Mac extension to a private extension repository hosted in Github, using Github Actions

Create extensions repository

First thing we need to is to create a new Github repository to publish our builds to. You can do this by either clicking on the New button from your Github Profile page under Repositories

or by using the Github CLI

$ gh repo create my-vsmac-extension-repo --public

We need this to be a Public repository as we will need to access the raw contents of our repository and add it to Visual Studio for Mac

Now that we have an initial repo, we need to create a branch to commit to. You can’t push commits to an empty repo

echo "# delete-me" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin git@github.com:christianhelle/my-vsmac-extension-repo.git
git push -u origin main

Now that we have a repository, we need to Create a personal access token

and give it access to push commits to our new repository by giving the PAT the public_repo scope

Copy the PAT to the clipboard as we will use this PAT

Store the PAT as a Github Actions Secret in the repository that builds the Visual Studio for Mac extension. From the Settings page of that repo, unfold Secrets and variables, select Actions, then click on New Repository Secret.

Give the Secret a name, in this case ACTIONS_GITHUB_TOKEN and paste the PAT that we just created

Build and Publish .mpack file

The next thing we need to is to update our workflow to publish the .mpack file to the new repo we created. We do this by adding a new job to our existing workflow:

  deploy:

    runs-on: ubuntu-latest
    timeout-minutes: 10
    needs: build
    if: github.ref == 'refs/heads/main'

    steps:
    - uses: actions/checkout@v3
      with:
        repository: christianhelle/my-vsmac-extension-repo
        ref: 'main'
        token:  ${{ secrets.ACTIONS_GITHUB_TOKEN }}

    - uses: actions/download-artifact@v3
      with:
        path: artifacts

    - name: Remove version number from filename
      run: |
        mv artifacts/Extension/Sample-${{ env.VERSION }}.mpack Sample.mpack
        rm -rf artifacts
    
    - name: Git Commit Build Artifacts      
      run: |
        git config --global user.name "Continuous Integration"
        git config --global user.email "username@users.noreply.github.com"
        git add Sample.mpack
        git commit -m "Update .mpack file to version ${{ env.VERSION }}"
        git push

This new job will checkout our new Visual Studio for Mac extension repository, which in this example I called my-vsmac-extension-repo. We set the token parameter because we will be committing back to this repository. Then it downloads the build artifacts from the previous build job to a folder called artifacts. You can only provide one version of your extension at a time in a Visual Studio for Mac extension repository so we need to strip the version number out of the filename. Lastly, add and commit the .mpack file to the extensions repository

Here’s the full contents of our workflow:

name: Build

on:
  workflow_dispatch:
  push:

env:
  VERSION: 1.0.${{ github.run_number }}

jobs:

  build:

    runs-on: macos-latest
    timeout-minutes: 10

    steps:
    - uses: actions/checkout@v3

    - name: Update Extension Version Info
      run: |
        sed -i -e 's/1.0/${{ env.VERSION }}/g' ./AddinInfo.cs
        cat ./AddinInfo.cs
      working-directory: src

    - name: Restore
      run: dotnet restore
      working-directory: src

    - name: Build
      run: /Applications/Visual\ Studio.app/Contents/MacOS/vstool build --configuration:Release $PWD/Sample.csproj
      working-directory: src

    - name: Pack
      run: /Applications/Visual\ Studio.app/Contents/MacOS/vstool setup pack $PWD/src/bin/Release/net7.0/Sample.dll -d:$PWD

    - name: Archive binaries
      run: zip -r Binaries.zip src/bin/Release/net7.0/

    - name: Publish binaries
      uses: actions/upload-artifact@v2
      with:
        name: Binaries
        path: Binaries.zip

    - name: Rename build output
      run: mv *.mpack Sample-${{ env.VERSION }}.mpack

    - name: Publish artifacts
      uses: actions/upload-artifact@v2
      with:
        name: Extension
        path: Sample-${{ env.VERSION }}.mpack

  deploy:

    runs-on: ubuntu-latest
    timeout-minutes: 10
    needs: build
    if: github.ref == 'refs/heads/main'

    steps:
    - uses: actions/checkout@v3
      with:
        repository: christianhelle/my-vsmac-extension-repo
        ref: 'main'
        token:  ${{ secrets.ACTIONS_GITHUB_TOKEN }}

    - uses: actions/download-artifact@v3
      with:
        path: artifacts

    - name: Remove version number from filename
      run: |
        mv artifacts/Extension/Sample-${{ env.VERSION }}.mpack Sample.mpack
        rm -rf artifacts
    
    - name: Git Commit Build Artifacts      
      run: |
        git config --global user.name "Continuous Integration"
        git config --global user.email "username@users.noreply.github.com"
        git add Sample.mpack
        git commit -m "Update .mpack file to version ${{ env.VERSION }}"
        git push

A successful run of this build should look like something like this:

Take a look at the extensions repo to make sure that the .mpack file got pushed correctly

Build the .mrep files

Now that we have .mpack files getting pushed into the extensions repository, we can setup a Github workflow that creates the Visual Studio for Mac .mrep files upon every push. The .mrep file is an XML file that contains meta data about the .mpack files in the extensions repository

Before we can grant Github Actions Read and Write permissions to its own repository. To do this, in the extensions repository Settings, unfold Actions and select General, enable Read and Write permissions under Workflow Permissions

Now we create a workflow:


name: Build

on:
  workflow_dispatch:
  push:
    paths-ignore:
      - '**/*'
      - '!**/*.mpack'
      - '!.github/workflows/build.yml'

jobs:
  build:

    runs-on: macos-latest
    timeout-minutes: 10

    env: 
      CI_COMMIT_MESSAGE: Continuous Integration Build Artifacts
      CI_COMMIT_AUTHOR: Continuous Integration

    steps:
    - uses: actions/checkout@v3

    - name: stable - vstool setup rep-build
      run: /Applications/Visual\ Studio.app/Contents/MacOS/vstool setup rep-build $PWD

    - name: Publish Stable Repo
      uses: actions/upload-artifact@v2
      with:
        name: Stable
        path: |
          *.mrep
          index.html
    
    - name: Git Commit Build Artifacts
      run: |
        git config --global user.name "${{ env.CI_COMMIT_AUTHOR }}"
        git config --global user.email "username@users.noreply.github.com"
        git add .
        git commit -m "${{ env.CI_COMMIT_MESSAGE }}"
        git push

We can either manually run the workflow above, or we can just trigger it by running the build workflow on the Visual Studio for Mac extension sample.

Once the workflow above has ran, the extensions repository should look something like this:

Thw workflow above ran vstool setup rep-build $PWD and committed the output files to its own repo. The command vstool setup rep-build $PWD produces 3 files, index.html, main.mrep, and root.mrep

The root.mrep file describes the files in th repository and looks something like this:

<?xml version="1.0" encoding="utf-8"?>
<Repository xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="https://www.w3.org/2001/XMLSchema">
  <Addin>
    <Url>Sample.mpack</Url>
    <Addin>
      <Id>Sample</Id>
      <Namespace>Sample</Namespace>
      <Name>My First Extension</Name>
      <Version>0.1.17</Version>
      <BaseVersion />
      <Author>Christian Resma Helle</Author>
      <Copyright />
      <Url />
      <Description>My first Visual Studio for Mac extension</Description>
      <Category>IDE extensions</Category>
      <Dependencies />
      <OptionalDependencies />
      <Properties>
        <Property name="DownloadSize">3188</Property>
      </Properties>
    </Addin>
  </Addin>
</Repository>

The main.mrep files points to the root.mrep file. You will be adding the direct link to the main.mrep file from Visual Studio for Mac

<?xml version="1.0" encoding="utf-8"?>
<Repository xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="https://www.w3.org/2001/XMLSchema">
  <Repository>
    <Url>root.mrep</Url>
    <LastModified>2023-03-20T15:31:42.3222075+00:00</LastModified>
  </Repository>
</Repository>

And lastly, index.html is a naive attempt to create a HTML page describing the available extensions

<html><body>
<h1>Add-in Repository</h1>
<p>This is a list of add-ins available in this repository.</p>
<table border=1><thead><tr><th>Add-in</th><th>Version</th><th>Description</th></tr></thead>
<tr><td>My First Extension</td><td>0.1.17</td><td>My first Visual Studio for Mac extension</td></tr>
</table>
</body></html>

Add extension repository to Visual Studio

Now all we need is to add a new custom extension repository to Visual Studio for Mac so we can download the extensions all from the IDE, and also see when there are updates coming in. To do this, we need to get the URL to the Raw main.mrep file. We do this by opening main.mrep from Github and getting the URL from the Raw link

This should be something like https://raw.githubusercontent.com/christianhelle/my-vsmac-extension-repo/main/main.mrep. We need to add this as an Extension Source to Visual Studio for Mac. This is done from Preferences, then scroll down on the side menu to Extensions, select Sources, then click on Add, then paste the Raw link to main.mrep

After this we should be able to see our extension from the Visual Studio for Mac Extensions screen

I hope you found this useful and get inspired to start building extensions of your own. If you’re interested in the full source code then you can checkout the Example VSMac extension project and the Visual Studio for Mac extension repository

Comments


Build Visual Studio for Mac Extensions using Github Actions

In my previous article on Extending Visual Studio for Mac 2022, I went through a step-by-step walkthrough to build a simple Visual Studio for Mac extension that does adds a menu item under the Edit menu, which when clicked, will write // Hello at the current cursor position of the currently active document.

Visual Studio for Mac extensions can be created using SDK Style .NET projects use .NET 7.0 as the target framework. The first requirement for setting up a build pipeline is being able to build the project from the command line. Using the example from the the previous article, let’s build the Sample project using a Github Actions workflow

In case you are new to Github Actions, you should create the folder /.github/workflows in your git repo, then create a workflow file under the folder we just created.

For this example, let’s create a file called build.yml.

Here’s the contents of our workflow:


name: Build

on:
  workflow_dispatch:
  push:

env:
  VERSION: 1.0.${{ github.run_number }}

jobs:

  build:

    runs-on: macos-latest
    timeout-minutes: 10

    steps:
    - uses: actions/checkout@v3

    - name: Update Extension Version Info
      run: |
        sed -i -e 's/1.0/${{ env.VERSION }}/g' ./AddinInfo.cs
        cat ./AddinInfo.cs
      working-directory: src

    - name: Restore
      run: dotnet restore
      working-directory: src

    - name: Build
      run: /Applications/Visual\ Studio.app/Contents/MacOS/vstool build --configuration:Release $PWD/Sample.csproj
      working-directory: src

    - name: Pack
      run: /Applications/Visual\ Studio.app/Contents/MacOS/vstool setup pack $PWD/src/bin/Release/net7.0/Sample.dll -d:$PWD

    - name: Archive binaries
      run: zip -r Binaries.zip src/bin/Release/net7.0/

    - name: Publish binaries
      uses: actions/upload-artifact@v2
      with:
        name: Binaries
        path: Binaries.zip

    - name: Rename build output
      run: mv *.mpack Sample-${{ env.VERSION }}.mpack

    - name: Publish artifacts
      uses: actions/upload-artifact@v2
      with:
        name: Extension
        path: Sample-${{ env.VERSION }}.mpack

Let’s break the workflow job steps down into detail…

Step 1) Checkout the branch.

Pretty a much a standard first for most Github Action workflows

Step 2) Update the version info of the extension.

For this naive example let’s just call it version 1.0.xxx where xxx is the workflow run number. We get the Github Action workflow run number from ${{ github.run_number }}. Let’s store this in an environment variable called VERSION. We then want to update the version number in the AddinInfo.cs file

To do this, we use the the sed (stream editor) command to replace all instances of 1.0 with 1.0.xxx.

We do this by running:

$ sed -i -e 's/1.0/$/g' ./AddinInfo.cs

The contents of AddinInfo.cs may look something like this:

using Mono.Addins;
using Mono.Addins.Description;

[assembly: Addin(Id = "Sample", Namespace = "Sample", Version = "1.0")]
[assembly: AddinName("My First Extension")]
[assembly: AddinCategory("IDE extensions")]
[assembly: AddinDescription("My first Visual Studio for Mac extension")]
[assembly: AddinAuthor("Christian Resma Helle")]

Step 3) Restore package reference

This simple step is where the workflow should basically just run dotnet restore from the folder that contains the solution file

Step 4) Build the extension

We want to build the project in Release configuration using the CLI. Normally we can do this using dotnet build -c Release which works fine if you have previously built a Visual Studio for mac extension on the machine you’re working on but if you are building it in a new machine that has previously never build a Visual Studio for Mac extension then you most likely will need to run the Visual Studio Tool Runner a.k.a. vstool but because we are probably running from a short lived virtual machine, we can’t assume that running dotnet build -c Release will work on the first try.

Instead of running dotnet build we should instead do something like run the Visual Studio Tool Runner build command

$ /Applications/Visual\ Studio.app/Contents/MacOS/vstool build --configuration:Release $PWD/Sample.csproj

You need to specify the absolute path to the project file, but you can simplify this by getting the present working directory from the PWD command

Step 5) Package the extension

Now that the extension is built, we now need to package it to be able to distribute it. To create a MonoDevelop package file .mpack you need to run the Visual Studio Extension Setup Utility pack command

$ /Applications/Visual\ Studio.app/Contents/MacOS/vstool setup pack [absolute path to main output DLL] -d:[absolute path to output folder]

A little tip for getting the absolute path is to use $PWD. So if you created your project under the ~/projects/my-extension folder and this is currently your working directory then you can do something like:

$ /Applications/Visual\ Studio.app/Contents/MacOS/vstool setup pack $PWD/Sample.dll -d:$PWD

Sample.dll is the build output of the project we just built

The command above will produce the output ~/projects/my-extension/Sample.mpack

Step 5) Package the binaries

Archive the binary files so we can use them as build artifiacts

$ zip -r Binaries.zip src/bin/Release/net7.0/

Step 6) Publish binaries as build artifacts

In this step we will publish the newly created Binaries.zip as a build artifact

Step 7) Rename built output

We do this so to help users who might have download multiple verssions of the extension be able to keep older versions of the extension. This is completely optional, but is something I find to be a good practice

Uploading artifacts uses the actions/upload-artifact@v2 task

Step 8) Publish .mpack file as build artifacts

This is pretty straight forward. You start off by using the actions/upload-artifact@v2 task. This workflow allows the developer to the filename of the artifact, and also which folder to publish as build artifacts

Build Output

If everything succeeds then we should be able to see the results of the build in Github Actions. At the bottom of the content section for every build should have 2 artifacts, the binary files and the .mpack file itself.

There are 2 artifacts in the build called Binaries and Extensions. They are packed as zip files when downloaded

I hope you found this useful and get inspired to start building extensions of your own. If you’re interested in the full source code then you can grab it here

Comments


Extending Visual Studio for Mac 2022

This is step by step walkthrough guide to getting started with developing extensions for Visual Studio for Mac 2022 with explanations, code examples, and a couple of links to offical documentation

The extensibility story for Visual Studio for Mac was almost non-existent for a while, and the documentation for getting started was really outdated. Visual Studio for Mac was originally a re-branding of Xamarin Studio, which was built over MonoDevelop and the extensibility SDK’s we used for the longest time was all from the old MonoDevelop Addin libraries. The original getting started guide from MonoDevelop is still somewhat correct, but the libraries referred to in the guide will no longer build.

For Visual Studio for Mac 2022 this has changed and now we can create Visual Studio for Mac extensions using the Microsoft.VisualStudioMac.Sdk library that we can install from nuget.org. To make things even better, we can now break free of our old .NET Framework 4.x shackles and start targetting .NET 7.0. and all it’s goodness

As of the time I’m writing this, there is still no File -> New -> Extension Project experience, but it’s not hard to get started either.

Walkthrough

In this walkthrough, we will build a simple Visual Studio for Mac extension that adds the Insert Text menu item to the Edit menu. All this can do is to insert the text // Hello to the active document from the current cursor position

Step 1 - Create New Project

Let’s start with creating a new project called Sample.csproj

Here’s how a csproj file for an empty Visual Studio for Mac extension project looks like:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.VisualStudioMac.Sdk" Version="17.0.0" />
  </ItemGroup>
</Project>

Step 2 - Addin info

A Visual Studio for Mac extension has metadata about its name, version, dependencies, etc. It also defines any number of extensions that plug into extension points defined by other extensions, and can also define extension points that other extensions can extend.

Let’s define some AddIn information in a file called AddinInfo.cs

using Mono.Addins;
using Mono.Addins.Description;

[assembly: Addin(Id = "Sample", Namespace = "Sample", Version = "1.0")]
[assembly: AddinName("My First Extension")]
[assembly: AddinCategory("IDE extensions")]
[assembly: AddinDescription("My first Visual Studio for Mac extension")]
[assembly: AddinAuthor("Christian Resma Helle")]

The combined Id and Namespace from Addin should be unique among all Visual Studio for Mac extensions. The other attributes are self-explanatory

Step 3 - Addin Manifest

Now that the Addin is defined, we can add some extensions.

We do this by defining the Manifest.addin.xml file

<?xml version="1.0" encoding="UTF-8"?>
<ExtensionModel>
    <Extension path = "/MonoDevelop/Ide/Commands/Edit">
        <Command id = "Sample.SampleCommands.InsertText"
            _label = "Insert Text"
            defaultHandler = "Sample.InsertTextHandler" />
    </Extension>

    <Extension path = "/MonoDevelop/Ide/MainMenu/Edit">
        <CommandItem id="Sample.SampleCommands.InsertText" />
    </Extension>
</ExtensionModel>

This extension defines a command for the command system. The Command ID should correspond to an enum value. The _label attribute is the display name of the command. The defaultHandler attribute is the full type name of the CommandHandler implementation that will execute when the extension executes

The Command System provides ways to control the availability, visibility and handling of commands depending on context.

Commands can be bound to keyboard shortcuts and can be inserted into menus. In this exaple, we are going to insert the InsertText command into the main Edit menu with another extension.

Step 4 - Implement the CommandHandler

Now that the InsertText command is registered, we need to implement a command handler. The simplest way to use it is with a default handler, which is a class that implements MonoDevelop.Components.Commands.CommandHandler. Let’s implement CommandHandler as InsertTextHandler to be only available when an active document is open

We will also need to create the SampleCommands enum

using MonoDevelop.Components.Commands;
using MonoDevelop.Ide;
using MonoDevelop.Ide.Gui;
using System;

namespace Sample
{
    public class InsertTextHandler : CommandHandler
    {
        protected override void Run()
        {
            var textBuffer = IdeApp.Workbench.ActiveDocument.GetContent<ITextBuffer>();
            var textView = IdeApp.Workbench.ActiveDocument.GetContent<ITextView>();
            textBuffer.Insert(textView.Caret.Position.BufferPosition.Position, "// Hello");
        }

        protected override void Update(CommandInfo info)
        {
            var textBuffer = IdeApp.Workbench.ActiveDocument.GetContent<ITextBuffer>();
            if (textBuffer != null && textBuffer.AsTextContainer() is SourceTextContainer container)
            {
                var document = container.GetTextBuffer();
                if (document != null)
                {
                    info.Enabled = true;
                }
           }
        }
    }

    public enum SampleCommands
    {
        InsertText,
    }
}

Step 5 - Package the extension

This can be done by right clicking on the extension project from Visual Studio for Mac then selecting Pack from the context menu

You can also do it from the command line. With the new SDK, Microsoft.VisualStudioMac.Sdk, you can build the project from the command line simply by using dotnet build. Running dotnet build will ONLY build the project, it will not create the distributable .mpack package.

Let’s start with building the project in Release configuration

$ dotnet build -c Release Sample.csproj

This will produce the bin/Release/net7.0/Sample.dll file

To create the .mpack package from the command line, we need to use the Visual Studio Tool Runner a.k.a. vstool. The Visual Studio Tool Runner is included in the Visual Studio for Mac installation. The Visual Studio Tool Runner is available from the following path

$ /Applications/Visual\ Studio.app/Contents/MacOS/vstool

We need to run the Visual Studio Extension Setup Utility pack command

$ /Applications/Visual\ Studio.app/Contents/MacOS/vstool setup pack [absolute path to main output DLL] -d:[absolute path to output folder]

A little tip for getting the absolute path is to use $PWD. So if you created your project under the ~/projects/my-extension folder and this is currently your working directory then you can do something like

$ /Applications/Visual\ Studio.app/Contents/MacOS/vstool setup pack $PWD/Sample.dll -d:$PWD

The command above will produce the output ~/projects/my-extension/Sample.mpack

Step 6 - Test the extension

Debugging a Visual Studio for Mac is possible, but doesn’t come out of the box. To enable Debugging the extension from Visual Studio for Mac we need to add the following to our C# project

<PropertyGroup Condition=" '$(RunConfiguration)' == 'Default' ">
  <StartAction>Program</StartAction>
  <StartProgram>\Applications\Visual Studio.app\Contents\MacOS\VisualStudio</StartProgram>
  <StartArguments>--no-redirect</StartArguments>
  <ExternalConsole>true</ExternalConsole>
</PropertyGroup>

Now our Sample project should look something like this:

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net7.0</TargetFramework>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(RunConfiguration)' == 'Default' ">
    <StartAction>Program</StartAction>
    <StartProgram>\Applications\Visual Studio.app\Contents\MacOS\VisualStudio</StartProgram>
    <StartArguments>--no-redirect</StartArguments>
    <ExternalConsole>true</ExternalConsole>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.VisualStudioMac.Sdk" Version="17.0.0" />
  </ItemGroup>
</Project>

Debugging the extension will basically start another instance of Visual Studio for Mac where you can test your extension

Try it out and if all goes well the Edit menu should have the Insert Text item at the bottom

Step 7 - Install extension

If you followed Step 5, then you should already have a .mpack at hand. To install a Visual Studio for Mac extension, you need to follow these steps:

You need to restart Visual Studio for Mac at this point before you can see our new extension under the Edit menu

I hope you found this useful and get inspired to start building extensions of your own. If you’re interested in the full source code then you can grab it here

Comments