TypeScript & Azure DevOps – ToolRunner

typescript

As you may notice, I’m a huge fan of TypeScript and I’m really proud to be an active developer of the Paris community. I love TypeScript because this langage is awesome and it works great with Node.js. Most of the time, I teach TypeScript to other developers and I always try to do my best to impart my passion for this langage. On another hand, as a Microsft PFE, I don’t have a lot of time to build softwares… That’s why I spend some of my personal time to build open source projects.

TypeScript can be used with many SDK to create usefull stuff for the community. Good examples are Visual Studio Code and Azure DevOps extensions. Both are my favorites Microsoft’s tools. I want to learn Rust this summer, unfortunately Azure DevOps doesn’t support this langage, so I decide to build a dedicated extension (available here). It’s also a good opportunity to talk about the Azure DevOps task library and the ToolRunner!

Introduction

Let’s start this post with a little bit of context. Rust is a programming language being supported by Mozilla since 2010. Rust compiler uses LLVM as its back end. In Azure DevOps, Rust is not a first class citizen, to ensure it can compile a project it needs the Rust tools. This step was one of my first use-cases for the extension!

 

which

Rustup must be in the PATH to install Rust tools. Azure DevOps task library allows us to check if a tool is available in the agent PATH with the which function. It requires the name of a tool and returns the corresponding path. The function also accepts an optional boolean parameter: check. When true, if the tool is not in the PATH, which function will throw an error. By default it returns an empty string. To know if Rustup is available, just check if the which function returns an empty string or not:

const isRustupAvailable = !!which("rustup");

 

exec & pipe

Thanks to the which function, the task can check for Rustup availability. If so, an update can be made to install latest version of Rust. If not, the task need to retreive the Rustup shell script on the web and execute it.

For the first scenario it’s really simple, because the task only needs to call Rustup update subcommand. Azure DevOps task library defines an exec function for that. It’s a wrapper over ToolRunner that executes a tool. When using exec function, you have to pass the name of the tool you want to run and arguments (A string or an array of string). This function return a Promise, so you can await it and get back the code returned by the tool:

const returnCode = await exec("rustup", "update");

If Rustup isn’t available, you have to use cURL to get the shell script and pipe the response to the Shell. For this case you can’t use the exec function directly, it will fail because of the pipe operation. To deal with that, you need to prepare cURL and sh with all arguments:

const curl = tool(which("curl"))
               .arg("https://sh.rustup.rs")
               .arg("-sSf");
const sh = tool(which("sh"))
               .arg("-s")
               .arg("--")
               .arg("-y");

When tools are ready, you can pipe the output of cURL to sh:

const pipe = curl.pipeExecOutputToTool(sh);

The pipeExecOutputToTool return a ToolRunner instance, so you can execute it asynchronously:

const returnCode = await pipe.exec();

That’s it! As you can see it’s really straightforward. You can take a look on my implementation here.

 

OS

At the begin, Azure DevOps was completely dedicated to Windows development, but Microsoft is changing and we have now Linux and macOS agents. As you saw, I used cURL and Shell in my development and may be you are asking, what about Windows? It doesn’t have those tools. That’s true, by default cURL and Shell aren’t available on Windows, but it not means you can’t use it. Azure DevOps team did a great job on that. By using the which function, on Windows agent, it returns those specifics tools for Windows. You can found the information inside the logs:

  • Hosted
  • "C:\Program Files\Git\usr\bin\curl.exe" https://sh.rustup.rs -sSf | "C:\Program Files\Git\bin\sh.exe" -s -- -y
  • Hosted 2017
  • "C:\Program Files\Git\mingw64\bin\curl.exe" https://sh.rustup.rs -sSf | "C:\Program Files\Git\bin\sh.exe" -s -- -y 

However it’s not magic and it has a cost. cURL and Shell are faster on Linux and macOS actually.

 

PATH

As I said previously, the which function returns a PATH corresponding to a tool. For my extension, the install task automatically adds Cargo folder to the path on debug (thanks to Rustup). But it’s completely different on a hosted agent, the PATH isn’t updated after the installation. To solve that, Cargo path needs to be added in the PATH programmatically. Rustup install Rust inside the Cargo bin folder in the home directory. It’s easy to resolve this path:

const cargoPath = join(homedir(), ".cargo", "bin");

Now, I check if PATH is defined. If not, I just set it with the Cargo path:

if (process.env.PATH) {
    // TODO
} else {
    process.env.PATH = cargoPath;
}

Otherwise, if Cargo isn’t in the PATH, I just concatenate it at the beginning of the PATH (it ensures that the PATH will not have any delimiter problems):

if (process.env.PATH.indexOf(cargoPath) === -1) {
    process.env.PATH = `${cargoPath}${delimiter}${process.env.PATH}`;
}

Job’s done, Cargo path can now be retrieve by the which function after the installation with Rustup!

 

Debug

Azure DevOps tasks aren’t really easy to debug because the agent environment will be completely different from your local one. That’s why a mock runner is available inside the library. It can be created with a new instance of the TaskMockRunner class. It requires a file path that will be considered by the mock runner as a task:

const taskMockRunner = new TaskMockRunner(taskPath);

Of course in debug mode no build definition is available to retrieve the task configuration. TaskMockRunner class define a setInput method to set the task configuration:

taskMockRunner.setInput("installNightly", "true");

You can use this method as much as you need. When TaskMockRunner instance is ready, you can launch the task with the run method:

taskMockRunner.run();

Finally, don’t forget to switch the sourceMap property in tsconfig.json to true.

 

Conclusion

The Rust extension I built is all about about tool execution with Azure DevOps task library. You can find the source code here.

This package is great but sometime there are lacks of documentation, that’s why I want to close this post with an advice: If you can’t find how to deal with a technical aspect, just take a look on samples and unit tests in the Azure DevOps repository, it helps a lot.

CYA

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s