CINXE.COM

Testing Packages on Linux ARM64 with GitHub Actions – Bioconductor community blog

<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en"><head> <meta charset="utf-8"> <meta name="generator" content="quarto-1.6.40"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes"> <meta name="author" content="Mike L. Smith"> <meta name="dcterms.date" content="2023-07-14"> <meta name="description" content="How to use GitHub Actions to systematically build and test a Bioconductor package on Linux ARM64 architecture."> <title>Testing Packages on Linux ARM64 with GitHub Actions – Bioconductor community blog</title> <style> code{white-space: pre-wrap;} span.smallcaps{font-variant: small-caps;} div.columns{display: flex; gap: min(4vw, 1.5em);} div.column{flex: auto; overflow-x: auto;} div.hanging-indent{margin-left: 1.5em; text-indent: -1.5em;} ul.task-list{list-style: none;} ul.task-list li input[type="checkbox"] { width: 0.8em; margin: 0 0.8em 0.2em -1em; /* quarto-specific, see https://github.com/quarto-dev/quarto-cli/issues/4556 */ vertical-align: middle; } /* CSS for syntax highlighting */ pre > code.sourceCode { white-space: pre; position: relative; } pre > code.sourceCode > span { line-height: 1.25; } pre > code.sourceCode > span:empty { height: 1.2em; } .sourceCode { overflow: visible; } code.sourceCode > span { color: inherit; text-decoration: inherit; } div.sourceCode { margin: 1em 0; } pre.sourceCode { margin: 0; } @media screen { div.sourceCode { overflow: auto; } } @media print { pre > code.sourceCode { white-space: pre-wrap; } pre > code.sourceCode > span { display: inline-block; text-indent: -5em; padding-left: 5em; } } pre.numberSource code { counter-reset: source-line 0; } pre.numberSource code > span { position: relative; left: -4em; counter-increment: source-line; } pre.numberSource code > span > a:first-child::before { content: counter(source-line); position: relative; left: -1em; text-align: right; vertical-align: baseline; border: none; display: inline-block; -webkit-touch-callout: none; -webkit-user-select: none; -khtml-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; padding: 0 4px; width: 4em; } pre.numberSource { margin-left: 3em; padding-left: 4px; } div.sourceCode { } @media screen { pre > code.sourceCode > span > a:first-child::before { text-decoration: underline; } } </style> <script src="../../site_libs/quarto-nav/quarto-nav.js"></script> <script src="../../site_libs/quarto-nav/headroom.min.js"></script> <script src="../../site_libs/clipboard/clipboard.min.js"></script> <script src="../../site_libs/quarto-search/autocomplete.umd.js"></script> <script src="../../site_libs/quarto-search/fuse.min.js"></script> <script src="../../site_libs/quarto-search/quarto-search.js"></script> <meta name="quarto:offset" content="../../"> <script src="../../site_libs/quarto-html/quarto.js"></script> <script src="../../site_libs/quarto-html/popper.min.js"></script> <script src="../../site_libs/quarto-html/tippy.umd.min.js"></script> <script src="../../site_libs/quarto-html/anchor.min.js"></script> <link href="../../site_libs/quarto-html/tippy.css" rel="stylesheet"> <link href="../../site_libs/quarto-html/quarto-syntax-highlighting-549806ee2085284f45b00abea8c6df48.css" rel="stylesheet" id="quarto-text-highlighting-styles"> <script src="../../site_libs/bootstrap/bootstrap.min.js"></script> <link href="../../site_libs/bootstrap/bootstrap-icons.css" rel="stylesheet"> <link href="../../site_libs/bootstrap/bootstrap-9c546a04fa00a275547e7eb78afa1c09.min.css" rel="stylesheet" append-hash="true" id="quarto-bootstrap" data-mode="light"> <script id="quarto-search-options" type="application/json">{ "location": "navbar", "copy-button": false, "collapse-after": 3, "panel-placement": "end", "type": "overlay", "limit": 50, "keyboard-shortcut": [ "f", "/", "s" ], "show-item-context": false, "language": { "search-no-results-text": "No results", "search-matching-documents-text": "matching documents", "search-copy-link-title": "Copy link to search", "search-hide-matches-text": "Hide additional matches", "search-more-match-text": "more match in this document", "search-more-matches-text": "more matches in this document", "search-clear-button-title": "Clear", "search-text-placeholder": "", "search-detached-cancel-button-title": "Cancel", "search-submit-button-title": "Submit", "search-label": "Search" } }</script> <script defer="" data-domain="bioconductor.github.io/biocblog" src="https://plausible.io/js/script.js"></script> <link rel="stylesheet" href="../../styles.css"> <meta property="og:title" content="Testing Packages on Linux ARM64 with GitHub Actions – Bioconductor community blog"> <meta property="og:description" content="How to use GitHub Actions to systematically build and test a Bioconductor package on Linux ARM64 architecture."> <meta property="og:image" content="https://blog.bioconductor.org/posts/2023-07-14-linux-arm64-github-actions/data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo1N0NEMjA4MDI1MjA2ODExOTk0QzkzNTEzRjZEQTg1NyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozM0NDOEJGNEZGNTcxMUUxODdBOEVCODg2RjdCQ0QwOSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozM0NDOEJGM0ZGNTcxMUUxODdBOEVCODg2RjdCQ0QwOSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IE1hY2ludG9zaCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkZDN0YxMTc0MDcyMDY4MTE5NUZFRDc5MUM2MUUwNEREIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjU3Q0QyMDgwMjUyMDY4MTE5OTRDOTM1MTNGNkRBODU3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+84NovQAAAR1JREFUeNpiZEADy85ZJgCpeCB2QJM6AMQLo4yOL0AWZETSqACk1gOxAQN+cAGIA4EGPQBxmJA0nwdpjjQ8xqArmczw5tMHXAaALDgP1QMxAGqzAAPxQACqh4ER6uf5MBlkm0X4EGayMfMw/Pr7Bd2gRBZogMFBrv01hisv5jLsv9nLAPIOMnjy8RDDyYctyAbFM2EJbRQw+aAWw/LzVgx7b+cwCHKqMhjJFCBLOzAR6+lXX84xnHjYyqAo5IUizkRCwIENQQckGSDGY4TVgAPEaraQr2a4/24bSuoExcJCfAEJihXkWDj3ZAKy9EJGaEo8T0QSxkjSwORsCAuDQCD+QILmD1A9kECEZgxDaEZhICIzGcIyEyOl2RkgwAAhkmC+eAm0TAAAAABJRU5ErkJggg=="> <meta property="og:site_name" content="Bioconductor community blog"> </head> <body class="nav-fixed fullcontent"> <div id="quarto-search-results"></div> <header id="quarto-header" class="headroom fixed-top quarto-banner"> <nav class="navbar navbar-expand-lg " data-bs-theme="dark"> <div class="navbar-container container-fluid"> <div class="navbar-brand-container mx-auto"> <a class="navbar-brand" href="../../index.html"> <span class="navbar-title">Bioconductor community blog</span> </a> </div> <div id="quarto-search" class="" title="Search"></div> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" role="menu" aria-expanded="false" aria-label="Toggle navigation" onclick="if (window.quartoToggleHeadroom) { window.quartoToggleHeadroom(); }"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul class="navbar-nav navbar-nav-scroll ms-auto"> <li class="nav-item"> <a class="nav-link" href="../../about.html"> <span class="menu-text">About</span></a> </li> <li class="nav-item"> <a class="nav-link" href="../../contributing.html"> <span class="menu-text">Contributing</span></a> </li> <li class="nav-item compact"> <a class="nav-link" href="https://github.com/bioconductor/biocblog"> <i class="bi bi-github" role="img"> </i> <span class="menu-text"></span></a> </li> <li class="nav-item compact"> <a class="nav-link" href="https://genomic.social/@bioconductor"> <i class="bi bi-mastodon" role="img"> </i> <span class="menu-text"></span></a> </li> <li class="nav-item compact"> <a class="nav-link" href="https://www.linkedin.com/company/bioconductor/"> <i class="bi bi-linkedin" role="img"> </i> <span class="menu-text"></span></a> </li> <li class="nav-item compact"> <a class="nav-link" href="https://www.youtube.com/user/bioconductor"> <i class="bi bi-youtube" role="img"> </i> <span class="menu-text"></span></a> </li> <li class="nav-item compact"> <a class="nav-link" href="https://slack.bioconductor.org/"> <i class="bi bi-slack" role="img"> </i> <span class="menu-text"></span></a> </li> <li class="nav-item compact"> <a class="nav-link" href="../../index.xml"> <i class="bi bi-rss" role="img"> </i> <span class="menu-text"></span></a> </li> </ul> </div> <!-- /navcollapse --> <div class="quarto-navbar-tools"> </div> </div> <!-- /container-fluid --> </nav> </header> <!-- content --> <header id="title-block-header" class="quarto-title-block default page-columns page-full"> <div class="quarto-title-banner page-columns page-full"> <div class="quarto-title column-body"> <h1 class="title">Testing Packages on Linux ARM64 with GitHub Actions</h1> <div> <div class="description"> <p>How to use GitHub Actions to systematically build and test a Bioconductor package on Linux ARM64 architecture.</p> </div> </div> <div class="quarto-categories"> <div class="quarto-category">tech-notes</div> <div class="quarto-category">GitHub Actions</div> <div class="quarto-category">ARM64</div> </div> </div> </div> <div class="quarto-title-meta"> <div> <div class="quarto-title-meta-heading">Author</div> <div class="quarto-title-meta-contents"> <p><a href="https://github.com/grimbough">Mike L. Smith</a> <a href="https://orcid.org/0000-0002-7800-3848" class="quarto-title-author-orcid"> <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA2ZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDo1N0NEMjA4MDI1MjA2ODExOTk0QzkzNTEzRjZEQTg1NyIgeG1wTU06RG9jdW1lbnRJRD0ieG1wLmRpZDozM0NDOEJGNEZGNTcxMUUxODdBOEVCODg2RjdCQ0QwOSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozM0NDOEJGM0ZGNTcxMUUxODdBOEVCODg2RjdCQ0QwOSIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgQ1M1IE1hY2ludG9zaCI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOkZDN0YxMTc0MDcyMDY4MTE5NUZFRDc5MUM2MUUwNEREIiBzdFJlZjpkb2N1bWVudElEPSJ4bXAuZGlkOjU3Q0QyMDgwMjUyMDY4MTE5OTRDOTM1MTNGNkRBODU3Ii8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+84NovQAAAR1JREFUeNpiZEADy85ZJgCpeCB2QJM6AMQLo4yOL0AWZETSqACk1gOxAQN+cAGIA4EGPQBxmJA0nwdpjjQ8xqArmczw5tMHXAaALDgP1QMxAGqzAAPxQACqh4ER6uf5MBlkm0X4EGayMfMw/Pr7Bd2gRBZogMFBrv01hisv5jLsv9nLAPIOMnjy8RDDyYctyAbFM2EJbRQw+aAWw/LzVgx7b+cwCHKqMhjJFCBLOzAR6+lXX84xnHjYyqAo5IUizkRCwIENQQckGSDGY4TVgAPEaraQr2a4/24bSuoExcJCfAEJihXkWDj3ZAKy9EJGaEo8T0QSxkjSwORsCAuDQCD+QILmD1A9kECEZgxDaEZhICIzGcIyEyOl2RkgwAAhkmC+eAm0TAAAAABJRU5ErkJggg=="></a></p> </div> </div> <div> <div class="quarto-title-meta-heading">Published</div> <div class="quarto-title-meta-contents"> <p class="date">July 14, 2023</p> </div> </div> </div> </header><div id="quarto-content" class="quarto-container page-columns page-rows-contents page-layout-article page-navbar"> <!-- sidebar --> <!-- margin-sidebar --> <!-- main --> <main class="content quarto-banner-title-block" id="quarto-document-content"> <div class="quarto-figure quarto-figure-center"> <figure class="figure"> <p><img src="../2023-06-09-debug-linux-arm64-on-docker/bioconductor_logo_cmyk.png" class="img-fluid figure-img"></p> <figcaption>Bioconductor on Linux ARM64</figcaption> </figure> </div> <section id="introduction" class="level1"> <h1>Introduction</h1> <p>The Bioconductor Build System (BBS) now includes routine package testing on Linux ARM64, but the relatively low frequency of testing means this, if a problem occurs with your package, it can take a while to identify and fix the issue using the build system alone.</p> <p>The previous blog post “<a href="https://bioconductor.github.io/biocblog/posts/2023-06-09-debug-linux-arm64-on-docker/">Emulated build and test of Bioconductor packages for Linux ARM64</a>” described how one can use Docker and architecture emulation to build, test, and debug a Bioconductor package for running on Linux ARM64 architecture when you only have access to local x86 hardware.</p> <p>However such manual testing can be frustrating to run as a package developer, either because it’s an extra task you have to run frequently, or because you only do it occasionally and forget the steps involved. Ideally such testing would happen automatically whenever you make changes to a package, but providing more rapid feedback than the BBS provides. In this article we build on these previously presented ideas to describe one approach for testing package on Linux ARM64 using a continuous integration environment on GitHub Actions.</p> </section> <section id="workflow-implementation" class="level1"> <h1>Workflow Implementation</h1> <p>An example workflow implementation can be found at <a href="https://github.com/grimbough/bioc-testing-with-arm64/blob/main/.github/workflows/test-package-arm64.yml">https://github.com/grimbough/bioc-testing-with-arm64/blob/main/.github/workflows/test-package-arm64.yml</a>. In the remainder of this post we’ll discuss some of the implementation choices made there and how they work.</p> <section id="choice-of-docker-container" class="level2"> <h2 class="anchored" data-anchor-id="choice-of-docker-container">Choice of Docker container</h2> <p>The first thing to remember when using architecture emulation is that everything works much slower than when running natively - typically by at least an order of magnitude. This influences some of the decisions made during this workflow regarding which containers to use and what we want to cache between workflow steps. Some operations that might be acceptable in a standard workflow become painfully slow under emulation, and so we try reduce the number of slow steps.</p> <p>The first of these is to use a modified version of the <a href="https://github.com/Bioconductor/bioconductor_docker/pkgs/container/bioconductor/102293490?tag=devel-amd64">Bioconductor:devel</a> docker image which has TinyTex pre-installed. This allows us to compile the package manual pages and and PDF vignettes during testing. Installing TinyTex and the required LaTeX packages takes approximately 10 minutes on our emulated system, so there is a noticeable time benefit to using an image with it already installed. The modified image can be found at <a href="https://ghcr.io/grimbough/bioc-with-tinytex:devel-arm64">ghcr.io/grimbough/bioc-with-tinytex:devel-arm64</a></p> <p><em>Note: You could probably achieve a similar result by using the standard Bioconductor container and running <code>R CMD check</code> with the arguments <code>--no-manual</code> and <code>--no-build-vignettes</code>, however I would rather run the complete testing process in case there is problematic code in either the manual page examples or vignette.</em></p> </section> <section id="installing-packages" class="level2"> <h2 class="anchored" data-anchor-id="installing-packages">Installing packages</h2> <p>Installing packages that require compilation is also incredibly slow on our emulated system, so it’s immediately desirable to cache the library of installed packages that are needed for testing. The <a href="https://github.com/actions/cache">actions/cache</a> GitHub action does a good job of this, however it will only create a cache after a successful job run. Given we’re creating this workflow to test a potentially problematic package, it can be frustrating to repeatedly wait several hours for all the necessary packages to install, because you haven’t managed to fix the issue.</p> <p>Given this, we can split our workflow into two jobs; the first installs the packages while the second runs the actual package tests. With this structure a failure in the second job doesn’t prevent the cache mechanism from working and makes repeated runs much faster.</p> <p>Now lets take a look at the first few steps on our <code>install-dependencies</code> job and explain what’s happening. Most of these steps are pretty standard for regular users of GitHub Actions.</p> <div class="sourceCode" id="cb1"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">install-dependencies</span><span class="kw">:</span></span> <span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Install package dependencies</span></span> <span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">runs-on</span><span class="kw">:</span><span class="at"> ubuntu-22.04</span></span> <span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a><span class="at"> </span></span> <span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">steps</span><span class="kw">:</span></span> <span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> checkout</span></span> <span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> actions/checkout@v3</span></span> <span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a><span class="at"> </span></span> <span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Set up QEMU</span></span> <span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> docker/setup-qemu-action@v2</span></span> <span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">with</span><span class="kw">:</span></span> <span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">platforms</span><span class="kw">:</span><span class="at"> arm64</span></span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> <p>First off we’re checking out the git repository the workflow is found in. That’s probably just the package you’re testing. Then we’re using the <a href="https://github.com/docker/setup-qemu-action">docker/setup-qemu-action</a> to install the QEMU emulator discussed in the <a href="https://bioconductor.github.io/biocblog/posts/2023-06-09-debug-linux-arm64-on-docker/">previous post</a>.</p> <p>Now we set up the library cache.</p> <div class="sourceCode" id="cb2"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb2-1"><a href="#cb2-1" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Make R library</span></span> <span id="cb2-2"><a href="#cb2-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">run</span><span class="kw">:</span><span class="at"> mkdir -p ${RUNNER_TEMP}/R-lib</span></span> <span id="cb2-3"><a href="#cb2-3" aria-hidden="true" tabindex="-1"></a></span> <span id="cb2-4"><a href="#cb2-4" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Cache Dependencies</span></span> <span id="cb2-5"><a href="#cb2-5" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">id</span><span class="kw">:</span><span class="at"> cache-deps</span></span> <span id="cb2-6"><a href="#cb2-6" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> actions/cache@v3</span></span> <span id="cb2-7"><a href="#cb2-7" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">with</span><span class="kw">:</span></span> <span id="cb2-8"><a href="#cb2-8" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">path</span><span class="kw">:</span><span class="at"> ${{ runner.temp }}/R-lib</span></span> <span id="cb2-9"><a href="#cb2-9" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">key</span><span class="kw">:</span><span class="at"> R_lib-ARM64-${{ hashFiles('**/DESCRIPTION') }}</span></span> <span id="cb2-10"><a href="#cb2-10" aria-hidden="true" tabindex="-1"></a><span class="fu"> restore-keys</span><span class="kw">: </span><span class="ch">|</span></span> <span id="cb2-11"><a href="#cb2-11" aria-hidden="true" tabindex="-1"></a> R_lib-ARM64-${{ hashFiles('**/DESCRIPTION') }}</span> <span id="cb2-12"><a href="#cb2-12" aria-hidden="true" tabindex="-1"></a> R_lib-ARM64-</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> <p>Initially we create an empty directory on our runner. In this example this is in the runners temporary directory, but it could be anywhere. We’ll later mount this location into our Docker container, and it will contain the installed package library. We have to create it outside of the Docker container and mount it so that the caching mechanism will work. If this location was created inside the Docker container, it would disappear when the container was destroyed, and we wouldn’t be able to retain the contents.</p> <p>We then provide this location to the <code>actions/cache</code> action, and use a hash of the <code>DESCRIPTION</code> file to tag our cache. Update the <code>DESCRIPTION</code> e.g.&nbsp;to add a new dependency or bump the version number and a new cache will be created. This isn’t perfect, as it won’t necessary capture updates to installed packages in the library, but it does a reasonable job with being too complex.</p> <p>The final step of the job is to install the dependencies.</p> <div class="sourceCode" id="cb3"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Run the build process with Docker</span></span> <span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> addnab/docker-run-action@v3</span></span> <span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">with</span><span class="kw">:</span></span> <span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">image</span><span class="kw">:</span><span class="at"> ghcr.io/grimbough/bioc-with-tinytex:devel-arm64</span></span> <span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="fu"> options</span><span class="kw">: </span><span class="ch">|</span></span> <span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a> --platform linux/arm64</span> <span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a> --volume ${{ runner.temp }}/R-lib:/R-lib</span> <span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a> --volume ${{ github.workspace }}/../:/build</span> <span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a> --env R_LIBS_USER=/R-lib</span> <span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a><span class="fu"> run</span><span class="kw">: </span><span class="ch">|</span></span> <span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a> echo "options(Ncpus=2L, timeout = 300)" &gt;&gt; ~/.Rprofile</span> <span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a> Rscript -e 'pkgs &lt;- remotes::dev_package_deps("/build/examplePKG", dependencies = TRUE)' \</span> <span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a> -e 'BiocManager::install(pkgs$package, update = TRUE, ask = FALSE)'</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> <p>We use the <code>addnab/docker-run-action</code> action to run this step inside a docker container and provide the <code>image</code> argument with the TinyTex arm64 container discussed earlier.</p> <p>The <code>options</code> argument supplies arguments you would give Docker at the command line if you were running it locally. Here we set the platform to <code>linux/arm64</code> to work with the QEMU emulation. We mount two locations from our runner into to container: the location of the library we created earlier and the directory containing the package to be tests. Inside the container these will be found at <code>/R-lib</code> and <code>/build</code> respectively. We also set the <code>R_LIBS_USER</code> environment variable, so R will use the mounted library in preference to anywhere else.</p> <p>The <code>run</code> section is where we provide the command to be executed inside the container. First there’s an optional step to set the number of CPUs R should use by default. Currently GitHub runners are dual core and there’s a performance benefit to ensuring R uses both of these when installing multiple packages from source as we’re doing here. Then we use the <code>remotes</code> and <code>BiocManager</code> packages to list the package dependencies and install them.</p> <p>If this job executes successfully we should have a cached library containing all the packages required to test the package.</p> </section> <section id="running-the-package-tests" class="level2"> <h2 class="anchored" data-anchor-id="running-the-package-tests">Running the package tests</h2> <p>The second job in our workflow will carry out the package tests. We can use the <code>needs:</code> argument to specify that this job requires the <code>install-dependencies</code> job to have completed successfully. Without specifying this GitHub Actions will try to run the two jobs simultaneously, which clearly isn’t appropriate.</p> <div class="sourceCode" id="cb4"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb4-1"><a href="#cb4-1" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">check-arm64</span><span class="kw">:</span></span> <span id="cb4-2"><a href="#cb4-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Test package on ARM64</span></span> <span id="cb4-3"><a href="#cb4-3" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">runs-on</span><span class="kw">:</span><span class="at"> ubuntu-22.04</span></span> <span id="cb4-4"><a href="#cb4-4" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">needs</span><span class="kw">:</span><span class="at"> install-dependencies</span></span> <span id="cb4-5"><a href="#cb4-5" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">steps</span><span class="kw">:</span></span> <span id="cb4-6"><a href="#cb4-6" aria-hidden="true" tabindex="-1"></a></span> <span id="cb4-7"><a href="#cb4-7" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> checkout</span></span> <span id="cb4-8"><a href="#cb4-8" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> actions/checkout@v3</span></span> <span id="cb4-9"><a href="#cb4-9" aria-hidden="true" tabindex="-1"></a></span> <span id="cb4-10"><a href="#cb4-10" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Set up QEMU</span></span> <span id="cb4-11"><a href="#cb4-11" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> docker/setup-qemu-action@v2</span></span> <span id="cb4-12"><a href="#cb4-12" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">with</span><span class="kw">:</span></span> <span id="cb4-13"><a href="#cb4-13" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">platforms</span><span class="kw">:</span><span class="at"> arm64</span></span> <span id="cb4-14"><a href="#cb4-14" aria-hidden="true" tabindex="-1"></a></span> <span id="cb4-15"><a href="#cb4-15" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Make R library</span></span> <span id="cb4-16"><a href="#cb4-16" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">run</span><span class="kw">:</span><span class="at"> mkdir -p ${RUNNER_TEMP}/R-lib</span></span> <span id="cb4-17"><a href="#cb4-17" aria-hidden="true" tabindex="-1"></a></span> <span id="cb4-18"><a href="#cb4-18" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Cache Dependencies</span></span> <span id="cb4-19"><a href="#cb4-19" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">id</span><span class="kw">:</span><span class="at"> cache-deps</span></span> <span id="cb4-20"><a href="#cb4-20" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> actions/cache@v3</span></span> <span id="cb4-21"><a href="#cb4-21" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">with</span><span class="kw">:</span></span> <span id="cb4-22"><a href="#cb4-22" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">path</span><span class="kw">:</span><span class="at"> ${{ runner.temp }}/R-lib</span></span> <span id="cb4-23"><a href="#cb4-23" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">key</span><span class="kw">:</span><span class="at"> R_lib-ARM64-${{ hashFiles('**/DESCRIPTION') }}</span></span> <span id="cb4-24"><a href="#cb4-24" aria-hidden="true" tabindex="-1"></a><span class="fu"> restore-keys</span><span class="kw">: </span><span class="ch">|</span></span> <span id="cb4-25"><a href="#cb4-25" aria-hidden="true" tabindex="-1"></a> R_lib-ARM64-${{ hashFiles('**/DESCRIPTION') }}</span> <span id="cb4-26"><a href="#cb4-26" aria-hidden="true" tabindex="-1"></a> R_lib-ARM64-</span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> <p>The first few steps are the same as before, checking out the package repository, installing QEMU, and then restoring the cached set of packages.</p> <p>Next we can again use the <code>addnab/docker-run-action</code> action to execute our tests inside a docker container. We use the same container image and set of options as before to mount the package and library locations, as well as supplying the <code>--workdir</code> argument to ensure the following commands are executed in the folder where the package directory can be found.</p> <div class="sourceCode" id="cb5"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> Test Package</span></span> <span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> addnab/docker-run-action@v3</span></span> <span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">with</span><span class="kw">:</span></span> <span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">image</span><span class="kw">:</span><span class="at"> ghcr.io/grimbough/bioc-with-tinytex:devel-arm64</span></span> <span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a><span class="fu"> options</span><span class="kw">: </span><span class="ch">|</span></span> <span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a> --platform linux/arm64</span> <span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a> --volume ${{ runner.temp }}/R-lib:/R-lib</span> <span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a> --volume ${{ github.workspace }}:/build</span> <span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a> --env R_LIBS_USER=/R-lib</span> <span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a> --workdir /build</span> <span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a><span class="fu"> run</span><span class="kw">: </span><span class="ch">|</span></span> <span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a> ## Install and store the log like on the BioC Build System</span> <span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a> R CMD INSTALL examplePKG &amp;&gt; examplePKG.install-out.txt</span> <span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a> if [ $? -eq 1 ]; then</span> <span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a> cat examplePKG.install-out.txt</span> <span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a> exit 1;</span> <span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a> fi</span> <span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a> </span> <span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a> ## build the package</span> <span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a> R CMD build --keep-empty-dirs --no-resave-data examplePKG</span> <span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a> if [ $? -eq 1 ]; then exit 1; fi</span> <span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a> </span> <span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a> ## Check the package using the shortcut from the BBS</span> <span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a> R CMD check --install=check:examplePKG.install-out.txt --library="${R_LIBS_USER}" --no-vignettes --timings examplePKG*.tar.gz</span> <span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a> if [ $? -eq 1 ]; then exit 1; fi</span> <span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a> </span> <span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a> ## build a package binary for Linux ARM64</span> <span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a> mkdir -p examplePKG.buildbin-libdir</span> <span id="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a> R CMD INSTALL --build --library=examplePKG.buildbin-libdir examplePKG*.tar.gz</span> <span id="cb5-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a> if [ $? -eq 1 ]; then exit 1; fi</span> <span id="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">shell</span><span class="kw">:</span><span class="at"> bash</span></span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> <p>We use the <code>run</code> option to provide steps similar to the Bioconductor Build System. There are four distinct stages to this process: install, build, check, and build binary. The arguments and setting used here are representative of the BBS, but one can change them if other testing mechanism are required. You could also choose to split this into four separate job steps if you wanted more fine grained control.</p> <p>One minor wrinkle when running this in a Docker container is that GitHub Actions will use the return code of the Docker process to determine whether the step has failed or not, rather than the processes run inside the container. Thus it will often give a green tick, even if something went wrong, and it is easy to miss an error if just glancing at the step summaries. To resolve this, after each process in the container we test the return code produced by R and exit if it indicates failure.</p> <p>Finally, although some of the test outputs will be printed to the workflow log, we might want to make any output available to download and investigate further. To do this we can use the <a href="https://github.com/actions/upload-artifact"><code>upload-artifact</code></a> action.</p> <div class="sourceCode" id="cb6"><pre class="sourceCode yaml code-with-copy"><code class="sourceCode yaml"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="kw">-</span><span class="at"> </span><span class="fu">uses</span><span class="kw">:</span><span class="at"> actions/upload-artifact@v3</span></span> <span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">if</span><span class="kw">:</span><span class="at"> always()</span></span> <span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">with</span><span class="kw">:</span></span> <span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">name</span><span class="kw">:</span><span class="at"> my-artifact</span></span> <span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a><span class="fu"> path</span><span class="kw">: </span><span class="ch">|</span></span> <span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a> ~/**/*.tar.gz</span> <span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a> ~/**/*.install-out.txt</span> <span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a> ~/**/*.Rcheck</span> <span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a><span class="at"> </span><span class="fu">if-no-files-found</span><span class="kw">:</span><span class="at"> warn</span></span></code><button title="Copy to Clipboard" class="code-copy-button"><i class="bi"></i></button></pre></div> <p>We use <code>if: always()</code> to ensure the upload happens even if a previous step has failed; it’s often more important to get the logs when there’s a problem! Assuming every step executed this should upload both the source and binary tarballs, the installation log file, and the folder produced by <code>R CMD check</code>. Hopefully that is enough information to diagnose any issue.</p> </section> <section id="conclusion" class="level2"> <h2 class="anchored" data-anchor-id="conclusion">Conclusion</h2> <p>The combination of GitHub Actions and QEMU provides a platform for testing packages across multiple CPU architectures more rapidly than the Bioconductor Build System. Using them in a continuous integration environment allows one to detect and highlight unforeseen issues introduced by changes to a package or the wider R environment. The same emulation techniques can then be employed on your local development environment to find a solution, before testing again in a your GitHub Workflow, and finally deploying to Bioconductor.</p> </section> </section> <p> © 2025 Bioconductor. Content is published under <a href="https://creativecommons.org/licenses/by/4.0/">Creative Commons CC-BY-4.0 License</a> for the text and <a href="https://opensource.org/licenses/BSD-3-Clause">BSD 3-Clause License</a> for any code. | <a href="https://www.r-bloggers.com">R-Bloggers</a> </p></main> <!-- /main --> <script id="quarto-html-after-body" type="application/javascript"> window.document.addEventListener("DOMContentLoaded", function (event) { const toggleBodyColorMode = (bsSheetEl) => { const mode = bsSheetEl.getAttribute("data-mode"); const bodyEl = window.document.querySelector("body"); if (mode === "dark") { bodyEl.classList.add("quarto-dark"); bodyEl.classList.remove("quarto-light"); } else { bodyEl.classList.add("quarto-light"); bodyEl.classList.remove("quarto-dark"); } } const toggleBodyColorPrimary = () => { const bsSheetEl = window.document.querySelector("link#quarto-bootstrap"); if (bsSheetEl) { toggleBodyColorMode(bsSheetEl); } } toggleBodyColorPrimary(); const icon = ""; const anchorJS = new window.AnchorJS(); anchorJS.options = { placement: 'right', icon: icon }; anchorJS.add('.anchored'); const isCodeAnnotation = (el) => { for (const clz of el.classList) { if (clz.startsWith('code-annotation-')) { return true; } } return false; } const onCopySuccess = function(e) { // button target const button = e.trigger; // don't keep focus button.blur(); // flash "checked" button.classList.add('code-copy-button-checked'); var currentTitle = button.getAttribute("title"); button.setAttribute("title", "Copied!"); let tooltip; if (window.bootstrap) { button.setAttribute("data-bs-toggle", "tooltip"); button.setAttribute("data-bs-placement", "left"); button.setAttribute("data-bs-title", "Copied!"); tooltip = new bootstrap.Tooltip(button, { trigger: "manual", customClass: "code-copy-button-tooltip", offset: [0, -8]}); tooltip.show(); } setTimeout(function() { if (tooltip) { tooltip.hide(); button.removeAttribute("data-bs-title"); button.removeAttribute("data-bs-toggle"); button.removeAttribute("data-bs-placement"); } button.setAttribute("title", currentTitle); button.classList.remove('code-copy-button-checked'); }, 1000); // clear code selection e.clearSelection(); } const getTextToCopy = function(trigger) { const codeEl = trigger.previousElementSibling.cloneNode(true); for (const childEl of codeEl.children) { if (isCodeAnnotation(childEl)) { childEl.remove(); } } return codeEl.innerText; } const clipboard = new window.ClipboardJS('.code-copy-button:not([data-in-quarto-modal])', { text: getTextToCopy }); clipboard.on('success', onCopySuccess); if (window.document.getElementById('quarto-embedded-source-code-modal')) { const clipboardModal = new window.ClipboardJS('.code-copy-button[data-in-quarto-modal]', { text: getTextToCopy, container: window.document.getElementById('quarto-embedded-source-code-modal') }); clipboardModal.on('success', onCopySuccess); } var localhostRegex = new RegExp(/^(?:http|https):\/\/localhost\:?[0-9]*\//); var mailtoRegex = new RegExp(/^mailto:/); var filterRegex = new RegExp("https:\/\/blog\.bioconductor\.org\/"); var isInternal = (href) => { return filterRegex.test(href) || localhostRegex.test(href) || mailtoRegex.test(href); } // Inspect non-navigation links and adorn them if external var links = window.document.querySelectorAll('a[href]:not(.nav-link):not(.navbar-brand):not(.toc-action):not(.sidebar-link):not(.sidebar-item-toggle):not(.pagination-link):not(.no-external):not([aria-hidden]):not(.dropdown-item):not(.quarto-navigation-tool):not(.about-link)'); for (var i=0; i<links.length; i++) { const link = links[i]; if (!isInternal(link.href)) { // undo the damage that might have been done by quarto-nav.js in the case of // links that we want to consider external if (link.dataset.originalHref !== undefined) { link.href = link.dataset.originalHref; } } } function tippyHover(el, contentFn, onTriggerFn, onUntriggerFn) { const config = { allowHTML: true, maxWidth: 500, delay: 100, arrow: false, appendTo: function(el) { return el.parentElement; }, interactive: true, interactiveBorder: 10, theme: 'quarto', placement: 'bottom-start', }; if (contentFn) { config.content = contentFn; } if (onTriggerFn) { config.onTrigger = onTriggerFn; } if (onUntriggerFn) { config.onUntrigger = onUntriggerFn; } window.tippy(el, config); } const noterefs = window.document.querySelectorAll('a[role="doc-noteref"]'); for (var i=0; i<noterefs.length; i++) { const ref = noterefs[i]; tippyHover(ref, function() { // use id or data attribute instead here let href = ref.getAttribute('data-footnote-href') || ref.getAttribute('href'); try { href = new URL(href).hash; } catch {} const id = href.replace(/^#\/?/, ""); const note = window.document.getElementById(id); if (note) { return note.innerHTML; } else { return ""; } }); } const xrefs = window.document.querySelectorAll('a.quarto-xref'); const processXRef = (id, note) => { // Strip column container classes const stripColumnClz = (el) => { el.classList.remove("page-full", "page-columns"); if (el.children) { for (const child of el.children) { stripColumnClz(child); } } } stripColumnClz(note) if (id === null || id.startsWith('sec-')) { // Special case sections, only their first couple elements const container = document.createElement("div"); if (note.children && note.children.length > 2) { container.appendChild(note.children[0].cloneNode(true)); for (let i = 1; i < note.children.length; i++) { const child = note.children[i]; if (child.tagName === "P" && child.innerText === "") { continue; } else { container.appendChild(child.cloneNode(true)); break; } } if (window.Quarto?.typesetMath) { window.Quarto.typesetMath(container); } return container.innerHTML } else { if (window.Quarto?.typesetMath) { window.Quarto.typesetMath(note); } return note.innerHTML; } } else { // Remove any anchor links if they are present const anchorLink = note.querySelector('a.anchorjs-link'); if (anchorLink) { anchorLink.remove(); } if (window.Quarto?.typesetMath) { window.Quarto.typesetMath(note); } if (note.classList.contains("callout")) { return note.outerHTML; } else { return note.innerHTML; } } } for (var i=0; i<xrefs.length; i++) { const xref = xrefs[i]; tippyHover(xref, undefined, function(instance) { instance.disable(); let url = xref.getAttribute('href'); let hash = undefined; if (url.startsWith('#')) { hash = url; } else { try { hash = new URL(url).hash; } catch {} } if (hash) { const id = hash.replace(/^#\/?/, ""); const note = window.document.getElementById(id); if (note !== null) { try { const html = processXRef(id, note.cloneNode(true)); instance.setContent(html); } finally { instance.enable(); instance.show(); } } else { // See if we can fetch this fetch(url.split('#')[0]) .then(res => res.text()) .then(html => { const parser = new DOMParser(); const htmlDoc = parser.parseFromString(html, "text/html"); const note = htmlDoc.getElementById(id); if (note !== null) { const html = processXRef(id, note); instance.setContent(html); } }).finally(() => { instance.enable(); instance.show(); }); } } else { // See if we can fetch a full url (with no hash to target) // This is a special case and we should probably do some content thinning / targeting fetch(url) .then(res => res.text()) .then(html => { const parser = new DOMParser(); const htmlDoc = parser.parseFromString(html, "text/html"); const note = htmlDoc.querySelector('main.content'); if (note !== null) { // This should only happen for chapter cross references // (since there is no id in the URL) // remove the first header if (note.children.length > 0 && note.children[0].tagName === "HEADER") { note.children[0].remove(); } const html = processXRef(null, note); instance.setContent(html); } }).finally(() => { instance.enable(); instance.show(); }); } }, function(instance) { }); } let selectedAnnoteEl; const selectorForAnnotation = ( cell, annotation) => { let cellAttr = 'data-code-cell="' + cell + '"'; let lineAttr = 'data-code-annotation="' + annotation + '"'; const selector = 'span[' + cellAttr + '][' + lineAttr + ']'; return selector; } const selectCodeLines = (annoteEl) => { const doc = window.document; const targetCell = annoteEl.getAttribute("data-target-cell"); const targetAnnotation = annoteEl.getAttribute("data-target-annotation"); const annoteSpan = window.document.querySelector(selectorForAnnotation(targetCell, targetAnnotation)); const lines = annoteSpan.getAttribute("data-code-lines").split(","); const lineIds = lines.map((line) => { return targetCell + "-" + line; }) let top = null; let height = null; let parent = null; if (lineIds.length > 0) { //compute the position of the single el (top and bottom and make a div) const el = window.document.getElementById(lineIds[0]); top = el.offsetTop; height = el.offsetHeight; parent = el.parentElement.parentElement; if (lineIds.length > 1) { const lastEl = window.document.getElementById(lineIds[lineIds.length - 1]); const bottom = lastEl.offsetTop + lastEl.offsetHeight; height = bottom - top; } if (top !== null && height !== null && parent !== null) { // cook up a div (if necessary) and position it let div = window.document.getElementById("code-annotation-line-highlight"); if (div === null) { div = window.document.createElement("div"); div.setAttribute("id", "code-annotation-line-highlight"); div.style.position = 'absolute'; parent.appendChild(div); } div.style.top = top - 2 + "px"; div.style.height = height + 4 + "px"; div.style.left = 0; let gutterDiv = window.document.getElementById("code-annotation-line-highlight-gutter"); if (gutterDiv === null) { gutterDiv = window.document.createElement("div"); gutterDiv.setAttribute("id", "code-annotation-line-highlight-gutter"); gutterDiv.style.position = 'absolute'; const codeCell = window.document.getElementById(targetCell); const gutter = codeCell.querySelector('.code-annotation-gutter'); gutter.appendChild(gutterDiv); } gutterDiv.style.top = top - 2 + "px"; gutterDiv.style.height = height + 4 + "px"; } selectedAnnoteEl = annoteEl; } }; const unselectCodeLines = () => { const elementsIds = ["code-annotation-line-highlight", "code-annotation-line-highlight-gutter"]; elementsIds.forEach((elId) => { const div = window.document.getElementById(elId); if (div) { div.remove(); } }); selectedAnnoteEl = undefined; }; // Handle positioning of the toggle window.addEventListener( "resize", throttle(() => { elRect = undefined; if (selectedAnnoteEl) { selectCodeLines(selectedAnnoteEl); } }, 10) ); function throttle(fn, ms) { let throttle = false; let timer; return (...args) => { if(!throttle) { // first call gets through fn.apply(this, args); throttle = true; } else { // all the others get throttled if(timer) clearTimeout(timer); // cancel #2 timer = setTimeout(() => { fn.apply(this, args); timer = throttle = false; }, ms); } }; } // Attach click handler to the DT const annoteDls = window.document.querySelectorAll('dt[data-target-cell]'); for (const annoteDlNode of annoteDls) { annoteDlNode.addEventListener('click', (event) => { const clickedEl = event.target; if (clickedEl !== selectedAnnoteEl) { unselectCodeLines(); const activeEl = window.document.querySelector('dt[data-target-cell].code-annotation-active'); if (activeEl) { activeEl.classList.remove('code-annotation-active'); } selectCodeLines(clickedEl); clickedEl.classList.add('code-annotation-active'); } else { // Unselect the line unselectCodeLines(); clickedEl.classList.remove('code-annotation-active'); } }); } const findCites = (el) => { const parentEl = el.parentElement; if (parentEl) { const cites = parentEl.dataset.cites; if (cites) { return { el, cites: cites.split(' ') }; } else { return findCites(el.parentElement) } } else { return undefined; } }; var bibliorefs = window.document.querySelectorAll('a[role="doc-biblioref"]'); for (var i=0; i<bibliorefs.length; i++) { const ref = bibliorefs[i]; const citeInfo = findCites(ref); if (citeInfo) { tippyHover(citeInfo.el, function() { var popup = window.document.createElement('div'); citeInfo.cites.forEach(function(cite) { var citeDiv = window.document.createElement('div'); citeDiv.classList.add('hanging-indent'); citeDiv.classList.add('csl-entry'); var biblioDiv = window.document.getElementById('ref-' + cite); if (biblioDiv) { citeDiv.innerHTML = biblioDiv.innerHTML; } popup.appendChild(citeDiv); }); return popup.innerHTML; }); } } }); </script> </div> <!-- /content --> </body></html>

Pages: 1 2 3 4 5 6 7 8 9 10