Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,4 @@ RUN apt-get update \
&& rm -rf /var/lib/apt/lists/* \
&& npm install --global pnpm@latest-11

RUN mkdir --parents /usr/share/dotnet /usr/share/devcontainer-config/NuGet/global-packages \
&& chmod --recursive a+rwX /usr/share/dotnet /usr/share/devcontainer-config

COPY --from=dotnet /features/dotnet/ /opt/devcontainer-config/features/dotnet/
1 change: 0 additions & 1 deletion .devcontainer/compose.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
services:
devcontainer:
env_file:
- container.env
- path: ../.local/.env
required: false
build:
Expand Down
1 change: 0 additions & 1 deletion .devcontainer/container.env

This file was deleted.

4 changes: 2 additions & 2 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@
}
],
"features": {
// "ghcr.io/devcontainer-config/features/user-init:3": {},
// "ghcr.io/devcontainer-config/features/dot-config:4": {},
// "ghcr.io/devcontainer-config/features/user-init:4": {},
// "ghcr.io/devcontainer-config/features/dot-config:5": {},
// "ghcr.io/devcontainer-config/features/features": {},
"./features/src/user-init": {},
"./features/src/dot-config": {},
Expand Down
6 changes: 3 additions & 3 deletions .devcontainer/dot-config.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"git": {
"attributes": "/etc/devcontainer-config/git/attributes",
"ignore": "/etc/devcontainer-config/git/ignore"
"attributes": "/etc/devcontainer-config/dev/git/attributes",
"ignore": "/etc/devcontainer-config/dev/git/ignore"
},
"cspell": {
"cspell.json": "cspell.json"
},
"pnpm": {
"config.yaml": "/etc/devcontainer-config/pnpm/config.yaml"
"config.yaml": "/etc/devcontainer-config/dev/pnpm/config.yaml"
},
"eslint": {
"config.ts": "eslint.config.ts"
Expand Down
2 changes: 1 addition & 1 deletion .devcontainer/features/src/dot-config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Synchronize configuration files from .config/ to devcontainer workspaces.

```json
"features": {
"ghcr.io/devcontainer-config/features/dot-config:4": {}
"ghcr.io/devcontainer-config/features/dot-config:5": {}
}
```

Expand Down
2 changes: 0 additions & 2 deletions .devcontainer/features/src/dot-config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,3 @@ export const parseConfig = async (projectRoot: string) => {
}
return mappings;
};

export const cachePath = "/var/cache/devcontainer-config/dot-config";
11 changes: 2 additions & 9 deletions .devcontainer/features/src/dot-config/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "dot-config",
"version": "4.0.0",
"version": "5.0.0",
"name": ".config/",
"description": "Synchronize configuration files from .config/ to devcontainer workspaces.",
"keywords": ["dot", "config"],
Expand All @@ -13,12 +13,5 @@
},
"installsAfter": ["ghcr.io/devcontainers/features/node"],
"onCreateCommand": "dot-config sync || true",
"postStartCommand": "dot-config watch --detach",
"mounts": [
{
"type": "volume",
"source": "dot-config-cache-${devcontainerId}",
"target": "/var/cache/devcontainer-config/dot-config"
}
]
"postStartCommand": "dot-config watch --detach"
}
3 changes: 2 additions & 1 deletion .devcontainer/features/src/dot-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { packageJson } from "./package.js";
import { sync } from "./sync.js";
import { watch } from "./watch.js";

initializeLogger();
const projectRoot = process.cwd();

program.name(packageJson.name).version(packageJson.version);
Expand All @@ -17,6 +16,7 @@ program
.command("sync")
.description("perform a one-time synchronization of .config/ files")
.action(async () => {
initializeLogger();
try {
await sync(projectRoot);
} catch (error) {
Expand All @@ -33,6 +33,7 @@ program
const childProcess = $({ detached: true, stdio: "ignore" })`${packageJson.name} watch`;
childProcess.nodeChildProcess.unref();
} else {
initializeLogger();
watch(projectRoot);
}
});
Expand Down
4 changes: 0 additions & 4 deletions .devcontainer/features/src/dot-config/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import path from "node:path";

import { $ } from "execa";

import { cachePath } from "./config.js";
import { packageJson } from "./package.js";

const insertShebang = async (path: string) => {
Expand All @@ -20,12 +19,10 @@ export const install = async () => {

const workspacesPath = process.env.WORKSPACES ?? "/workspaces";
await mkdir(workspacesPath, { recursive: true });
await mkdir(cachePath, { recursive: true });
if (path.basename(import.meta.dirname) === "dist") {
// called from install.dist.sh.
const $$ = $({ stdio: "inherit", verbose: "full" });
await $$`chmod -R a=rwx ${workspacesPath}`;
await $$`chmod -R a=rwx ${cachePath}`;

await cp(import.meta.dirname, featureInstallPath, { recursive: true, verbatimSymlinks: true });
await $$`npm install --global ${featureInstallPath}`;
Expand All @@ -45,7 +42,6 @@ export const install = async () => {
},
});
await $$`chmod -R a=rwx ${workspacesPath}`;
await $$`chmod -R a=rwx ${cachePath}`;

await $$`tsc --project ${import.meta.dirname} --noEmit false --outDir .`;
await writeFile(path.resolve(tempPath, "package.json"), JSON.stringify(packageJson, null, 2));
Expand Down
12 changes: 10 additions & 2 deletions .devcontainer/features/src/dot-config/logging.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,16 @@
import { homedir } from "node:os";
import path from "node:path";

import winston from "winston";

import { cachePath } from "./config.js";
const cacheRoot = (): string => {
const xdgCacheHome = process.env.XDG_CACHE_HOME;
const cacheHome =
xdgCacheHome !== undefined && xdgCacheHome !== "" && path.isAbsolute(xdgCacheHome)
? xdgCacheHome
: path.join(homedir(), ".cache");
return path.join(cacheHome, "dot-config");
};

export const initializeLogger = () => {
winston.configure({
Expand All @@ -13,7 +21,7 @@ export const initializeLogger = () => {
handleRejections: true,
}),
new winston.transports.File({
filename: path.resolve(cachePath, "dot-config.log"),
filename: path.join(cacheRoot(), "dot-config.log"),
options: { flags: "w" },
format: winston.format.combine(winston.format.timestamp(), winston.format.json()),
handleExceptions: true,
Expand Down
12 changes: 12 additions & 0 deletions .devcontainer/features/src/user-init/NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
## Notes

Creates `remoteUser` with UID 1000.

Each user gets its own subtree under the four volume-mounted XDG roots:

| Variable | Directory |
| ----------------- | --------------------------------------- |
| `XDG_CONFIG_HOME` | `/etc/devcontainer-config/<user>` |
| `XDG_CACHE_HOME` | `/var/cache/devcontainer-config/<user>` |
| `XDG_DATA_HOME` | `/usr/share/devcontainer-config/<user>` |
| `XDG_STATE_HOME` | `/var/lib/devcontainer-config/<user>` |
15 changes: 14 additions & 1 deletion .devcontainer/features/src/user-init/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,23 @@ Rename user with UID 1000 to remoteUser and setup XDG base directories.

```json
"features": {
"ghcr.io/devcontainer-config/features/user-init:3": {}
"ghcr.io/devcontainer-config/features/user-init:4": {}
}
```

## Notes

Creates `remoteUser` with UID 1000.

Each user gets its own subtree under the four volume-mounted XDG roots:

| Variable | Directory |
| ----------------- | --------------------------------------- |
| `XDG_CONFIG_HOME` | `/etc/devcontainer-config/<user>` |
| `XDG_CACHE_HOME` | `/var/cache/devcontainer-config/<user>` |
| `XDG_DATA_HOME` | `/usr/share/devcontainer-config/<user>` |
| `XDG_STATE_HOME` | `/var/lib/devcontainer-config/<user>` |

---

_Note: This file was auto-generated from the [devcontainer-feature.json](https://github.com/devcontainer-config/features/blob/main/.devcontainer/features/src/user-init/devcontainer-feature.json). Add additional notes to a `NOTES.md`._
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
{
"id": "user-init",
"version": "3.0.0",
"version": "4.0.0",
"name": "Initialize User",
"description": "Rename user with UID 1000 to remoteUser and setup XDG base directories.",
"keywords": ["initialize", "user", "username", "uid", "common-utils"],
"installsAfter": ["ghcr.io/devcontainers/features/common-utils"],
"containerEnv": {
"XDG_CONFIG_HOME": "/etc/devcontainer-config",
"XDG_CACHE_HOME": "/var/cache/devcontainer-config",
"XDG_DATA_HOME": "/usr/share/devcontainer-config",
"XDG_STATE_HOME": "/var/lib/devcontainer-config",
"XDG_DATA_DIRS": "/usr/local/share:/usr/share",
"XDG_CONFIG_DIRS": "/etc/xdg"
},
Expand Down
23 changes: 17 additions & 6 deletions .devcontainer/features/src/user-init/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,21 @@ chmod ug=r,o= "/etc/sudoers.d/${_REMOTE_USER}"

# Ideally these folders would be in ${HOME}, but there's no way to mount volumes base on remoteUser.
# See https://github.com/devcontainers/spec/issues/220
XDG_CONFIG_HOME=/etc/devcontainer-config
XDG_CACHE_HOME=/var/cache/devcontainer-config
XDG_DATA_HOME=/usr/share/devcontainer-config
XDG_STATE_HOME=/var/lib/devcontainer-config
mkdir --parents "${XDG_CONFIG_HOME}" "${XDG_CACHE_HOME}" "${XDG_DATA_HOME}" "${XDG_STATE_HOME}"
chmod -R a+rwx "${XDG_CONFIG_HOME}" "${XDG_CACHE_HOME}" "${XDG_DATA_HOME}" "${XDG_STATE_HOME}"
XDG_CONFIG_HOME_ROOT=/etc/devcontainer-config
XDG_CACHE_HOME_ROOT=/var/cache/devcontainer-config
XDG_DATA_HOME_ROOT=/usr/share/devcontainer-config
XDG_STATE_HOME_ROOT=/var/lib/devcontainer-config
mkdir --parents "${XDG_CONFIG_HOME_ROOT}" "${XDG_CACHE_HOME_ROOT}" "${XDG_DATA_HOME_ROOT}" "${XDG_STATE_HOME_ROOT}"
chmod -R a+rwx "${XDG_CONFIG_HOME_ROOT}" "${XDG_CACHE_HOME_ROOT}" "${XDG_DATA_HOME_ROOT}" "${XDG_STATE_HOME_ROOT}"

cat > /etc/profile.d/00-xdg.sh << EOF
XDG_CONFIG_HOME="${XDG_CONFIG_HOME_ROOT}/\$(id -un)"
XDG_CACHE_HOME="${XDG_CACHE_HOME_ROOT}/\$(id -un)"
XDG_DATA_HOME="${XDG_DATA_HOME_ROOT}/\$(id -un)"
XDG_STATE_HOME="${XDG_STATE_HOME_ROOT}/\$(id -un)"
export XDG_CONFIG_HOME XDG_CACHE_HOME XDG_DATA_HOME XDG_STATE_HOME
mkdir -p "\$XDG_CONFIG_HOME" "\$XDG_CACHE_HOME" "\$XDG_DATA_HOME" "\$XDG_STATE_HOME"
EOF
chmod 0644 /etc/profile.d/00-xdg.sh

chown --recursive "${_REMOTE_USER}:${_REMOTE_USER}" "${HOME}"
10 changes: 6 additions & 4 deletions .devcontainer/features/test/user-init/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@ test("remote user home", async () => {
});

test("XDG base directories", async () => {
const dirs = {
const { username } = userInfo();
const roots = {
XDG_CONFIG_HOME: "/etc/devcontainer-config",
XDG_CACHE_HOME: "/var/cache/devcontainer-config",
XDG_DATA_HOME: "/usr/share/devcontainer-config",
XDG_STATE_HOME: "/var/lib/devcontainer-config",
} as const;

for (const [name, dir] of Object.entries(dirs)) {
expect(process.env[name]).toBe(dir);
await access(dir, R_OK | W_OK | X_OK);
for (const [name, root] of Object.entries(roots)) {
const directory = `${root}/${username}`;
expect(process.env[name]).toBe(directory);
await access(directory, R_OK | W_OK | X_OK);
}
});
2 changes: 1 addition & 1 deletion ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Most base images for devcontainers comes with a non-root user with UID 1000. Thi

A non-root user can be created via the `ghcr.io/devcontainers/features/common-utils` feature, but it cannot override an existing user with UID 1000. A UID 1000 user in devcontainer is necessary for sensible file permission behavior in the host.

According to Docker behavior, directories mounted as named volumes are owned by root, unless created up-front with the correct ownership. This feature creates default XDG base directories for `remoteUser` so that they have the correct ownership when mounted as named volumes.
According to Docker behavior, directories mounted as named volumes are owned by root, unless created up-front with the correct ownership. This feature creates XDG base directories for `remoteUser` so that they have the correct ownership when mounted as named volumes.

Use cases:

Expand Down
2 changes: 1 addition & 1 deletion scripts/verify/dotnet/lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { login } from "@/scripts/tasks/features/dotnet/registry.js";
import type { ComponentImage } from "./selection.js";

const baseImage = "mcr.microsoft.com/devcontainers/base:debian";
const remoteUser = "verify";
export const remoteUser = "verify";

const $$docker = $({ reject: false, stdin: "ignore", stderr: "ignore" });
const $$capture = $({ reject: false, stdio: ["ignore", "pipe", "pipe"], verbose: "full", cwd: projectRoot });
Expand Down
15 changes: 12 additions & 3 deletions scripts/verify/dotnet/selection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readFile } from "node:fs/promises";
import path from "node:path";
import path, { posix } from "node:path";

import { z } from "zod";

Expand All @@ -8,9 +8,14 @@ import type { Component, FeatureConfig } from "@/scripts/tasks/features/dotnet/g
import { configPath, readConfig } from "@/scripts/tasks/features/dotnet/generateConfig.js";
import { canonicalTag, imageRef } from "@/scripts/tasks/features/dotnet/tags.js";

import { remoteUser } from "./lifecycle.js";

const userInitFeaturePath = path.resolve(projectRoot, ".devcontainer/features/src/user-init/devcontainer-feature.json");
const dataRootSource = "XDG_DATA_HOME-${devcontainerId}";

const userInitFeatureSchema = z.object({ containerEnv: z.object({ XDG_DATA_HOME: z.string() }) });
const userInitFeatureSchema = z.object({
mounts: z.array(z.object({ source: z.string(), target: z.string() })),
});

export interface ComponentImage {
component: Component;
Expand Down Expand Up @@ -57,7 +62,11 @@ const readGlobalPackagesPath = async (): Promise<string> => {
if (!result.success) {
throw new Error(`${userInitFeaturePath}: ${z.prettifyError(result.error)}`);
}
return `${result.data.containerEnv.XDG_DATA_HOME}/NuGet/global-packages`;
const dataRoot = result.data.mounts.find((mount) => mount.source === dataRootSource)?.target;
if (dataRoot === undefined) {
throw new Error(`${userInitFeaturePath}: no mounts entry with source ${dataRootSource}`);
}
return posix.join(dataRoot, remoteUser, "NuGet/global-packages");
};

const derive = async (channel: string, prefix: string): Promise<Selection> => {
Expand Down
Loading