docker2wslc 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docker2wslc-0.1.0/.gitignore +14 -0
- docker2wslc-0.1.0/LICENSE +21 -0
- docker2wslc-0.1.0/PKG-INFO +161 -0
- docker2wslc-0.1.0/README.md +130 -0
- docker2wslc-0.1.0/pyproject.toml +48 -0
- docker2wslc-0.1.0/src/docker2wslc/__init__.py +21 -0
- docker2wslc-0.1.0/src/docker2wslc/cli.py +233 -0
- docker2wslc-0.1.0/src/docker2wslc/compose.py +167 -0
- docker2wslc-0.1.0/src/docker2wslc/rules.json +167 -0
- docker2wslc-0.1.0/src/docker2wslc/translate.py +267 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WSL Containers
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: docker2wslc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Translate Docker commands and Compose files to wslc, the native WSL container runtime
|
|
5
|
+
Project-URL: Homepage, https://wslcontainers.com
|
|
6
|
+
Project-URL: Documentation, https://wslcontainers.com/reference/cheatsheet/
|
|
7
|
+
Project-URL: Repository, https://github.com/ylwl1997/docker2wslc
|
|
8
|
+
Project-URL: Issues, https://github.com/ylwl1997/docker2wslc/issues
|
|
9
|
+
Author: WSL Containers
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: compose,containers,devcontainer,docker,windows,wsl,wslc
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Operating System :: OS Independent
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
25
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
26
|
+
Classifier: Topic :: System :: Systems Administration
|
|
27
|
+
Requires-Python: >=3.9
|
|
28
|
+
Provides-Extra: yaml
|
|
29
|
+
Requires-Dist: pyyaml>=6.0; extra == 'yaml'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# docker2wslc
|
|
33
|
+
|
|
34
|
+
Translate Docker commands, Compose files and dev container configs to
|
|
35
|
+
[**wslc**](https://wslcontainers.com) — the native Linux container runtime built into the
|
|
36
|
+
Windows Subsystem for Linux, which runs containers on Windows 11 without Docker Desktop.
|
|
37
|
+
|
|
38
|
+
No network calls, no daemon, no telemetry. Pure rule-driven translation.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install docker2wslc # translation only
|
|
42
|
+
pip install 'docker2wslc[yaml]' # + Compose analysis
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Convert a command
|
|
46
|
+
|
|
47
|
+
```console
|
|
48
|
+
$ docker2wslc convert docker run --gpus all --restart always -p 8080:80 nginx
|
|
49
|
+
wslc run --device nvidia.com/gpu=all -p 8080:80 nginx
|
|
50
|
+
|
|
51
|
+
Migration notes
|
|
52
|
+
WARN Restart policies are not implemented in the wslc preview. Flag dropped — use a
|
|
53
|
+
Windows scheduled task or a wrapper script for auto-restart.
|
|
54
|
+
INFO GPU access in wslc goes through the Container Device Interface. Use
|
|
55
|
+
`--device nvidia.com/gpu=all` instead of `--gpus`.
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Commands come from arguments, a file, or stdin:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
docker2wslc convert docker ps -a
|
|
62
|
+
docker2wslc convert --file deploy.sh
|
|
63
|
+
cat deploy.sh | docker2wslc convert
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Analyse a Compose file
|
|
67
|
+
|
|
68
|
+
wslc has **no Compose runtime**. This turns each service into an equivalent `wslc run`
|
|
69
|
+
and tells you exactly what cannot be carried over:
|
|
70
|
+
|
|
71
|
+
```console
|
|
72
|
+
$ docker2wslc compose docker-compose.yml
|
|
73
|
+
# wslc has no Compose runtime. Equivalent commands:
|
|
74
|
+
|
|
75
|
+
wslc volume create pgdata
|
|
76
|
+
wslc network create backend
|
|
77
|
+
|
|
78
|
+
# service: web
|
|
79
|
+
wslc run -d --name web -e NGINX_HOST=localhost -p 8080:80 --network backend nginx:alpine
|
|
80
|
+
x depends_on: No dependency ordering. Start services in order yourself and add readiness waits.
|
|
81
|
+
x restart: No restart policies in the wslc preview.
|
|
82
|
+
|
|
83
|
+
# service: db
|
|
84
|
+
wslc run -d --name db -v pgdata:/var/lib/postgresql/data postgres:16-alpine
|
|
85
|
+
x healthcheck: wslc has no healthcheck support; depends_on conditions relying on it cannot work.
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Lint a repository
|
|
89
|
+
|
|
90
|
+
Scans for `docker-compose.y*ml`, `compose.y*ml` and `devcontainer.json`:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
docker2wslc lint .
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Use in CI
|
|
97
|
+
|
|
98
|
+
Exit codes are meaningful, so this works as a gate:
|
|
99
|
+
|
|
100
|
+
| Code | Meaning |
|
|
101
|
+
|------|---------|
|
|
102
|
+
| `0` | Fully compatible |
|
|
103
|
+
| `1` | Degraded — flags dropped or rewritten, still runnable |
|
|
104
|
+
| `2` | Unmigratable — Compose, Swarm, buildx, or a parse failure |
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
- run: pip install 'docker2wslc[yaml]'
|
|
108
|
+
- run: docker2wslc lint . # fails the job on exit 2
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Python API
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from docker2wslc import translate, analyse
|
|
115
|
+
|
|
116
|
+
result = translate("docker run --platform linux/amd64 -it ubuntu bash")
|
|
117
|
+
print(result.output) # wslc run -it ubuntu bash
|
|
118
|
+
print(result.exit_code) # 1
|
|
119
|
+
for note in result.notes:
|
|
120
|
+
print(note.severity, note.text)
|
|
121
|
+
|
|
122
|
+
report = analyse(open("docker-compose.yml").read())
|
|
123
|
+
print(report.as_dict())
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`--json` on any subcommand gives the same structure for shell pipelines.
|
|
127
|
+
|
|
128
|
+
## What wslc cannot do
|
|
129
|
+
|
|
130
|
+
Worth knowing before you migrate. These are runtime limitations, not gaps in this tool:
|
|
131
|
+
|
|
132
|
+
- **No Compose runtime** — translate services by hand, see the [migration guide](https://wslcontainers.com/guides/compose/)
|
|
133
|
+
- **No restart policies** — use a scheduled task
|
|
134
|
+
- **No `--platform`** — host architecture only
|
|
135
|
+
- **No healthchecks** — so `depends_on` conditions cannot work
|
|
136
|
+
- **No Docker socket or Engine API** — [Testcontainers, Portainer and act cannot attach](https://wslcontainers.com/guides/testcontainers/)
|
|
137
|
+
- **No buildx / bake** — single-platform `wslc build` only
|
|
138
|
+
- **GPU via CDI** — `--device nvidia.com/gpu=all`, not `--gpus all`
|
|
139
|
+
|
|
140
|
+
CLI-driven tooling ports to wslc. API-driven tooling does not. That single distinction
|
|
141
|
+
explains most migration surprises — including why
|
|
142
|
+
[VS Code Dev Containers *does* work](https://wslcontainers.com/guides/vscode-dev-containers/)
|
|
143
|
+
once you set `dev.containers.dockerPath` to `wslc`.
|
|
144
|
+
|
|
145
|
+
## Docs
|
|
146
|
+
|
|
147
|
+
- Interactive converter — <https://wslcontainers.com>
|
|
148
|
+
- Command cheat sheet — <https://wslcontainers.com/reference/cheatsheet/>
|
|
149
|
+
- Install guide — <https://wslcontainers.com/guides/install/>
|
|
150
|
+
- wslc vs Docker Desktop — <https://wslcontainers.com/guides/vs-docker-desktop/>
|
|
151
|
+
|
|
152
|
+
## Accuracy
|
|
153
|
+
|
|
154
|
+
Rules target the **2026-07 wslc public preview** and live in a single
|
|
155
|
+
[`rules.json`](https://github.com/ylwl1997/docker2wslc/blob/main/rules.json) shared by the
|
|
156
|
+
Python package, the npm CLI, the MCP server and the VS Code extension. wslc is a moving
|
|
157
|
+
target; if you hit a mapping that is wrong,
|
|
158
|
+
[open an issue](https://github.com/ylwl1997/docker2wslc/issues) with the command and the
|
|
159
|
+
actual `wslc` output.
|
|
160
|
+
|
|
161
|
+
MIT licensed.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# docker2wslc
|
|
2
|
+
|
|
3
|
+
Translate Docker commands, Compose files and dev container configs to
|
|
4
|
+
[**wslc**](https://wslcontainers.com) — the native Linux container runtime built into the
|
|
5
|
+
Windows Subsystem for Linux, which runs containers on Windows 11 without Docker Desktop.
|
|
6
|
+
|
|
7
|
+
No network calls, no daemon, no telemetry. Pure rule-driven translation.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install docker2wslc # translation only
|
|
11
|
+
pip install 'docker2wslc[yaml]' # + Compose analysis
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Convert a command
|
|
15
|
+
|
|
16
|
+
```console
|
|
17
|
+
$ docker2wslc convert docker run --gpus all --restart always -p 8080:80 nginx
|
|
18
|
+
wslc run --device nvidia.com/gpu=all -p 8080:80 nginx
|
|
19
|
+
|
|
20
|
+
Migration notes
|
|
21
|
+
WARN Restart policies are not implemented in the wslc preview. Flag dropped — use a
|
|
22
|
+
Windows scheduled task or a wrapper script for auto-restart.
|
|
23
|
+
INFO GPU access in wslc goes through the Container Device Interface. Use
|
|
24
|
+
`--device nvidia.com/gpu=all` instead of `--gpus`.
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Commands come from arguments, a file, or stdin:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
docker2wslc convert docker ps -a
|
|
31
|
+
docker2wslc convert --file deploy.sh
|
|
32
|
+
cat deploy.sh | docker2wslc convert
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Analyse a Compose file
|
|
36
|
+
|
|
37
|
+
wslc has **no Compose runtime**. This turns each service into an equivalent `wslc run`
|
|
38
|
+
and tells you exactly what cannot be carried over:
|
|
39
|
+
|
|
40
|
+
```console
|
|
41
|
+
$ docker2wslc compose docker-compose.yml
|
|
42
|
+
# wslc has no Compose runtime. Equivalent commands:
|
|
43
|
+
|
|
44
|
+
wslc volume create pgdata
|
|
45
|
+
wslc network create backend
|
|
46
|
+
|
|
47
|
+
# service: web
|
|
48
|
+
wslc run -d --name web -e NGINX_HOST=localhost -p 8080:80 --network backend nginx:alpine
|
|
49
|
+
x depends_on: No dependency ordering. Start services in order yourself and add readiness waits.
|
|
50
|
+
x restart: No restart policies in the wslc preview.
|
|
51
|
+
|
|
52
|
+
# service: db
|
|
53
|
+
wslc run -d --name db -v pgdata:/var/lib/postgresql/data postgres:16-alpine
|
|
54
|
+
x healthcheck: wslc has no healthcheck support; depends_on conditions relying on it cannot work.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Lint a repository
|
|
58
|
+
|
|
59
|
+
Scans for `docker-compose.y*ml`, `compose.y*ml` and `devcontainer.json`:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
docker2wslc lint .
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Use in CI
|
|
66
|
+
|
|
67
|
+
Exit codes are meaningful, so this works as a gate:
|
|
68
|
+
|
|
69
|
+
| Code | Meaning |
|
|
70
|
+
|------|---------|
|
|
71
|
+
| `0` | Fully compatible |
|
|
72
|
+
| `1` | Degraded — flags dropped or rewritten, still runnable |
|
|
73
|
+
| `2` | Unmigratable — Compose, Swarm, buildx, or a parse failure |
|
|
74
|
+
|
|
75
|
+
```yaml
|
|
76
|
+
- run: pip install 'docker2wslc[yaml]'
|
|
77
|
+
- run: docker2wslc lint . # fails the job on exit 2
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Python API
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from docker2wslc import translate, analyse
|
|
84
|
+
|
|
85
|
+
result = translate("docker run --platform linux/amd64 -it ubuntu bash")
|
|
86
|
+
print(result.output) # wslc run -it ubuntu bash
|
|
87
|
+
print(result.exit_code) # 1
|
|
88
|
+
for note in result.notes:
|
|
89
|
+
print(note.severity, note.text)
|
|
90
|
+
|
|
91
|
+
report = analyse(open("docker-compose.yml").read())
|
|
92
|
+
print(report.as_dict())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`--json` on any subcommand gives the same structure for shell pipelines.
|
|
96
|
+
|
|
97
|
+
## What wslc cannot do
|
|
98
|
+
|
|
99
|
+
Worth knowing before you migrate. These are runtime limitations, not gaps in this tool:
|
|
100
|
+
|
|
101
|
+
- **No Compose runtime** — translate services by hand, see the [migration guide](https://wslcontainers.com/guides/compose/)
|
|
102
|
+
- **No restart policies** — use a scheduled task
|
|
103
|
+
- **No `--platform`** — host architecture only
|
|
104
|
+
- **No healthchecks** — so `depends_on` conditions cannot work
|
|
105
|
+
- **No Docker socket or Engine API** — [Testcontainers, Portainer and act cannot attach](https://wslcontainers.com/guides/testcontainers/)
|
|
106
|
+
- **No buildx / bake** — single-platform `wslc build` only
|
|
107
|
+
- **GPU via CDI** — `--device nvidia.com/gpu=all`, not `--gpus all`
|
|
108
|
+
|
|
109
|
+
CLI-driven tooling ports to wslc. API-driven tooling does not. That single distinction
|
|
110
|
+
explains most migration surprises — including why
|
|
111
|
+
[VS Code Dev Containers *does* work](https://wslcontainers.com/guides/vscode-dev-containers/)
|
|
112
|
+
once you set `dev.containers.dockerPath` to `wslc`.
|
|
113
|
+
|
|
114
|
+
## Docs
|
|
115
|
+
|
|
116
|
+
- Interactive converter — <https://wslcontainers.com>
|
|
117
|
+
- Command cheat sheet — <https://wslcontainers.com/reference/cheatsheet/>
|
|
118
|
+
- Install guide — <https://wslcontainers.com/guides/install/>
|
|
119
|
+
- wslc vs Docker Desktop — <https://wslcontainers.com/guides/vs-docker-desktop/>
|
|
120
|
+
|
|
121
|
+
## Accuracy
|
|
122
|
+
|
|
123
|
+
Rules target the **2026-07 wslc public preview** and live in a single
|
|
124
|
+
[`rules.json`](https://github.com/ylwl1997/docker2wslc/blob/main/rules.json) shared by the
|
|
125
|
+
Python package, the npm CLI, the MCP server and the VS Code extension. wslc is a moving
|
|
126
|
+
target; if you hit a mapping that is wrong,
|
|
127
|
+
[open an issue](https://github.com/ylwl1997/docker2wslc/issues) with the command and the
|
|
128
|
+
actual `wslc` output.
|
|
129
|
+
|
|
130
|
+
MIT licensed.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.21"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "docker2wslc"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Translate Docker commands and Compose files to wslc, the native WSL container runtime"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "WSL Containers" }]
|
|
13
|
+
keywords = ["docker", "wsl", "wslc", "containers", "windows", "compose", "devcontainer"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: Microsoft :: Windows",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Software Development :: Build Tools",
|
|
28
|
+
"Topic :: System :: Systems Administration",
|
|
29
|
+
]
|
|
30
|
+
dependencies = []
|
|
31
|
+
|
|
32
|
+
[project.optional-dependencies]
|
|
33
|
+
yaml = ["PyYAML>=6.0"]
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://wslcontainers.com"
|
|
37
|
+
Documentation = "https://wslcontainers.com/reference/cheatsheet/"
|
|
38
|
+
Repository = "https://github.com/ylwl1997/docker2wslc"
|
|
39
|
+
Issues = "https://github.com/ylwl1997/docker2wslc/issues"
|
|
40
|
+
|
|
41
|
+
[project.scripts]
|
|
42
|
+
docker2wslc = "docker2wslc.cli:main"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/docker2wslc"]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.sdist]
|
|
48
|
+
include = ["src/", "README.md", "LICENSE"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""docker2wslc — translate Docker commands and Compose files to wslc.
|
|
2
|
+
|
|
3
|
+
wslc is the native Linux container runtime in the Windows Subsystem for Linux.
|
|
4
|
+
Docs and an interactive converter: https://wslcontainers.com
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
from .compose import ComposeReport, analyse
|
|
10
|
+
from .translate import Note, Result, load_rules, translate, translate_line
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"__version__",
|
|
14
|
+
"translate",
|
|
15
|
+
"translate_line",
|
|
16
|
+
"analyse",
|
|
17
|
+
"load_rules",
|
|
18
|
+
"Result",
|
|
19
|
+
"Note",
|
|
20
|
+
"ComposeReport",
|
|
21
|
+
]
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""docker2wslc command line interface."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .compose import analyse
|
|
12
|
+
from .translate import RULES, translate
|
|
13
|
+
|
|
14
|
+
SITE = RULES["links"]["site"]
|
|
15
|
+
|
|
16
|
+
C = {
|
|
17
|
+
"reset": "\033[0m", "dim": "\033[2m", "bold": "\033[1m",
|
|
18
|
+
"red": "\033[31m", "yellow": "\033[33m", "cyan": "\033[36m", "green": "\033[32m",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _paint(enabled: bool):
|
|
23
|
+
if enabled:
|
|
24
|
+
return lambda s, c: f"{C[c]}{s}{C['reset']}"
|
|
25
|
+
return lambda s, c: s
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _read(source: str | None) -> str:
|
|
29
|
+
if source in (None, "-"):
|
|
30
|
+
return sys.stdin.read()
|
|
31
|
+
return Path(source).read_text(encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
SEV_COLOR = {"error": "red", "warn": "yellow", "info": "cyan"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def cmd_convert(args: argparse.Namespace) -> int:
|
|
38
|
+
text = " ".join(args.command) if args.command else _read(args.file)
|
|
39
|
+
res = translate(text)
|
|
40
|
+
if args.json:
|
|
41
|
+
print(json.dumps(res.as_dict(), indent=2))
|
|
42
|
+
return res.exit_code
|
|
43
|
+
|
|
44
|
+
paint = _paint(not args.no_color and sys.stdout.isatty())
|
|
45
|
+
print(res.output)
|
|
46
|
+
if res.notes and not args.quiet:
|
|
47
|
+
print(file=sys.stderr)
|
|
48
|
+
print(paint("Migration notes", "bold"), file=sys.stderr)
|
|
49
|
+
for note in res.notes:
|
|
50
|
+
tag = paint(note.severity.upper().ljust(5), SEV_COLOR.get(note.severity, "dim"))
|
|
51
|
+
print(f" {tag} {note.text}", file=sys.stderr)
|
|
52
|
+
if res.compose_hit:
|
|
53
|
+
print(f"\n See {RULES['links']['compose']}", file=sys.stderr)
|
|
54
|
+
return res.exit_code
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def cmd_compose(args: argparse.Namespace) -> int:
|
|
58
|
+
report = analyse(_read(args.file))
|
|
59
|
+
if args.json:
|
|
60
|
+
print(json.dumps(report.as_dict(), indent=2))
|
|
61
|
+
return report.exit_code
|
|
62
|
+
|
|
63
|
+
paint = _paint(not args.no_color and sys.stdout.isatty())
|
|
64
|
+
if report.parse_error:
|
|
65
|
+
print(paint(f"error: {report.parse_error}", "red"), file=sys.stderr)
|
|
66
|
+
return report.exit_code
|
|
67
|
+
|
|
68
|
+
print(paint("# wslc has no Compose runtime. Equivalent commands:", "dim"))
|
|
69
|
+
if report.prelude:
|
|
70
|
+
print()
|
|
71
|
+
for line in report.prelude:
|
|
72
|
+
print(line)
|
|
73
|
+
for svc in report.services:
|
|
74
|
+
print()
|
|
75
|
+
print(paint(f"# service: {svc.name}", "bold"))
|
|
76
|
+
print(svc.command)
|
|
77
|
+
for key, note in svc.partial:
|
|
78
|
+
print(paint(f" ! {key}: {note}", "yellow"), file=sys.stderr)
|
|
79
|
+
for key, note in svc.missing:
|
|
80
|
+
print(paint(f" x {key}: {note}", "red"), file=sys.stderr)
|
|
81
|
+
if svc.unknown:
|
|
82
|
+
print(paint(f" ? unrecognised keys: {', '.join(svc.unknown)}", "dim"), file=sys.stderr)
|
|
83
|
+
print(f"\n{paint('Guide:', 'dim')} {RULES['links']['compose']}", file=sys.stderr)
|
|
84
|
+
return report.exit_code
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
LINT_TARGETS = ("docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def cmd_lint(args: argparse.Namespace) -> int:
|
|
91
|
+
root = Path(args.path)
|
|
92
|
+
paint = _paint(not args.no_color and sys.stdout.isatty())
|
|
93
|
+
worst = 0
|
|
94
|
+
found = False
|
|
95
|
+
|
|
96
|
+
if root.is_file():
|
|
97
|
+
candidates = [root]
|
|
98
|
+
else:
|
|
99
|
+
# rglob("*") does not descend into dot-directories, so the standard
|
|
100
|
+
# .devcontainer/devcontainer.json location must be globbed explicitly.
|
|
101
|
+
patterns = [f"**/{name}" for name in LINT_TARGETS]
|
|
102
|
+
patterns += ["**/devcontainer.json", ".devcontainer/**/devcontainer.json"]
|
|
103
|
+
seen: set[Path] = set()
|
|
104
|
+
candidates = []
|
|
105
|
+
for pattern in patterns:
|
|
106
|
+
for path in root.glob(pattern):
|
|
107
|
+
if not path.is_file():
|
|
108
|
+
continue
|
|
109
|
+
if "node_modules" in path.parts or ".git" in path.parts:
|
|
110
|
+
continue
|
|
111
|
+
resolved = path.resolve()
|
|
112
|
+
if resolved in seen:
|
|
113
|
+
continue
|
|
114
|
+
seen.add(resolved)
|
|
115
|
+
candidates.append(path)
|
|
116
|
+
candidates.sort()
|
|
117
|
+
|
|
118
|
+
for path in candidates:
|
|
119
|
+
found = True
|
|
120
|
+
rel = path.relative_to(root) if root.is_dir() else path
|
|
121
|
+
if path.name == "devcontainer.json":
|
|
122
|
+
code = _lint_devcontainer(path, rel, paint)
|
|
123
|
+
else:
|
|
124
|
+
report = analyse(path.read_text(encoding="utf-8"))
|
|
125
|
+
code = report.exit_code
|
|
126
|
+
issues = [(k, n, "x") for s in report.services for k, n in s.missing]
|
|
127
|
+
issues += [(k, n, "!") for s in report.services for k, n in s.partial]
|
|
128
|
+
if issues:
|
|
129
|
+
print(paint(str(rel), "bold"))
|
|
130
|
+
for key, note, mark in issues:
|
|
131
|
+
col = "red" if mark == "x" else "yellow"
|
|
132
|
+
print(paint(f" {mark} {key}: {note}", col))
|
|
133
|
+
worst = max(worst, code)
|
|
134
|
+
|
|
135
|
+
if not found:
|
|
136
|
+
print("No compose or devcontainer files found.", file=sys.stderr)
|
|
137
|
+
return 0
|
|
138
|
+
if worst == 0:
|
|
139
|
+
print(paint("All checked files are wslc-compatible.", "green"))
|
|
140
|
+
return worst
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _lint_devcontainer(path: Path, rel: Path, paint) -> int:
|
|
144
|
+
keys = RULES["devcontainer"]["keys"]
|
|
145
|
+
try:
|
|
146
|
+
raw = path.read_text(encoding="utf-8")
|
|
147
|
+
# devcontainer.json permits // comments
|
|
148
|
+
stripped = "\n".join(
|
|
149
|
+
line for line in raw.splitlines() if not line.strip().startswith("//")
|
|
150
|
+
)
|
|
151
|
+
doc = json.loads(stripped)
|
|
152
|
+
except Exception as exc:
|
|
153
|
+
print(paint(f"{rel}: cannot parse ({exc})", "red"))
|
|
154
|
+
return 2
|
|
155
|
+
code = 0
|
|
156
|
+
header = False
|
|
157
|
+
for key in doc:
|
|
158
|
+
spec = keys.get(key)
|
|
159
|
+
if not spec or spec["status"] == "ok":
|
|
160
|
+
continue
|
|
161
|
+
if not header:
|
|
162
|
+
print(paint(str(rel), "bold"))
|
|
163
|
+
header = True
|
|
164
|
+
if spec["status"] == "missing":
|
|
165
|
+
print(paint(f" x {key}: {spec.get('note','unsupported')}", "red"))
|
|
166
|
+
code = 2
|
|
167
|
+
else:
|
|
168
|
+
print(paint(f" ! {key}: {spec.get('note','differs')}", "yellow"))
|
|
169
|
+
code = max(code, 1)
|
|
170
|
+
return code
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
174
|
+
p = argparse.ArgumentParser(
|
|
175
|
+
prog="docker2wslc",
|
|
176
|
+
description=f"Translate Docker commands and Compose files to wslc. Docs: {SITE}",
|
|
177
|
+
epilog="Exit codes: 0 clean, 1 degraded, 2 unmigratable.",
|
|
178
|
+
)
|
|
179
|
+
p.add_argument("--version", action="version", version=f"docker2wslc {__version__}")
|
|
180
|
+
p.add_argument("--no-color", action="store_true", help="disable ANSI colour")
|
|
181
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
182
|
+
|
|
183
|
+
c = sub.add_parser("convert", help="translate a docker command or script")
|
|
184
|
+
c.add_argument("command", nargs="*", help="docker command (or use --file/stdin)")
|
|
185
|
+
c.add_argument("-f", "--file", help="read commands from a file, - for stdin")
|
|
186
|
+
c.add_argument("--json", action="store_true", help="machine-readable output")
|
|
187
|
+
c.add_argument("-q", "--quiet", action="store_true", help="suppress migration notes")
|
|
188
|
+
c.add_argument("--no-color", action="store_true", help=argparse.SUPPRESS)
|
|
189
|
+
c.set_defaults(func=cmd_convert)
|
|
190
|
+
|
|
191
|
+
m = sub.add_parser("compose", help="analyse a compose file for wslc migration")
|
|
192
|
+
m.add_argument("file", nargs="?", default="docker-compose.yml")
|
|
193
|
+
m.add_argument("--json", action="store_true", help="machine-readable output")
|
|
194
|
+
m.add_argument("--no-color", action="store_true", help=argparse.SUPPRESS)
|
|
195
|
+
m.set_defaults(func=cmd_compose)
|
|
196
|
+
|
|
197
|
+
l = sub.add_parser("lint", help="scan a repo for wslc incompatibilities")
|
|
198
|
+
l.add_argument("path", nargs="?", default=".")
|
|
199
|
+
l.add_argument("--no-color", action="store_true", help=argparse.SUPPRESS)
|
|
200
|
+
l.set_defaults(func=cmd_lint)
|
|
201
|
+
return p
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def main(argv: list[str] | None = None) -> int:
|
|
205
|
+
raw = list(sys.argv[1:] if argv is None else argv)
|
|
206
|
+
|
|
207
|
+
# `convert docker run --gpus all ...` — everything after the first `docker`/`podman`
|
|
208
|
+
# token belongs to the command being translated, not to argparse. Without this,
|
|
209
|
+
# argparse rejects `--gpus`/`--platform`/`-p` as unrecognised options.
|
|
210
|
+
passthrough: list[str] = []
|
|
211
|
+
for i, tok in enumerate(raw):
|
|
212
|
+
if tok in ("docker", "podman", "sudo"):
|
|
213
|
+
passthrough = raw[i:]
|
|
214
|
+
raw = raw[:i]
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
parser = build_parser()
|
|
218
|
+
args = parser.parse_args(raw)
|
|
219
|
+
if passthrough:
|
|
220
|
+
args.command = passthrough
|
|
221
|
+
try:
|
|
222
|
+
return args.func(args)
|
|
223
|
+
except FileNotFoundError as exc:
|
|
224
|
+
print(f"error: {exc.filename}: no such file", file=sys.stderr)
|
|
225
|
+
return 2
|
|
226
|
+
except BrokenPipeError: # pragma: no cover
|
|
227
|
+
return 0
|
|
228
|
+
except KeyboardInterrupt: # pragma: no cover
|
|
229
|
+
return 130
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == "__main__": # pragma: no cover
|
|
233
|
+
sys.exit(main())
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Compose migration analyser: docker-compose.yml -> wslc run equivalents."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .translate import RULES
|
|
9
|
+
|
|
10
|
+
COMPOSE_KEYS: dict[str, dict[str, Any]] = RULES["compose"]["keys"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ServiceReport:
|
|
15
|
+
name: str
|
|
16
|
+
command: str = ""
|
|
17
|
+
ok: list[str] = field(default_factory=list)
|
|
18
|
+
partial: list[tuple[str, str]] = field(default_factory=list)
|
|
19
|
+
missing: list[tuple[str, str]] = field(default_factory=list)
|
|
20
|
+
unknown: list[str] = field(default_factory=list)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ComposeReport:
|
|
25
|
+
services: list[ServiceReport] = field(default_factory=list)
|
|
26
|
+
prelude: list[str] = field(default_factory=list)
|
|
27
|
+
parse_error: str | None = None
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def exit_code(self) -> int:
|
|
31
|
+
if self.parse_error:
|
|
32
|
+
return 2
|
|
33
|
+
if any(s.missing for s in self.services):
|
|
34
|
+
return 2
|
|
35
|
+
if any(s.partial for s in self.services):
|
|
36
|
+
return 1
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
def as_dict(self) -> dict[str, Any]:
|
|
40
|
+
return {
|
|
41
|
+
"parseError": self.parse_error,
|
|
42
|
+
"prelude": self.prelude,
|
|
43
|
+
"services": [
|
|
44
|
+
{
|
|
45
|
+
"name": s.name,
|
|
46
|
+
"command": s.command,
|
|
47
|
+
"ok": s.ok,
|
|
48
|
+
"partial": [{"key": k, "note": n} for k, n in s.partial],
|
|
49
|
+
"missing": [{"key": k, "note": n} for k, n in s.missing],
|
|
50
|
+
"unknown": s.unknown,
|
|
51
|
+
}
|
|
52
|
+
for s in self.services
|
|
53
|
+
],
|
|
54
|
+
"exitCode": self.exit_code,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _as_list(value: Any) -> list[str]:
|
|
59
|
+
if value is None:
|
|
60
|
+
return []
|
|
61
|
+
if isinstance(value, list):
|
|
62
|
+
return [str(v) for v in value]
|
|
63
|
+
if isinstance(value, dict):
|
|
64
|
+
return [f"{k}={v}" for k, v in value.items()]
|
|
65
|
+
return [str(value)]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _build_run(name: str, svc: dict[str, Any]) -> str:
|
|
69
|
+
args: list[str] = ["wslc", "run", "-d", "--name", svc.get("container_name") or name]
|
|
70
|
+
|
|
71
|
+
for env in _as_list(svc.get("environment")):
|
|
72
|
+
args += ["-e", env]
|
|
73
|
+
for env_file in _as_list(svc.get("env_file")):
|
|
74
|
+
args += ["--env-file", env_file]
|
|
75
|
+
for port in _as_list(svc.get("ports")):
|
|
76
|
+
args += ["-p", port.strip('"')]
|
|
77
|
+
for vol in _as_list(svc.get("volumes")):
|
|
78
|
+
args += ["-v", vol]
|
|
79
|
+
for net in _as_list(svc.get("networks")):
|
|
80
|
+
args += ["--network", net]
|
|
81
|
+
for cap in _as_list(svc.get("cap_add")):
|
|
82
|
+
args += ["--cap-add", cap]
|
|
83
|
+
for dev in _as_list(svc.get("devices")):
|
|
84
|
+
args += ["--device", dev]
|
|
85
|
+
for label in _as_list(svc.get("labels")):
|
|
86
|
+
args += ["--label", label]
|
|
87
|
+
for tmp in _as_list(svc.get("tmpfs")):
|
|
88
|
+
args += ["--tmpfs", tmp]
|
|
89
|
+
|
|
90
|
+
if svc.get("working_dir"):
|
|
91
|
+
args += ["-w", str(svc["working_dir"])]
|
|
92
|
+
if svc.get("user"):
|
|
93
|
+
args += ["-u", str(svc["user"])]
|
|
94
|
+
if svc.get("hostname"):
|
|
95
|
+
args += ["--hostname", str(svc["hostname"])]
|
|
96
|
+
if svc.get("entrypoint"):
|
|
97
|
+
ep = svc["entrypoint"]
|
|
98
|
+
args += ["--entrypoint", ep if isinstance(ep, str) else " ".join(ep)]
|
|
99
|
+
if svc.get("stdin_open"):
|
|
100
|
+
args.append("-i")
|
|
101
|
+
if svc.get("tty"):
|
|
102
|
+
args.append("-t")
|
|
103
|
+
|
|
104
|
+
image = svc.get("image")
|
|
105
|
+
if not image and svc.get("build"):
|
|
106
|
+
image = f"{name}:local"
|
|
107
|
+
args.append(str(image or "<image>"))
|
|
108
|
+
|
|
109
|
+
cmd = svc.get("command")
|
|
110
|
+
if cmd:
|
|
111
|
+
args += cmd.split() if isinstance(cmd, str) else [str(c) for c in cmd]
|
|
112
|
+
|
|
113
|
+
return " ".join(args)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def analyse(text: str) -> ComposeReport:
|
|
117
|
+
"""Analyse a compose file. Requires PyYAML; reports cleanly if absent."""
|
|
118
|
+
report = ComposeReport()
|
|
119
|
+
try:
|
|
120
|
+
import yaml
|
|
121
|
+
except ImportError: # pragma: no cover
|
|
122
|
+
report.parse_error = "PyYAML is required: pip install 'docker2wslc[yaml]'"
|
|
123
|
+
return report
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
doc = yaml.safe_load(text) or {}
|
|
127
|
+
except Exception as exc:
|
|
128
|
+
report.parse_error = f"YAML parse error: {exc}"
|
|
129
|
+
return report
|
|
130
|
+
|
|
131
|
+
if not isinstance(doc, dict):
|
|
132
|
+
report.parse_error = "Compose file did not parse to a mapping."
|
|
133
|
+
return report
|
|
134
|
+
|
|
135
|
+
for vol in (doc.get("volumes") or {}):
|
|
136
|
+
report.prelude.append(f"wslc volume create {vol}")
|
|
137
|
+
for net in (doc.get("networks") or {}):
|
|
138
|
+
report.prelude.append(f"wslc network create {net}")
|
|
139
|
+
|
|
140
|
+
services = doc.get("services") or {}
|
|
141
|
+
if not isinstance(services, dict):
|
|
142
|
+
report.parse_error = "`services` is not a mapping."
|
|
143
|
+
return report
|
|
144
|
+
|
|
145
|
+
for name, svc in services.items():
|
|
146
|
+
if not isinstance(svc, dict):
|
|
147
|
+
continue
|
|
148
|
+
sr = ServiceReport(name=str(name))
|
|
149
|
+
for key in svc:
|
|
150
|
+
spec = COMPOSE_KEYS.get(str(key))
|
|
151
|
+
if spec is None:
|
|
152
|
+
sr.unknown.append(str(key))
|
|
153
|
+
elif spec["status"] == "ok":
|
|
154
|
+
sr.ok.append(str(key))
|
|
155
|
+
elif spec["status"] == "partial":
|
|
156
|
+
sr.partial.append((str(key), spec.get("note", "")))
|
|
157
|
+
else:
|
|
158
|
+
sr.missing.append((str(key), spec.get("note", "")))
|
|
159
|
+
sr.command = _build_run(str(name), svc)
|
|
160
|
+
report.services.append(sr)
|
|
161
|
+
|
|
162
|
+
# depends_on ordering hint
|
|
163
|
+
ordered = [s.name for s in report.services]
|
|
164
|
+
if any("depends_on" in dict(s.missing) for s in report.services):
|
|
165
|
+
report.prelude.append(f"# start order matters: {' -> '.join(ordered)}")
|
|
166
|
+
|
|
167
|
+
return report
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$comment": "Single source of truth for Docker -> wslc translation. Consumed by the Python package, the npm CLI, the MCP server and the VS Code extension. Do not fork this logic per language.",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"wslcPreview": "2026-07",
|
|
5
|
+
"identical": [
|
|
6
|
+
"run", "build", "pull", "push", "stop", "start", "restart", "exec", "logs",
|
|
7
|
+
"inspect", "stats", "cp", "tag", "login", "logout", "kill", "pause",
|
|
8
|
+
"unpause", "version", "info", "top", "wait", "port", "rename", "diff",
|
|
9
|
+
"commit", "save", "load", "attach"
|
|
10
|
+
],
|
|
11
|
+
"renamed": {
|
|
12
|
+
"ps": "container list",
|
|
13
|
+
"images": "image list",
|
|
14
|
+
"rmi": "image remove",
|
|
15
|
+
"rm": "container remove",
|
|
16
|
+
"create": "container create",
|
|
17
|
+
"history": "image history",
|
|
18
|
+
"search": "image search"
|
|
19
|
+
},
|
|
20
|
+
"renamedFlags": {
|
|
21
|
+
"ps": { "-a": "--all", "--all": "--all", "-q": "--quiet", "--quiet": "--quiet" },
|
|
22
|
+
"images": { "-a": "--all", "--all": "--all" },
|
|
23
|
+
"rm": { "-f": "--force", "--force": "--force" },
|
|
24
|
+
"rmi": { "-f": "--force", "--force": "--force" }
|
|
25
|
+
},
|
|
26
|
+
"unsupported": {
|
|
27
|
+
"swarm": "Swarm orchestration has no wslc equivalent. Use Kubernetes or keep Docker for this workload.",
|
|
28
|
+
"service": "Swarm services are not supported by wslc.",
|
|
29
|
+
"stack": "Swarm stacks are not supported by wslc.",
|
|
30
|
+
"node": "Swarm node management is not supported by wslc.",
|
|
31
|
+
"secret": "Swarm secrets are not supported. Pass values with -e or mount a file.",
|
|
32
|
+
"config": "Swarm configs are not supported by wslc.",
|
|
33
|
+
"plugin": "Docker plugins are not supported by wslc.",
|
|
34
|
+
"context": "wslc has a single local runtime, so there are no contexts to switch.",
|
|
35
|
+
"buildx": "buildx / bake is not available. Use wslc build for single-platform images.",
|
|
36
|
+
"manifest": "Manifest lists cannot be created with wslc in the preview.",
|
|
37
|
+
"trust": "Docker Content Trust is Docker-specific and has no wslc equivalent.",
|
|
38
|
+
"checkpoint": "Checkpoint/restore is not implemented in wslc."
|
|
39
|
+
},
|
|
40
|
+
"flags": {
|
|
41
|
+
"--platform": {
|
|
42
|
+
"action": "drop",
|
|
43
|
+
"takesValue": true,
|
|
44
|
+
"severity": "warn",
|
|
45
|
+
"note": "`--platform` is dropped: wslc builds and runs for the host architecture only in the preview."
|
|
46
|
+
},
|
|
47
|
+
"--restart": {
|
|
48
|
+
"action": "drop",
|
|
49
|
+
"takesValue": true,
|
|
50
|
+
"severity": "warn",
|
|
51
|
+
"note": "Restart policies are not implemented in the wslc preview. Flag dropped — use a Windows scheduled task or a wrapper script for auto-restart."
|
|
52
|
+
},
|
|
53
|
+
"--gpus": {
|
|
54
|
+
"action": "rewrite",
|
|
55
|
+
"takesValue": true,
|
|
56
|
+
"replaceWith": "--device nvidia.com/gpu=all",
|
|
57
|
+
"severity": "info",
|
|
58
|
+
"note": "GPU access in wslc goes through the Container Device Interface. Use `--device nvidia.com/gpu=all` instead of `--gpus`."
|
|
59
|
+
},
|
|
60
|
+
"--network": {
|
|
61
|
+
"action": "conditional",
|
|
62
|
+
"takesValue": true,
|
|
63
|
+
"whenValue": {
|
|
64
|
+
"host": {
|
|
65
|
+
"action": "drop",
|
|
66
|
+
"severity": "info",
|
|
67
|
+
"note": "`--network host` behaves differently: wslc routes container traffic through the Windows host stack already, so host networking is implicit. Flag dropped."
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"severity": "info",
|
|
71
|
+
"note": "Custom networks are supported but bridge-only. Create it first with `wslc network create`."
|
|
72
|
+
},
|
|
73
|
+
"--volume": {
|
|
74
|
+
"action": "keep",
|
|
75
|
+
"takesValue": true,
|
|
76
|
+
"aliases": ["-v", "--mount"],
|
|
77
|
+
"severity": "info",
|
|
78
|
+
"noteWhenWindowsPath": "Windows host paths are shared over VirtioFS. Use forward slashes (`C:/work:/app`) and expect Linux-style permissions on the container side."
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
"compose": {
|
|
82
|
+
"supported": false,
|
|
83
|
+
"note": "wslc has no built-in Compose runtime. Translate each service into a wslc run call, or drive them from a PowerShell/bash script.",
|
|
84
|
+
"docsUrl": "https://wslcontainers.com/guides/compose/",
|
|
85
|
+
"keys": {
|
|
86
|
+
"image": { "status": "ok", "maps": "positional image argument" },
|
|
87
|
+
"build": { "status": "ok", "maps": "wslc build -t <name>" },
|
|
88
|
+
"command": { "status": "ok", "maps": "trailing command args" },
|
|
89
|
+
"entrypoint": { "status": "ok", "maps": "--entrypoint" },
|
|
90
|
+
"environment": { "status": "ok", "maps": "-e KEY=VALUE" },
|
|
91
|
+
"env_file": { "status": "ok", "maps": "--env-file" },
|
|
92
|
+
"ports": { "status": "ok", "maps": "-p HOST:CONTAINER" },
|
|
93
|
+
"volumes": { "status": "partial", "maps": "-v", "note": "Named volumes must be created first with wslc volume create. Windows paths go over VirtioFS." },
|
|
94
|
+
"networks": { "status": "partial", "maps": "--network", "note": "Bridge-only. Create with wslc network create before running." },
|
|
95
|
+
"working_dir": { "status": "ok", "maps": "-w" },
|
|
96
|
+
"user": { "status": "ok", "maps": "-u" },
|
|
97
|
+
"hostname": { "status": "ok", "maps": "--hostname" },
|
|
98
|
+
"container_name": { "status": "ok", "maps": "--name" },
|
|
99
|
+
"labels": { "status": "ok", "maps": "--label" },
|
|
100
|
+
"cap_add": { "status": "partial", "maps": "--cap-add", "note": "Some capabilities are restricted inside the utility VM." },
|
|
101
|
+
"devices": { "status": "partial", "maps": "--device", "note": "CDI syntax differs from Docker for GPUs." },
|
|
102
|
+
"tmpfs": { "status": "ok", "maps": "--tmpfs" },
|
|
103
|
+
"stdin_open": { "status": "ok", "maps": "-i" },
|
|
104
|
+
"tty": { "status": "ok", "maps": "-t" },
|
|
105
|
+
"restart": { "status": "missing", "note": "No restart policies in the wslc preview." },
|
|
106
|
+
"healthcheck": { "status": "missing", "note": "wslc has no healthcheck support; depends_on conditions relying on it cannot work." },
|
|
107
|
+
"depends_on": { "status": "missing", "note": "No dependency ordering. Start services in order yourself and add readiness waits." },
|
|
108
|
+
"deploy": { "status": "missing", "note": "Swarm-only section, ignored entirely." },
|
|
109
|
+
"profiles": { "status": "missing", "note": "Compose-only concept." },
|
|
110
|
+
"extends": { "status": "missing", "note": "Compose-only concept." },
|
|
111
|
+
"configs": { "status": "missing", "note": "Swarm configs are unsupported; mount a file instead." },
|
|
112
|
+
"secrets": { "status": "missing", "note": "Swarm secrets are unsupported; use -e or a mounted file." }
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"devcontainer": {
|
|
116
|
+
"keys": {
|
|
117
|
+
"image": { "status": "ok" },
|
|
118
|
+
"build.dockerfile": { "status": "ok" },
|
|
119
|
+
"build.args": { "status": "ok", "maps": "--build-arg" },
|
|
120
|
+
"build.platform": { "status": "missing", "note": "Host architecture only." },
|
|
121
|
+
"workspaceMount": { "status": "partial", "note": "VirtioFS; use forward slashes for Windows paths." },
|
|
122
|
+
"workspaceFolder": { "status": "ok" },
|
|
123
|
+
"containerEnv": { "status": "ok", "maps": "-e" },
|
|
124
|
+
"remoteEnv": { "status": "ok" },
|
|
125
|
+
"forwardPorts": { "status": "ok", "maps": "-p" },
|
|
126
|
+
"postCreateCommand": { "status": "ok", "maps": "wslc exec" },
|
|
127
|
+
"postStartCommand": { "status": "ok" },
|
|
128
|
+
"postAttachCommand": { "status": "ok" },
|
|
129
|
+
"remoteUser": { "status": "ok", "maps": "-u" },
|
|
130
|
+
"containerUser": { "status": "ok" },
|
|
131
|
+
"mounts": { "status": "partial", "note": "VirtioFS path rules apply." },
|
|
132
|
+
"runArgs": { "status": "partial", "note": "Passed through verbatim; unsupported flags will fail at runtime." },
|
|
133
|
+
"dockerComposeFile": { "status": "missing", "note": "No Compose runtime. Replace with a single image or build entry." },
|
|
134
|
+
"features": { "status": "missing", "note": "Requires BuildKit metadata that wslc build does not emit." },
|
|
135
|
+
"runServices": { "status": "missing", "note": "Compose-only key." },
|
|
136
|
+
"shutdownAction": { "status": "partial", "note": "stopCompose is meaningless without Compose." }
|
|
137
|
+
},
|
|
138
|
+
"requiredSetting": {
|
|
139
|
+
"key": "dev.containers.dockerPath",
|
|
140
|
+
"value": "wslc",
|
|
141
|
+
"note": "Point the Dev Containers extension at wslc. Prefer workspace scope over global."
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
"apiIncompatible": {
|
|
145
|
+
"note": "CLI-driven tooling ports to wslc. API-driven tooling cannot, because wslc is daemonless and exposes no Docker Engine API or socket.",
|
|
146
|
+
"tools": [
|
|
147
|
+
{ "name": "Testcontainers", "via": "Engine API", "works": false },
|
|
148
|
+
{ "name": "Docker-in-Docker sidecars", "via": "mounted socket", "works": false },
|
|
149
|
+
{ "name": "Portainer", "via": "Engine API", "works": false },
|
|
150
|
+
{ "name": "Lazydocker", "via": "Engine API", "works": false },
|
|
151
|
+
{ "name": "LocalStack (docker mode)", "via": "Engine API", "works": false },
|
|
152
|
+
{ "name": "act", "via": "Engine API", "works": false },
|
|
153
|
+
{ "name": "VS Code Dev Containers", "via": "CLI binary", "works": true },
|
|
154
|
+
{ "name": "shell scripts calling docker", "via": "CLI binary", "works": true }
|
|
155
|
+
]
|
|
156
|
+
},
|
|
157
|
+
"links": {
|
|
158
|
+
"site": "https://wslcontainers.com",
|
|
159
|
+
"converter": "https://wslcontainers.com/",
|
|
160
|
+
"cheatsheet": "https://wslcontainers.com/reference/cheatsheet/",
|
|
161
|
+
"compose": "https://wslcontainers.com/guides/compose/",
|
|
162
|
+
"install": "https://wslcontainers.com/guides/install/",
|
|
163
|
+
"vsDocker": "https://wslcontainers.com/guides/vs-docker-desktop/",
|
|
164
|
+
"devcontainers": "https://wslcontainers.com/guides/vscode-dev-containers/",
|
|
165
|
+
"testcontainers": "https://wslcontainers.com/guides/testcontainers/"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Docker -> wslc command translation, driven by the shared rules.json."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from importlib.resources import files
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
WINDOWS_PATH = re.compile(r"^[A-Za-z]:[\\/]")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_rules() -> dict[str, Any]:
|
|
15
|
+
"""Load the bundled rules.json (single source of truth across languages)."""
|
|
16
|
+
data = files("docker2wslc").joinpath("rules.json").read_text(encoding="utf-8")
|
|
17
|
+
return json.loads(data)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
RULES = load_rules()
|
|
21
|
+
|
|
22
|
+
SEV_ORDER = {"info": 0, "warn": 1, "error": 2}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class Note:
|
|
27
|
+
severity: str
|
|
28
|
+
text: str
|
|
29
|
+
|
|
30
|
+
def __str__(self) -> str: # pragma: no cover - display helper
|
|
31
|
+
return f"[{self.severity}] {self.text}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class Result:
|
|
36
|
+
"""Outcome of translating one or more lines."""
|
|
37
|
+
|
|
38
|
+
output: str = ""
|
|
39
|
+
notes: list[Note] = field(default_factory=list)
|
|
40
|
+
compose_hit: bool = False
|
|
41
|
+
unsupported_hit: bool = False
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def exit_code(self) -> int:
|
|
45
|
+
"""0 clean, 1 degraded (flags dropped/rewritten), 2 unmigratable."""
|
|
46
|
+
if self.unsupported_hit or self.compose_hit:
|
|
47
|
+
return 2
|
|
48
|
+
if any(n.severity in ("warn", "error") for n in self.notes):
|
|
49
|
+
return 1
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
def as_dict(self) -> dict[str, Any]:
|
|
53
|
+
return {
|
|
54
|
+
"output": self.output,
|
|
55
|
+
"notes": [{"severity": n.severity, "text": n.text} for n in self.notes],
|
|
56
|
+
"composeHit": self.compose_hit,
|
|
57
|
+
"unsupportedHit": self.unsupported_hit,
|
|
58
|
+
"exitCode": self.exit_code,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def tokenize(line: str) -> list[str]:
|
|
63
|
+
"""Split a shell-ish line, keeping quoted spans intact (quotes preserved)."""
|
|
64
|
+
out: list[str] = []
|
|
65
|
+
cur = ""
|
|
66
|
+
quote: str | None = None
|
|
67
|
+
for ch in line:
|
|
68
|
+
if quote:
|
|
69
|
+
cur += ch
|
|
70
|
+
if ch == quote:
|
|
71
|
+
quote = None
|
|
72
|
+
elif ch in ("'", '"'):
|
|
73
|
+
quote = ch
|
|
74
|
+
cur += ch
|
|
75
|
+
elif ch.isspace():
|
|
76
|
+
if cur:
|
|
77
|
+
out.append(cur)
|
|
78
|
+
cur = ""
|
|
79
|
+
else:
|
|
80
|
+
cur += ch
|
|
81
|
+
if cur:
|
|
82
|
+
out.append(cur)
|
|
83
|
+
return out
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _flag_spec(flag: str) -> tuple[str, dict[str, Any]] | tuple[None, None]:
|
|
87
|
+
flags = RULES["flags"]
|
|
88
|
+
if flag in flags:
|
|
89
|
+
return flag, flags[flag]
|
|
90
|
+
for canonical, spec in flags.items():
|
|
91
|
+
if flag in spec.get("aliases", []):
|
|
92
|
+
return canonical, spec
|
|
93
|
+
return None, None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _translate_args(args: list[str], notes: list[Note]) -> list[str]:
|
|
97
|
+
out: list[str] = []
|
|
98
|
+
i = 0
|
|
99
|
+
while i < len(args):
|
|
100
|
+
arg = args[i]
|
|
101
|
+
bare, _, inline = arg.partition("=")
|
|
102
|
+
canonical, spec = _flag_spec(bare)
|
|
103
|
+
|
|
104
|
+
if spec is None:
|
|
105
|
+
out.append(arg)
|
|
106
|
+
i += 1
|
|
107
|
+
continue
|
|
108
|
+
|
|
109
|
+
takes_value = spec.get("takesValue", False)
|
|
110
|
+
has_inline = "=" in arg
|
|
111
|
+
value = inline if has_inline else (args[i + 1] if i + 1 < len(args) else "")
|
|
112
|
+
consumed = 1 if has_inline or not takes_value else 2
|
|
113
|
+
|
|
114
|
+
action = spec["action"]
|
|
115
|
+
|
|
116
|
+
if action == "conditional":
|
|
117
|
+
branch = spec.get("whenValue", {}).get(value)
|
|
118
|
+
if branch:
|
|
119
|
+
if branch["action"] == "drop":
|
|
120
|
+
notes.append(Note(branch.get("severity", "info"), branch["note"]))
|
|
121
|
+
i += consumed
|
|
122
|
+
continue
|
|
123
|
+
notes.append(Note(spec.get("severity", "info"), spec["note"]))
|
|
124
|
+
out.append(f"{bare}={value}" if has_inline else bare)
|
|
125
|
+
if not has_inline and takes_value and value:
|
|
126
|
+
out.append(value)
|
|
127
|
+
i += consumed
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
if action == "drop":
|
|
131
|
+
notes.append(Note(spec.get("severity", "warn"), spec["note"]))
|
|
132
|
+
i += consumed
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
if action == "rewrite":
|
|
136
|
+
notes.append(Note(spec.get("severity", "info"), spec["note"]))
|
|
137
|
+
out.extend(spec["replaceWith"].split())
|
|
138
|
+
i += consumed
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
# action == "keep"
|
|
142
|
+
note_win = spec.get("noteWhenWindowsPath")
|
|
143
|
+
if note_win and (WINDOWS_PATH.match(value) or value.startswith("/mnt/")):
|
|
144
|
+
notes.append(Note(spec.get("severity", "info"), note_win))
|
|
145
|
+
out.append(arg)
|
|
146
|
+
if not has_inline and takes_value and value:
|
|
147
|
+
out.append(value)
|
|
148
|
+
i += consumed
|
|
149
|
+
return out
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def translate_line(raw: str) -> Result | None:
|
|
153
|
+
"""Translate a single line. Returns None for blank lines."""
|
|
154
|
+
line = raw.strip()
|
|
155
|
+
if not line:
|
|
156
|
+
return None
|
|
157
|
+
if line.startswith("#"):
|
|
158
|
+
return Result(output=line)
|
|
159
|
+
|
|
160
|
+
notes: list[Note] = []
|
|
161
|
+
tokens = tokenize(line)
|
|
162
|
+
|
|
163
|
+
if tokens and tokens[0] == "sudo":
|
|
164
|
+
tokens.pop(0)
|
|
165
|
+
notes.append(
|
|
166
|
+
Note(
|
|
167
|
+
"info",
|
|
168
|
+
"Dropped sudo — wslc runs as your Windows user, no elevation needed "
|
|
169
|
+
"for normal container operations.",
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if not tokens or tokens[0] not in ("docker", "podman"):
|
|
174
|
+
return Result(
|
|
175
|
+
output=f"# not a docker command: {line}",
|
|
176
|
+
notes=[Note("info", "Only docker/podman commands are translated. Line left unchanged.")],
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if tokens[0] == "podman":
|
|
180
|
+
notes.append(
|
|
181
|
+
Note("info", "Treated podman as Docker-compatible; flag coverage is nearly identical.")
|
|
182
|
+
)
|
|
183
|
+
tokens.pop(0)
|
|
184
|
+
|
|
185
|
+
if not tokens:
|
|
186
|
+
return Result(output="wslc", notes=notes)
|
|
187
|
+
|
|
188
|
+
if tokens[0] in ("compose", "docker-compose"):
|
|
189
|
+
compose = RULES["compose"]
|
|
190
|
+
notes.append(Note("error", compose["note"]))
|
|
191
|
+
return Result(
|
|
192
|
+
output="# No native Compose runtime in wslc.", notes=notes, compose_hit=True
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
two = f"{tokens[0]} {tokens[1]}" if len(tokens) > 1 else ""
|
|
196
|
+
if two and two in (RULES["renamed"].values()):
|
|
197
|
+
rest = _translate_args(tokens[2:], notes)
|
|
198
|
+
return Result(output=" ".join(["wslc", two, *rest]).strip(), notes=notes)
|
|
199
|
+
|
|
200
|
+
verb = tokens[0]
|
|
201
|
+
|
|
202
|
+
if verb in RULES["unsupported"]:
|
|
203
|
+
notes.append(Note("error", RULES["unsupported"][verb]))
|
|
204
|
+
return Result(
|
|
205
|
+
output=f"# {verb}: not supported by wslc", notes=notes, unsupported_hit=True
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if verb in RULES["renamed"]:
|
|
209
|
+
mapped = RULES["renamed"][verb]
|
|
210
|
+
flag_map = RULES["renamedFlags"].get(verb, {})
|
|
211
|
+
rest: list[str] = []
|
|
212
|
+
for arg in tokens[1:]:
|
|
213
|
+
rest.append(flag_map.get(arg, arg))
|
|
214
|
+
rest = _translate_args(rest, notes)
|
|
215
|
+
notes.append(
|
|
216
|
+
Note(
|
|
217
|
+
"info",
|
|
218
|
+
f"`docker {verb}` is grouped under a noun in wslc: `wslc {mapped}`. "
|
|
219
|
+
f"Recent previews accept `wslc {verb}` as an alias, but the noun form is stable.",
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
return Result(output=" ".join(["wslc", mapped, *rest]).strip(), notes=notes)
|
|
223
|
+
|
|
224
|
+
if verb in RULES["identical"]:
|
|
225
|
+
rest = _translate_args(tokens[1:], notes)
|
|
226
|
+
return Result(output=" ".join(["wslc", verb, *rest]).strip(), notes=notes)
|
|
227
|
+
|
|
228
|
+
notes.append(
|
|
229
|
+
Note(
|
|
230
|
+
"warn",
|
|
231
|
+
f"Unknown docker subcommand `{verb}` — passed through unchanged. "
|
|
232
|
+
"Verify against `wslc --help`.",
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
rest = _translate_args(tokens[1:], notes)
|
|
236
|
+
return Result(output=" ".join(["wslc", verb, *rest]).strip(), notes=notes)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def translate(text: str) -> Result:
|
|
240
|
+
"""Translate a multi-line script, de-duplicating notes."""
|
|
241
|
+
lines: list[str] = []
|
|
242
|
+
notes: list[Note] = []
|
|
243
|
+
seen: set[str] = set()
|
|
244
|
+
compose_hit = False
|
|
245
|
+
unsupported_hit = False
|
|
246
|
+
|
|
247
|
+
for raw in str(text).split("\n"):
|
|
248
|
+
res = translate_line(raw)
|
|
249
|
+
if res is None:
|
|
250
|
+
lines.append("")
|
|
251
|
+
continue
|
|
252
|
+
lines.append(res.output)
|
|
253
|
+
compose_hit = compose_hit or res.compose_hit
|
|
254
|
+
unsupported_hit = unsupported_hit or res.unsupported_hit
|
|
255
|
+
for note in res.notes:
|
|
256
|
+
if note.text not in seen:
|
|
257
|
+
seen.add(note.text)
|
|
258
|
+
notes.append(note)
|
|
259
|
+
|
|
260
|
+
joined = re.sub(r"\n{3,}", "\n\n", "\n".join(lines)).strip()
|
|
261
|
+
notes.sort(key=lambda n: -SEV_ORDER.get(n.severity, 0))
|
|
262
|
+
return Result(
|
|
263
|
+
output=joined,
|
|
264
|
+
notes=notes,
|
|
265
|
+
compose_hit=compose_hit,
|
|
266
|
+
unsupported_hit=unsupported_hit,
|
|
267
|
+
)
|