1
2022-12-18 17:12:33 +00:00

264 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" type="image/jpg" href="https:&#x2F;&#x2F;branding.ewpratten.com&#x2F;pfp&#x2F;2022&#x2F;460x460.webp" />
<link rel="canonical" href="https:&#x2F;&#x2F;ewpratten.com&#x2F;blog&#x2F;corepack-development&#x2F;" />
<link rel="alternate" type="application/rss+xml" title="RSS" href="https://ewpratten.com/rss.xml">
<meta name="twitter:card" content="summary" />
<meta name="og:site" content="ewpratten.com" />
<meta name="og:site_name" content="Evan Pratten" />
<meta name="og:image"
content="https:&#x2F;&#x2F;branding.ewpratten.com&#x2F;pfp&#x2F;2022&#x2F;460x460.webp" />
<meta property="og:description" content="An overview of how I automated the build process for CorePack" />
<meta property="description" content="An overview of how I automated the build process for CorePack" />
<meta name="description" content="An overview of how I automated the build process for CorePack">
<meta property="og:title" content="Using Bazel to create Minecraft modpacks - Evan Pratten" />
<meta property="og:type" content="article" />
<title>Using Bazel to create Minecraft modpacks | Evan Pratten</title>
<link rel="stylesheet" href="/global.css">
<link rel="stylesheet" href="/dist/github-markdown-css/github-markdown-light.css" lazyload>
<link rel="stylesheet" href="/styles/bootstrap.css" lazyload>
<link rel="stylesheet" href="/styles/typography.css">
</head>
<body>
<div class="page">
<link rel="stylesheet" href="/styles/components/heading-card.css">
<div class="heading-card">
<div class="profile-photo-container">
<img src="https:&#x2F;&#x2F;branding.ewpratten.com&#x2F;pfp&#x2F;2022&#x2F;460x460.webp" alt="Profile Photo" loading="lazy">
</div>
<div class="text-container">
<h1>Evan Pratten</h1>
<p>Software Developer</p>
</div>
</div>
<div class="container">
<link rel="stylesheet" href="/styles/components/navbar.css">
<div class="ewp-navbar">
<hr>
<ul class="navbar-items">
<li><a href="/">Home</a></li>
<li class="separator">|</li>
<li><a href="/timeline">Timeline</a></li>
<li class="separator">|</li>
<li class="dropdown-center">
<a href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
More
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="/photography">Photography</a></li>
<li><a class="dropdown-item" href="/contact">Contact</a></li>
</ul>
</li>
</ul>
<hr>
</div>
</div>
<article id="content" class="container markdown-body">
<h1 style="margin-bottom:0;padding-bottom:0;">Using Bazel to create Minecraft modpacks</h1>
<em>An overview of how I automated the build process for CorePack</em>
<br><br>
<p><em>All content of this post is based around the work I did <a rel="noopener" target="_blank" href="https://github.com/Ewpratten/corepack">here</a></em></p>
<p>Back in <a rel="noopener" target="_blank" href="https://minecraft.gamepedia.com/Java_Edition_1.2.5">2012</a>, I got in to Minecraft mod development, and soon after, put together an almost-vanilla client-side modpack for myself that mainly contained rendering, UI, and quality-of-life tweaks. While this modpack never got published, or was even given a name, I kept maintaining it for years until I eventually stopped playing Minecraft just before the release of Minecraft <a rel="noopener" target="_blank" href="https://minecraft.gamepedia.com/Java_Edition_1.9"><code>1.9</code></a> (in 2016). I had gotten so used to the features of this modpack, that playing truly vanilla Minecraft didn't feel correct.</p>
<p>Recently, a few friends invited me to join their private Minecraft server, and despite having not touched the game for around four years, I decided to join. This was a bit of a mistake on their part, as they now get the pleasure of someone who used to main <a rel="noopener" target="_blank" href="https://minecraft.gamepedia.com/Java_Edition_1.6.4"><code>1.6.4</code></a> constantly walking up to things and asking <em>&quot;What is this and how does it work?&quot;</em>. I have started to get used to the very weird new collection of blocks, completely reworked command system, over-complicated combat system, and a new rendering system that makes everything &quot;look wrong&quot;.</p>
<p>One major thing was still missing though, <em>where was my modpack?</em> I set out to rebuild my good old modpack (and finally give it a name, <em>CorePack</em>). Not much has changed, most of the same rendering and UI mods are back, along with the same <a rel="noopener" target="_blank" href="https://en.wikipedia.org/wiki/OpenGL_Shading_Language">GLSL</a> shaders, and similar textures. Although, I did decide to take a <em>&quot;major step&quot;</em> and switch from the <a rel="noopener" target="_blank" href="http://files.minecraftforge.net/">Forge Mod Loader</a> to the <a rel="noopener" target="_blank" href="https://fabricmc.net/">Fabric Loader</a>, since I prefer Fabric's API. </p>
<h2 id="curseforge-bazel">Curseforge &amp; Bazel</h2>
<p>I don't remember <a rel="noopener" target="_blank" href="https://curseforge.com/">Curseforge</a> existing back when I used to play regularly. It is a huge improvement over the <a rel="noopener" target="_blank" href="https://www.planetminecraft.com/">PlanetMinecraft</a> forums, as curse provides a clean way to access data about published Minecraft mods, and even has an API! Luckily, since I switched the modpack to Fabric, every mod I was looking for was available through curse (although, it seems <a rel="noopener" target="_blank" href="https://www.curseforge.com/minecraft/mc-mods/notenoughitems">NEI</a> is a thing of the past).</p>
<p>My main goal for the updated version of CorePack was to design it in such a way I could make a CI pipeline generate new releases for me when mods are updated. This requires programmatically pulling information about mods, and their JAR files using a buildsystem script. Since this project involves working with a large amount of data from various external sources, I once-again chose to use <a rel="noopener" target="_blank" href="https://bazel.build">Bazel</a>, a buildsystem that excels at these kinds of projects.</p>
<p>While Curseforge provides a very easy to use API for working with mod data, @Wyn-Price (a fellow mod developer) has put together an amazing project called <a rel="noopener" target="_blank" href="https://www.cursemaven.com/">Curse Maven</a> that I decided to use instead. Curse Maven is a serverless API that acts much like my <a href="/blog/2020/09/17/ultralight-writeup">Ultralight project</a>. Any request for an artifact to Curse Maven will be redirected, and served from the <a rel="noopener" target="_blank" href="https://authors.curseforge.com/knowledge-base/projects/529-api#Maven">Curseforge Maven server</a> without the need for me to figure out the long-form artifact identifiers used internally by curse.</p>
<p>Curse Maven makes loading a mod (in this case, <a rel="noopener" target="_blank" href="https://www.curseforge.com/minecraft/mc-mods/fabric-api"><code>fabric-api</code></a>) into Bazel as easy as:</p>
<pre data-lang="python" style="background-color:#2b303b;color:#c0c5ce;" class="language-python "><code class="language-python" data-lang="python"><span style="color:#65737e;"># WORKSPACE
</span><span style="color:#65737e;"># Load bazel_maven_repository
</span><span style="color:#bf616a;">http_archive</span><span>(
</span><span> </span><span style="color:#bf616a;">name </span><span>= &quot;</span><span style="color:#a3be8c;">maven_repository_rules</span><span>&quot;,
</span><span> </span><span style="color:#bf616a;">strip_prefix </span><span>= &quot;</span><span style="color:#a3be8c;">bazel_maven_repository-1.2.0</span><span>&quot;,
</span><span> </span><span style="color:#bf616a;">type </span><span>= &quot;</span><span style="color:#a3be8c;">zip</span><span>&quot;,
</span><span> </span><span style="color:#bf616a;">urls </span><span>= [&quot;</span><span style="color:#a3be8c;">https://github.com/square/bazel_maven_repository/archive/1.2.0.zip</span><span>&quot;],
</span><span>)
</span><span style="color:#bf616a;">load</span><span>(&quot;</span><span style="color:#a3be8c;">@maven_repository_rules//maven:maven.bzl</span><span>&quot;, &quot;</span><span style="color:#a3be8c;">maven_repository_specification</span><span>&quot;)
</span><span style="color:#bf616a;">load</span><span>(&quot;</span><span style="color:#a3be8c;">@maven_repository_rules//maven:jetifier.bzl</span><span>&quot;, &quot;</span><span style="color:#a3be8c;">jetifier_init</span><span>&quot;)
</span><span style="color:#bf616a;">jetifier_init</span><span>()
</span><span>
</span><span style="color:#65737e;"># Declare any mods as maven artifacts
</span><span style="color:#bf616a;">maven_repository_specification</span><span>(
</span><span> </span><span style="color:#bf616a;">name </span><span>= &quot;</span><span style="color:#a3be8c;">maven</span><span>&quot;,
</span><span> </span><span style="color:#bf616a;">artifacts </span><span>= {
</span><span> &quot;</span><span style="color:#a3be8c;">curse.maven:fabric-api:3049174</span><span>&quot;: {&quot;</span><span style="color:#a3be8c;">insecure</span><span>&quot;: </span><span style="color:#d08770;">True</span><span>}
</span><span> },
</span><span> </span><span style="color:#bf616a;">repository_urls </span><span>= [
</span><span> &quot;</span><span style="color:#a3be8c;">https://www.cursemaven.com</span><span>&quot;,
</span><span> ],
</span><span>)
</span></code></pre>
<p>The above snippet uses a Bazel ruleset developed by <a rel="noopener" target="_blank" href="https://squareup.com/ca/en">Square, Inc.</a> called <a rel="noopener" target="_blank" href="https://github.com/square/bazel_maven_repository"><code>bazel_maven_repository</code></a>. </p>
<h2 id="modpack-configuration">Modpack configuration</h2>
<p>Since my pack is designed for use with <a rel="noopener" target="_blank" href="https://multimc.org/">MultiMC</a>, two sets of configuration files are needed. The first set tells MultiMC which versions of <a rel="noopener" target="_blank" href="https://www.lwjgl.org/">LWJGL</a>, Minecraft, and Fabric to use, and the second set are the in-game config files. Many of these files contain information that I would like to modify from Bazel during the modpack build step. Luckily, the <a rel="noopener" target="_blank" href="https://docs.bazel.build/versions/master/skylark/language.html">Starlark</a> core library comes with an action called <a rel="noopener" target="_blank" href="https://docs.bazel.build/versions/2.0.0/skylark/lib/actions.html#expand_template"><code>expand_template</code></a>. <code>expand_template</code> is basically a find-and-replace tool that will perform substitutions on files. Since this is an action, and not a rule, it must be wrapped with a small rule declaration:</p>
<pre data-lang="python" style="background-color:#2b303b;color:#c0c5ce;" class="language-python "><code class="language-python" data-lang="python"><span style="color:#65737e;"># tools/template.bzl
</span><span style="color:#b48ead;">def </span><span style="color:#8fa1b3;">expand_template_impl</span><span>(</span><span style="color:#bf616a;">ctx</span><span>):
</span><span> ctx.actions.</span><span style="color:#bf616a;">expand_template</span><span>(
</span><span> </span><span style="color:#bf616a;">template </span><span>= ctx.file.template,
</span><span> </span><span style="color:#bf616a;">output </span><span>= ctx.outputs.out,
</span><span> </span><span style="color:#bf616a;">substitutions </span><span>= {
</span><span> k: ctx.</span><span style="color:#bf616a;">expand_location</span><span>(v, ctx.attr.data)
</span><span> </span><span style="color:#b48ead;">for </span><span>k, v </span><span style="color:#b48ead;">in </span><span>ctx.attr.substitutions.</span><span style="color:#bf616a;">items</span><span>()
</span><span> },
</span><span> </span><span style="color:#bf616a;">is_executable </span><span>= ctx.attr.is_executable,
</span><span> )
</span><span>
</span><span>expand_template = </span><span style="color:#bf616a;">rule</span><span>(
</span><span> </span><span style="color:#bf616a;">implementation </span><span>= expand_template_impl,
</span><span> </span><span style="color:#bf616a;">attrs </span><span>= {
</span><span> &quot;</span><span style="color:#a3be8c;">template</span><span>&quot;: attr.</span><span style="color:#bf616a;">label</span><span>(</span><span style="color:#bf616a;">mandatory </span><span>= </span><span style="color:#d08770;">True</span><span>, </span><span style="color:#bf616a;">allow_single_file </span><span>= </span><span style="color:#d08770;">True</span><span>),
</span><span> &quot;</span><span style="color:#a3be8c;">substitutions</span><span>&quot;: attr.</span><span style="color:#bf616a;">string_dict</span><span>(</span><span style="color:#bf616a;">mandatory </span><span>= </span><span style="color:#d08770;">True</span><span>),
</span><span> &quot;</span><span style="color:#a3be8c;">out</span><span>&quot;: attr.</span><span style="color:#bf616a;">output</span><span>(</span><span style="color:#bf616a;">mandatory </span><span>= </span><span style="color:#d08770;">True</span><span>),
</span><span> &quot;</span><span style="color:#a3be8c;">is_executable</span><span>&quot;: attr.</span><span style="color:#bf616a;">bool</span><span>(</span><span style="color:#bf616a;">default </span><span>= </span><span style="color:#d08770;">False</span><span>, </span><span style="color:#bf616a;">mandatory </span><span>= </span><span style="color:#d08770;">False</span><span>),
</span><span> &quot;</span><span style="color:#a3be8c;">data</span><span>&quot;: attr.</span><span style="color:#bf616a;">label_list</span><span>(</span><span style="color:#bf616a;">allow_files </span><span>= </span><span style="color:#d08770;">True</span><span>),
</span><span> },
</span><span>)
</span></code></pre>
<p>In a <code>BUILD</code> file, template rules can be defined as follows:</p>
<pre data-lang="python" style="background-color:#2b303b;color:#c0c5ce;" class="language-python "><code class="language-python" data-lang="python"><span style="color:#65737e;"># BUILD
</span><span style="color:#bf616a;">load</span><span>(&quot;</span><span style="color:#a3be8c;">//tools:template.bzl</span><span>&quot;, &quot;</span><span style="color:#a3be8c;">expand_template</span><span>&quot;)
</span><span>
</span><span style="color:#bf616a;">expand_template</span><span>(
</span><span> </span><span style="color:#bf616a;">name </span><span>= &quot;</span><span style="color:#a3be8c;">my_config</span><span>&quot;,
</span><span> </span><span style="color:#bf616a;">template </span><span>= &quot;</span><span style="color:#a3be8c;">config.json.in</span><span>&quot;,
</span><span> </span><span style="color:#bf616a;">out </span><span>= &quot;</span><span style="color:#a3be8c;">config.json</span><span>&quot;,
</span><span> </span><span style="color:#bf616a;">substitutions </span><span>= {
</span><span> &quot;</span><span style="color:#a3be8c;">TEST_SUBS</span><span>&quot;: &quot;</span><span style="color:#a3be8c;">hello world</span><span>&quot;
</span><span> }
</span><span>)
</span></code></pre>
<p>Using the following example file as <code>config.json.in</code>, this rule would have the following effect:</p>
<pre data-lang="js" style="background-color:#2b303b;color:#c0c5ce;" class="language-js "><code class="language-js" data-lang="js"><span style="color:#65737e;">// config.json.in
</span><span>{
</span><span> &quot;</span><span style="color:#a3be8c;">key</span><span>&quot;: &quot;</span><span style="color:#a3be8c;">TEST_SUBS</span><span>&quot;
</span><span>}
</span><span>
</span><span style="color:#65737e;">// config.json
</span><span>{
</span><span> &quot;</span><span style="color:#a3be8c;">key</span><span>&quot;: &quot;</span><span style="color:#a3be8c;">hello world</span><span>&quot;
</span><span>}
</span></code></pre>
<h2 id="packaging">Packaging</h2>
<p>Once mods are loaded, and configuration files are defined in the buildsystem, I use a large number of <a rel="noopener" target="_blank" href="https://docs.bazel.build/versions/master/be/general.html#filegroup"><code>filegroup</code></a> and <a rel="noopener" target="_blank" href="https://docs.bazel.build/versions/master/be/general.html#genrule"><code>genrule</code></a> rules to set up a directory hierarchy in the workspace, and wrap everything in a call to <a rel="noopener" target="_blank" href="https://sourcegraph.com/github.com/v2ray/ext/-/blob/bazel/zip.bzl#L23:25"><code>zipper</code></a> to package the modpack into a ZIP file.</p>
<p>Finally, I use <a rel="noopener" target="_blank" href="https://github.com/features/actions">GitHub Actions</a> to automatically run the buildscript, and publish the resulting MultiMC instance zip to the <a rel="noopener" target="_blank" href="https://github.com/Ewpratten/corepack">GitHub repo</a> for this project.</p>
</article>
<link rel="stylesheet" href="/styles/components/footer.css">
<div class="footer">
<br>
<span class="gray">-- EOF --</span>
<p>
Site design & content by: <a href="/contact">Evan Pratten</a><br>
Consider <a href="/donate" target="_blank">supporting my work</a> if you like what you see<br>
</p>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.2/dist/js/bootstrap.bundle.min.js"
integrity="sha384-OERcA2EqjJCMA+/3y+gxIOqMEjwtxJY7qPCqsdltbNJuaOe923+mo//f6V8Qbsw3"
crossorigin="anonymous"></script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script defer src="https://www.googletagmanager.com/gtag/js?id=G-5912H4H03P"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() { dataLayer.push(arguments); }
gtag('js', new Date());
gtag('config', 'G-5912H4H03P');
</script>
</body>
</html>