Skip to content

Commit 477d764

Browse files
authored
KTOR-7949: Publish DevContainer image (#5048)
* Install Node.js using NVM * Install Chrome and set CHROME_BIN env var * Update Android build tools * Enforce linux/amd64 platform * Use .nvmrc to set Node.js version * Publish devcontainer image to GHCR
1 parent e504ab9 commit 477d764

10 files changed

Lines changed: 233 additions & 21 deletions

File tree

.devcontainer/Dockerfile

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
# syntax=docker/dockerfile:1
2+
# check=skip=FromPlatformFlagConstDisallowed
23

3-
FROM eclipse-temurin:21.0.7_6-jdk-noble AS dev
4+
# Enforce linux/amd64 platform because of two reasons:
5+
# 1. Chrome doesn't provide arm64 binaries for Linux (https://issues.chromium.org/issues/374811603)
6+
# 2. Kotlin/Native doesn't support linux/arm64 as a host platform (KT-36871)
7+
FROM --platform=linux/amd64 eclipse-temurin:21.0.8_9-jdk-noble AS dev
48

59
ARG USERNAME=developer
610
ARG USER_UID=1001
711
ARG USER_GID=$USER_UID
812

913
# Use Bash in "strict mode" to run scripts
10-
SHELL ["/bin/bash", "-euxo", "pipefail", "-c"]
14+
SHELL ["/bin/bash", "-euo", "pipefail", "-c"]
15+
16+
COPY chrome-dependencies.txt /tmp/chrome-dependencies.txt
1117

1218
RUN <<EOT
1319
# Add Adoptium repository to download Eclipse Temurin JDK
@@ -22,18 +28,14 @@ RUN <<EOT
2228
apt-get install --yes --no-install-recommends \
2329
git util-linux openssh-server \
2430
ca-certificates curl fonts-liberation \
25-
libasound2t64 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libcurl4 libcurl4-gnutls-dev \
26-
libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libncurses6 libnspr4 \
27-
libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 \
28-
libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release \
29-
nodejs npm \
31+
$(grep --invert-match '^#' /tmp/chrome-dependencies.txt | tr '\n' ' ') \
3032
temurin-8-jdk temurin-11-jdk temurin-17-jdk \
3133
zip unzip wget xdg-utils sudo
3234
apt-get clean
33-
rm -rf /var/lib/apt/lists/*
35+
rm -rf /var/lib/apt/lists/* /tmp/chrome-dependencies.txt
3436
EOT
3537

36-
# Create non-root user
38+
# Create a non-root user
3739
RUN <<EOT
3840
groupadd --gid $USER_GID $USERNAME
3941
useradd --uid $USER_UID --gid $USER_GID --create-home $USERNAME
@@ -47,6 +49,50 @@ ENV HOME=/home/$USERNAME \
4749
SHELL=/bin/bash
4850
WORKDIR $HOME
4951

52+
# Common variables
53+
ENV KTOR_DEV="true"
54+
ARG local_bin="$HOME/.local/bin"
55+
ENV PATH="$local_bin:$PATH"
56+
57+
# Install Node.js
58+
ARG node_version="22"
59+
ARG nvm_version="0.40.3"
60+
ENV NVM_DIR="$HOME/.nvm"
61+
62+
RUN <<EOT
63+
# Install NVM
64+
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v$nvm_version/install.sh | bash
65+
source $NVM_DIR/nvm.sh
66+
67+
# Install Node and Yarn Classic
68+
nvm install "$node_version"
69+
npm install -g yarn
70+
71+
# Make Node.js binaries available without "source $NVM_DIR/nvm.sh"
72+
# This should be done *after* installation of Yarn
73+
mkdir --parents "$local_bin"
74+
ln -s "$NVM_BIN"/* "$local_bin"
75+
76+
# Print versions
77+
echo "Node $(node --version)"
78+
echo "NPM $(npm --version)"
79+
echo "Yarn $(yarn --version)"
80+
EOT
81+
82+
# Install Chrome
83+
ENV CHROME_BIN="$local_bin/chrome"
84+
85+
RUN <<EOT
86+
source $NVM_DIR/nvm.sh
87+
browsers_dir="$HOME/.cache/browsers"
88+
89+
npx @puppeteer/browsers install chrome@stable --path "$browsers_dir"
90+
91+
# Make Chrome binary visible for Karma
92+
chrome_bin_path=$(npx @puppeteer/browsers list --path "$browsers_dir" | head -n1 | sed 's/.* //')
93+
ln -s "$chrome_bin_path" "$CHROME_BIN"
94+
EOT
95+
5096
# Change shell back to bash from jshell
5197
CMD ["/bin/bash"]
5298

@@ -57,7 +103,7 @@ FROM dev AS dev-android
57103
# Specify build tools version.
58104
# Use the latest available version by default.
59105
# https://developer.android.com/tools/releases/build-tools
60-
ARG build_tools="35.0.0"
106+
ARG build_tools="36.0.0"
61107
ENV ANDROID_BUILD_TOOLS_VERSION=$build_tools
62108

63109
# Define Android environment variables
@@ -69,10 +115,10 @@ ENV PATH="$PATH:$ANDROID_HOME/cmdline-tools/latest/bin"
69115
ENV PATH="$PATH:$ANDROID_HOME/platform-tools"
70116
ENV PATH="$PATH:$ANDROID_HOME/build-tools/$ANDROID_BUILD_TOOLS_VERSION"
71117

72-
# Commandline Tools revisions: https://dl.google.com/android/repository/repository2-3.xml
118+
# Commandline Tools revisions: https://dl.google.com/android/repository/repository2-3.xml (search for "cmdline-tools;latest")
73119
# NOTE: Remember to update cmdline-tools-package.xml on commandlinetools update.
74-
ARG commandlinetools_url=https://dl.google.com/android/repository/commandlinetools-linux-12266719_latest.zip
75-
ARG commandlinetools_sha1=47e61d3bb57b5e907a74f225a767a767a8b4d7a5
120+
ARG commandlinetools_url=https://dl.google.com/android/repository/commandlinetools-linux-13114758_latest.zip
121+
ARG commandlinetools_sha1=5fdcc763663eefb86a5b8879697aa6088b041e70
76122

77123
WORKDIR $ANDROID_HOME
78124
RUN <<EOF
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Source: https://source.chromium.org/chromium/chromium/src/+/main:chrome/installer/linux/debian/dist_package_versions.json
2+
# Keep the list sorted.
3+
libasound2t64
4+
libatk-bridge2.0-0t64
5+
libatk1.0-0t64
6+
libatspi2.0-0t64
7+
libc6
8+
libcairo2
9+
libcups2t64
10+
libdbus-1-3
11+
libdrm2
12+
libexpat1
13+
libgbm1
14+
libglib2.0-0t64
15+
libnspr4
16+
libnss3
17+
libpango-1.0-0
18+
libpangocairo-1.0-0
19+
libstdc++6
20+
libudev1
21+
libuuid1
22+
libx11-6
23+
libx11-xcb1
24+
libxcb-dri3-0
25+
libxcb1
26+
libxcomposite1
27+
libxcursor1
28+
libxdamage1
29+
libxext6
30+
libxfixes3
31+
libxi6
32+
libxkbcommon0
33+
libxrandr2
34+
libxrender1
35+
libxshmfence1
36+
libxss1
37+
libxtst6

.devcontainer/cmdline-tools-package.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,4 +138,4 @@ This is the Android Software Development Kit License Agreement
138138
14.7 The License Agreement, and your relationship with Google under the License Agreement, shall be governed by the laws of the State of California without regard to its conflict of laws provisions. You and Google agree to submit to the exclusive jurisdiction of the courts located within the county of Santa Clara, California to resolve any legal matter arising from the License Agreement. Notwithstanding this, you agree that Google shall still be allowed to apply for injunctive remedies (or an equivalent type of urgent legal relief) in any jurisdiction.
139139

140140

141-
January 16, 2019</license><localPackage path="cmdline-tools;latest" obsolete="false"><type-details xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ns5:genericDetailsType"/><revision><major>16</major><minor>0</minor></revision><display-name>Android SDK Command-line Tools (latest)</display-name><uses-license ref="android-sdk-license"/></localPackage></ns2:repository>
141+
January 16, 2019</license><localPackage path="cmdline-tools;latest" obsolete="false"><type-details xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="ns5:genericDetailsType"/><revision><major>19</major><minor>0</minor></revision><display-name>Android SDK Command-line Tools (latest)</display-name><uses-license ref="android-sdk-license"/></localPackage></ns2:repository>

.devcontainer/devcontainer.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
{
2-
"build": {
3-
"dockerfile": "./Dockerfile"
4-
},
2+
"image": "ghcr.io/ktorio/devcontainer:latest",
3+
"runArgs": [
4+
// See an explanation for this option in the Dockerfile
5+
"--platform=linux/amd64"
6+
],
57
"postStartCommand": "git config --global --add safe.directory ${containerWorkspaceFolder}",
68
// Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root
79
"remoteUser": "developer"

.github/workflows/devcontainer.yml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: DevContainer
2+
on:
3+
pull_request:
4+
branches: [ main ]
5+
paths:
6+
- .devcontainer/**
7+
- .github/workflows/devcontainer.yml
8+
- .nvmrc
9+
10+
push:
11+
branches: [ main ]
12+
paths:
13+
- .devcontainer/**
14+
- .github/workflows/devcontainer.yml
15+
- .nvmrc
16+
17+
schedule:
18+
- cron: '0 0 * * 6' # Every Saturday at 00:00
19+
20+
# Allow running manually
21+
workflow_dispatch:
22+
23+
env:
24+
REGISTRY: ghcr.io
25+
IMAGE_NAME: ktorio/devcontainer
26+
27+
jobs:
28+
build:
29+
name: Build
30+
runs-on: ubuntu-latest
31+
permissions:
32+
contents: read
33+
packages: write
34+
attestations: write
35+
id-token: write
36+
37+
env:
38+
# If the event is a "pull_request", just test the image builds successfully and do not push it
39+
PUSH_IMAGE: ${{ github.event_name != 'pull_request' }}
40+
41+
steps:
42+
- uses: actions/checkout@v5
43+
44+
- name: Set up Docker Buildx
45+
uses: docker/setup-buildx-action@v3
46+
with:
47+
platforms: linux/amd64
48+
49+
- name: Log in to the Container registry
50+
uses: docker/login-action@v3
51+
with:
52+
registry: ${{ env.REGISTRY }}
53+
username: ${{ github.actor }}
54+
password: ${{ secrets.GITHUB_TOKEN }}
55+
56+
- name: Extract metadata (tags, labels) for Docker
57+
id: meta
58+
uses: docker/metadata-action@v5
59+
with:
60+
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
61+
tags: |
62+
latest
63+
{{date 'YYYYMMDD'}}
64+
65+
- name: Extract Node.js version
66+
id: node-version
67+
run: echo "result=$(cat .nvmrc)" >> $GITHUB_OUTPUT
68+
69+
- name: Build image
70+
id: build
71+
uses: docker/build-push-action@v6
72+
with:
73+
push: ${{ env.PUSH_IMAGE }}
74+
context: .devcontainer
75+
target: dev
76+
build-args: |
77+
node_version=${{ steps.node-version.outputs.result }}
78+
platforms: linux/amd64
79+
tags: ${{ steps.meta.outputs.tags }}
80+
labels: ${{ steps.meta.outputs.labels }}
81+
82+
- name: Generate artifact attestation
83+
uses: actions/attest-build-provenance@v2
84+
if: ${{ env.PUSH_IMAGE == 'true' }}
85+
with:
86+
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
87+
subject-digest: ${{ steps.build.outputs.digest }}
88+
push-to-registry: true

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
22.18.0

build-logic/src/main/kotlin/ktorbuild.kmp.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ if (targets.hasWasmJs) configureWasmJs()
5050
if (targets.hasAndroidJvm && project.hasAndroidPlugin()) configureAndroidJvm()
5151

5252
if (targets.hasJsOrWasmJs) {
53+
configureNodeJs()
54+
5355
tasks.configureEach {
5456
if (name == "compileJsAndWasmSharedMainKotlinMetadata") enabled = false
5557
}

build-logic/src/main/kotlin/ktorbuild/targets/JsConfig.kt

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,23 @@ import ktorbuild.internal.gradle.maybeNamed
99
import ktorbuild.internal.kotlin
1010
import ktorbuild.internal.libs
1111
import org.gradle.api.Project
12+
import org.gradle.api.provider.Provider
1213
import org.gradle.kotlin.dsl.assign
1314
import org.gradle.kotlin.dsl.invoke
1415
import org.gradle.kotlin.dsl.the
1516
import org.gradle.kotlin.dsl.withType
1617
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
1718
import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsSubTargetDsl
1819
import org.jetbrains.kotlin.gradle.targets.js.dsl.KotlinJsTargetDsl
20+
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsEnvSpec
21+
import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsPlugin
1922
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnPlugin
2023
import org.jetbrains.kotlin.gradle.targets.js.yarn.YarnRootEnvSpec
24+
import org.jetbrains.kotlin.gradle.targets.wasm.nodejs.WasmNodeJsEnvSpec
25+
import org.jetbrains.kotlin.gradle.targets.wasm.nodejs.WasmNodeJsPlugin
2126
import org.jetbrains.kotlin.gradle.targets.wasm.yarn.WasmYarnPlugin
2227
import org.jetbrains.kotlin.gradle.targets.wasm.yarn.WasmYarnRootEnvSpec
28+
import org.jetbrains.kotlin.gradle.targets.web.nodejs.BaseNodeJsEnvSpec
2329
import org.jetbrains.kotlin.gradle.targets.web.yarn.BaseYarnRootEnvSpec
2430

2531
internal fun KotlinJsTargetDsl.addSubTargets(targets: KtorTargets) {
@@ -54,6 +60,7 @@ internal fun Project.configureJs() {
5460

5561
sourceSets {
5662
jsTest.dependencies {
63+
// Puppeteer is used to install Chrome for tests
5764
implementation(npm("puppeteer", libs.versions.puppeteer.get()))
5865
}
5966
}
@@ -70,6 +77,7 @@ internal fun Project.configureWasmJs() {
7077
implementation(libs.kotlinx.browser)
7178
}
7279
wasmJsTest.dependencies {
80+
// Puppeteer is used to install Chrome for tests
7381
implementation(npm("puppeteer", libs.versions.puppeteer.get()))
7482
}
7583
}
@@ -86,12 +94,36 @@ internal fun Project.configureJsTestTasks(target: String) {
8694
tasks.maybeNamed("${target}BrowserTest") { onlyIf { false } }
8795
}
8896

97+
fun Project.configureNodeJs() {
98+
@Suppress("UnstableApiUsage")
99+
val nvmrc = project.layout.settingsDirectory.file(".nvmrc")
100+
val nodeVersion = provider { nvmrc.asFile.readText().trim() }
101+
102+
plugins.withType<NodeJsPlugin> { the<NodeJsEnvSpec>().configure(nodeVersion) }
103+
plugins.withType<WasmNodeJsPlugin> { the<WasmNodeJsEnvSpec>().configure(nodeVersion) }
104+
}
105+
106+
private fun BaseNodeJsEnvSpec.configure(nodeVersion: Provider<String>) {
107+
version = nodeVersion
108+
if (isKtorDevEnvironment) download = false
109+
}
110+
89111
fun Project.configureYarn() {
90112
plugins.withType<YarnPlugin> { the<YarnRootEnvSpec>().configure() }
91113
plugins.withType<WasmYarnPlugin> { the<WasmYarnRootEnvSpec>().configure() }
92114
}
93115

94116
private fun BaseYarnRootEnvSpec.configure() {
95-
// Don't ignore scripts as we want Chrome to be installed automatically with puppeteer.
96-
ignoreScripts = false
117+
if (isKtorDevEnvironment) download = false
118+
// Don't ignore scripts if we want Chrome to be installed automatically with puppeteer.
119+
if (shouldDownloadBrowser) ignoreScripts = false
97120
}
121+
122+
// KTOR_DEV is set to `true` in the docker image used to build Ktor.
123+
// This image has Node.js and Yarn bundled so this flag disables downloading of them.
124+
private val isKtorDevEnvironment: Boolean
125+
get() = System.getenv("KTOR_DEV") == "true"
126+
127+
// If CHROME_BIN is undefined, puppeteer will install Chrome automatically
128+
private val shouldDownloadBrowser: Boolean
129+
get() = System.getenv("CHROME_BIN") == null

build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5-
import ktorbuild.targets.configureYarn
5+
import ktorbuild.targets.*
66
import ktorbuild.wirePackageJsonAggregationTasks
77

88
plugins {
@@ -16,3 +16,4 @@ println("Kotlin version: ${libs.versions.kotlin.get()}")
1616
wirePackageJsonAggregationTasks()
1717

1818
configureYarn()
19+
configureNodeJs()

karma/chrome_bin.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,7 @@ config.set({
2727
}
2828
});
2929

30-
process.env.CHROME_BIN = require('puppeteer').executablePath();
30+
// CHROME_BIN might be already defined, otherwise use puppeteer to get the path
31+
if (!process.env.CHROME_BIN) {
32+
process.env.CHROME_BIN = require('puppeteer').executablePath();
33+
}

0 commit comments

Comments
 (0)