├── .devcontainer
├── Dockerfile
├── devcontainer.json
└── init.sh
├── .gitattributes
├── .github
├── FUNDING.yml
└── workflows
│ ├── build.yml
│ ├── ci.yml
│ └── release.yml
├── .gitignore
├── .gitmodules
├── .libdragon
└── config.json
├── .prettierrc.js
├── .vscode
├── c_cpp_properties.json
├── launch.json
└── tasks.json
├── CHANGELOG.md
├── LICENSE.md
├── Makefile
├── README.md
├── bundle.mjs
├── eslint.config.mjs
├── index.js
├── jest.config.cjs
├── libdragon.ico
├── modules
├── actions
│ ├── destroy.js
│ ├── disasm.js
│ ├── exec.js
│ ├── help.js
│ ├── index.js
│ ├── init.js
│ ├── install.js
│ ├── make.js
│ ├── start.js
│ ├── stop.js
│ ├── update.js
│ └── version.js
├── constants.js
├── docker-utils.js
├── globals.js
├── helpers.js
├── npm-utils.js
├── parameters.js
├── project-info.js
└── utils.js
├── pack.mjs
├── package-lock.json
├── package.json
├── sea-config.json
├── setup.iss
├── skeleton
├── Makefile.mk
├── index.d.ts
└── src
│ └── main.c
├── src
├── Makefile
└── main.c
├── test.mjs
└── tsconfig.json
/.devcontainer/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 | FROM ghcr.io/dragonminded/libdragon:latest
3 |
4 | ENV DOCKER_CONTAINER=true
5 |
6 | COPY ./.devcontainer/init.sh /tmp/init.sh
7 |
8 | WORKDIR /tmp
9 | RUN /bin/bash -c ./init.sh
--------------------------------------------------------------------------------
/.devcontainer/devcontainer.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use image instead of build, if you don't need the cli installed
3 | // "image": ghcr.io/dragonminded/libdragon:latest
4 | "build": { "dockerfile": "./Dockerfile", "context": "../" },
5 | "workspaceMount": "source=${localWorkspaceFolder},target=/libdragon,type=bind",
6 | "workspaceFolder": "/libdragon",
7 | // You can execute ./build.sh instead, if you don't need the cli in the container
8 | "postCreateCommand": "npm install && npm link && libdragon install"
9 | }
--------------------------------------------------------------------------------
/.devcontainer/init.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 |
5 | # Install nodejs & npm
6 | apt-get update
7 | apt install curl -y
8 | curl -fsSL https://deb.nodesource.com/setup_16.x | bash -
9 | apt-get install nodejs
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | patreon: anacierdem
2 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | on:
2 | workflow_call:
3 |
4 | jobs:
5 | staticAnalysis:
6 | runs-on: ubuntu-latest
7 | strategy:
8 | fail-fast: true
9 | steps:
10 | - uses: actions/checkout@v4
11 | - uses: actions/setup-node@v4
12 | with:
13 | node-version: '22'
14 | registry-url: 'https://registry.npmjs.org'
15 | cache: 'npm'
16 |
17 | - name: Install
18 | run: npm ci
19 |
20 | - name: Test
21 | run: |
22 | npm run format-check
23 | npm run lint-check
24 | npm run tsc
25 |
26 | integrationTests:
27 | strategy:
28 | fail-fast: true
29 | matrix:
30 | os: [ubuntu-latest]
31 | runs-on: ${{ matrix.os }}
32 | steps:
33 | - uses: actions/checkout@v4
34 | with:
35 | submodules: true
36 | fetch-depth: 1
37 | - uses: actions/setup-node@v4
38 | with:
39 | node-version: '22'
40 | registry-url: 'https://registry.npmjs.org'
41 | cache: 'npm'
42 |
43 | - name: Install
44 | run: npm ci
45 |
46 | - name: Tests
47 | run: |
48 | git config --global user.name "libdragon-cli"
49 | git config --global user.email "cli@libdragon.dev"
50 | npm run test
51 |
52 | buildExecutables:
53 | strategy:
54 | matrix:
55 | runs-on: [ubuntu-latest, macos-latest, windows-latest]
56 | runs-on: ${{ matrix.runs-on }}
57 | steps:
58 | - uses: actions/checkout@v4
59 | - uses: actions/setup-node@v4
60 | with:
61 | node-version: '22'
62 | cache: 'npm'
63 |
64 | - name: Install
65 | run: npm ci
66 |
67 | - name: Write next version
68 | env:
69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
70 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
71 | run: npx semantic-release --dry-run --no-ci
72 |
73 | - name: Get next version
74 | id: next_release
75 | shell: bash
76 | run: echo "version=$(cat version.txt)" >> $GITHUB_OUTPUT
77 |
78 | - name: Build
79 | run: npm run pack ${{ steps.next_release.outputs.version }}
80 |
81 | - name: Upload build artifacts
82 | uses: actions/upload-artifact@v4
83 | with:
84 | name: executable-${{ matrix.runs-on }}
85 | path: build/libdragon*
86 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: ci
2 |
3 | on:
4 | push:
5 | branches:
6 | - '*'
7 | - '!master'
8 | - '!beta'
9 | pull_request:
10 |
11 | jobs:
12 | Verify:
13 | uses: ./.github/workflows/build.yml
14 | secrets: inherit
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Build and release
2 | on:
3 | push:
4 | branches:
5 | - 'beta'
6 | - 'master'
7 |
8 | jobs:
9 | verify:
10 | name: Verify and build
11 | uses: ./.github/workflows/build.yml
12 | secrets: inherit
13 |
14 | release:
15 | name: Release
16 | runs-on: windows-latest
17 | needs: verify
18 | steps:
19 | - name: Checkout
20 | uses: actions/checkout@v4
21 | with:
22 | persist-credentials: false
23 |
24 | - name: Prepare
25 | uses: actions/setup-node@v4
26 | with:
27 | # Do not include registy url here, it creates a .npmrc which prevents
28 | # semantic-release from authenticating with npm
29 | node-version: '22'
30 | cache: 'npm'
31 |
32 | - name: Install
33 | run: npm ci
34 |
35 | - name: Download artifacts
36 | uses: actions/download-artifact@v4
37 | with:
38 | path: ./build
39 | pattern: executable-*
40 | merge-multiple: true
41 |
42 | - name: Release
43 | env:
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
46 | run: npx semantic-release
47 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Build files
2 | *.a
3 | build/
4 |
5 |
6 | ## OSX junk
7 | .DS_Store
8 | .Trashes
9 | ._*
10 |
11 | ## Temporary files
12 | *.tmp
13 | *.swp
14 | *~
15 |
16 | *.v64
17 | *.z64
18 | *.elf
19 | *.bin
20 | *.o
21 | *.dfs
22 |
23 | node_modules
24 |
25 | ## Editors
26 | .vscode/settings.json
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "libdragon"]
2 | path = libdragon
3 | url = https://github.com/DragonMinded/libdragon
4 | branch = trunk
5 |
--------------------------------------------------------------------------------
/.libdragon/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "imageName": "ghcr.io/dragonminded/libdragon:latest",
3 | "vendorDirectory": "libdragon",
4 | "vendorStrategy": "submodule"
5 | }
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | singleQuote: true,
3 | trailingComma: 'es5',
4 | endOfLine: 'auto',
5 | };
6 |
--------------------------------------------------------------------------------
/.vscode/c_cpp_properties.json:
--------------------------------------------------------------------------------
1 | {
2 | "configurations": [
3 | {
4 | "name": "N64 Generic",
5 | "includePath": ["${workspaceFolder}/libdragon/include/**"],
6 | "defines": ["N64"],
7 | "cStandard": "gnu99",
8 | "cppStandard": "c++14",
9 | "intelliSenseMode": "gcc-x86",
10 | // By default we use the container, so querying it is not possible from
11 | // the host.
12 | "compilerPath": ""
13 | }
14 | ],
15 | "version": 4
16 | }
17 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "cwd": "${workspaceFolder}/src/",
9 | "type": "cppvsdbg",
10 | "request": "launch",
11 | "name": "Test Bench",
12 | "program": "UNFLoader",
13 | "args": ["-b", "-d", "-r", "${workspaceFolder}/src/test_bench.z64"],
14 | "preLaunchTask": "build",
15 | "console": "integratedTerminal"
16 | },
17 |
18 | {
19 | "cwd": "${workspaceFolder}/src/",
20 | "type": "cppvsdbg",
21 | "request": "launch",
22 | "name": "Test Bench (emu)",
23 | "program": "ares",
24 | "args": [
25 | "${workspaceFolder}/src/test_bench.z64",
26 | "--setting=DebugServer/Enabled=true",
27 | "--setting=DebugServer/UseIPv4=true",
28 | "--setting=DebugServer/Port=9123",
29 | "--setting Input/Defocus=Block"
30 | ],
31 | "preLaunchTask": "build",
32 | "console": "integratedTerminal"
33 | },
34 |
35 | {
36 | "cwd": "${workspaceFolder}/src/",
37 | "type": "cppdbg",
38 | "request": "launch",
39 | "name": "Attach to Test Bench (emu)",
40 | "program": "${workspaceFolder}/src/build/test_bench.elf",
41 | "preLaunchTask": "build",
42 | "stopAtEntry": true,
43 | "externalConsole": false,
44 | "MIMode": "gdb",
45 | "miDebuggerPath": "gdb-multiarch",
46 | "miDebuggerServerAddress": "127.0.0.1:9123",
47 | "setupCommands": [
48 | {
49 | "description": "Set architecture",
50 | "text": "set arch mips:4300"
51 | }
52 | ],
53 | "sourceFileMap": {
54 | "libdragon": "${workspaceFolder}"
55 | }
56 | },
57 |
58 | {
59 | "cwd": "${workspaceFolder}/src/",
60 | "type": "cppvsdbg",
61 | "request": "launch",
62 | "name": "Tests",
63 | "program": "UNFLoader",
64 | "args": [
65 | "-b",
66 | "-d",
67 | "-r",
68 | "${workspaceFolder}/libdragon/tests/testrom.z64"
69 | ],
70 | "preLaunchTask": "buildTests",
71 | "console": "integratedTerminal"
72 | },
73 |
74 | {
75 | "cwd": "${workspaceFolder}/libdragon/tests",
76 | "type": "cppvsdbg",
77 | "request": "launch",
78 | "name": "Tests (emu)",
79 | "program": "ares",
80 | "args": [
81 | "${workspaceFolder}/libdragon/tests/testrom_emu.z64",
82 | "--setting=DebugServer/Enabled=true",
83 | "--setting=DebugServer/UseIPv4=true",
84 | "--setting=DebugServer/Port=9123",
85 | "--setting Input/Defocus=Block"
86 | ],
87 | "preLaunchTask": "buildTests",
88 | "console": "integratedTerminal"
89 | },
90 |
91 | {
92 | "cwd": "${fileDirname}",
93 | "type": "cppvsdbg",
94 | "request": "launch",
95 | "name": "Run selected example",
96 | "program": "UNFLoader",
97 | "args": [
98 | "-b",
99 | "-d",
100 | "-r",
101 | "${fileDirname}/${fileBasenameNoExtension}.z64"
102 | ],
103 | "preLaunchTask": "buildExamples",
104 | "console": "integratedTerminal"
105 | },
106 |
107 | {
108 | "cwd": "${workspaceFolder}/src/",
109 | "type": "cppvsdbg",
110 | "request": "launch",
111 | "name": "Run selected example (emu)",
112 | "program": "ares",
113 | "args": [
114 | "${fileDirname}/${fileBasenameNoExtension}.z64",
115 | "--setting=DebugServer/Enabled=true",
116 | "--setting=DebugServer/UseIPv4=true",
117 | "--setting=DebugServer/Port=9123",
118 | "--setting Input/Defocus=Block"
119 | ],
120 | "preLaunchTask": "buildTests",
121 | "console": "integratedTerminal"
122 | }
123 | ],
124 |
125 | "compounds": [
126 | {
127 | "name": "Debug Test Bench (emu)",
128 | "configurations": ["Test Bench (emu)", "Attach to Test Bench (emu)"],
129 | "stopAll": true
130 | }
131 | ]
132 | }
133 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "clean",
8 | "type": "shell",
9 | "command": "npm run libdragon -- make clean",
10 | "group": "none"
11 | },
12 | {
13 | "label": "build",
14 | "type": "shell",
15 | "command": "npm run libdragon -- make bench",
16 | "group": {
17 | "kind": "build",
18 | "isDefault": true
19 | },
20 | "problemMatcher": [
21 | {
22 | "owner": "cpp",
23 | "fileLocation": ["relative", "${workspaceFolder}"],
24 | "pattern": {
25 | "regexp": "^\\/libdragon(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
26 | "file": 1,
27 | "line": 2,
28 | "column": 3,
29 | "severity": 4,
30 | "message": 5
31 | }
32 | }
33 | ]
34 | },
35 | {
36 | "label": "buildTests",
37 | "type": "shell",
38 | "group": "test",
39 | "command": "npm run libdragon -- make tests -j8",
40 | "problemMatcher": [
41 | {
42 | "owner": "cpp",
43 | "fileLocation": ["relative", "${workspaceFolder}"],
44 | "pattern": {
45 | "regexp": "^\\/libdragon(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
46 | "file": 1,
47 | "line": 2,
48 | "column": 3,
49 | "severity": 4,
50 | "message": 5
51 | }
52 | }
53 | ]
54 | },
55 | {
56 | "label": "buildExamples",
57 | "type": "shell",
58 | "group": "build",
59 | "command": "npm run libdragon -- make examples",
60 | "problemMatcher": [
61 | {
62 | "owner": "cpp",
63 | "fileLocation": ["relative", "${workspaceFolder}"],
64 | "pattern": {
65 | "regexp": "^\\/libdragon(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
66 | "file": 1,
67 | "line": 2,
68 | "column": 3,
69 | "severity": 4,
70 | "message": 5
71 | }
72 | }
73 | ]
74 | }
75 | ]
76 | }
77 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## [12.0.4](https://github.com/anacierdem/libdragon-docker/compare/v12.0.3...v12.0.4) (2024-10-20)
2 |
3 |
4 | ### Bug Fixes
5 |
6 | * properly passthrough environment variables ([2477f1c](https://github.com/anacierdem/libdragon-docker/commit/2477f1c6a04f7cc69a2e3d09938fe7f7adc5252c))
7 |
8 |
9 | ### Reverts
10 |
11 | * Revert "fix: do not use shell for user initiated docker exec" ([b4b400c](https://github.com/anacierdem/libdragon-docker/commit/b4b400c12b134c9af37e3cc090cddc4827e72132))
12 | * Revert "fix: fix ENOENT issues on macOS" ([42d27c5](https://github.com/anacierdem/libdragon-docker/commit/42d27c5008591ddcb6a74d72fe7db248397de1c4))
13 | * Revert "fix: only invoke a shell for docker commands known to cause problems on macos" ([8635ac0](https://github.com/anacierdem/libdragon-docker/commit/8635ac093fa37585d1dac994bd86bd94b8ba953a))
14 | * Revert "fix: run all commands in a shell and quote all user provided parameters" ([887893f](https://github.com/anacierdem/libdragon-docker/commit/887893fab23af3a9d47bba94ba9ad5ab90957ccf))
15 | * Revert "Remove potentially unnecessary shell" ([48b9b8f](https://github.com/anacierdem/libdragon-docker/commit/48b9b8f14568f3d0d84aa7a630c8624bb574cd22))
16 |
17 | ## [12.0.3](https://github.com/anacierdem/libdragon-docker/compare/v12.0.2...v12.0.3) (2024-10-20)
18 |
19 |
20 | ### Bug Fixes
21 |
22 | * do not use shell for user initiated docker exec ([4eb5910](https://github.com/anacierdem/libdragon-docker/commit/4eb5910ba11351b77b57a5c27ba46ad59fe74e54))
23 |
24 | ## [12.0.2](https://github.com/anacierdem/libdragon-docker/compare/v12.0.1...v12.0.2) (2024-10-20)
25 |
26 |
27 | ### Bug Fixes
28 |
29 | * run all commands in a shell and quote all user provided parameters ([9383222](https://github.com/anacierdem/libdragon-docker/commit/9383222c855b7aa54184578c8af60e0d35b70af4))
30 |
31 | ## [12.0.1](https://github.com/anacierdem/libdragon-docker/compare/v12.0.0...v12.0.1) (2024-10-20)
32 |
33 |
34 | ### Bug Fixes
35 |
36 | * only invoke a shell for docker commands known to cause problems on macos ([bc88f2e](https://github.com/anacierdem/libdragon-docker/commit/bc88f2e3a49a2a9365bc39e94e7daf9736b93a94))
37 |
38 | # [12.0.0](https://github.com/anacierdem/libdragon-docker/compare/v11.4.4...v12.0.0) (2024-10-19)
39 |
40 |
41 | ### Bug Fixes
42 |
43 | * disable docker hints for all operations ([37a9397](https://github.com/anacierdem/libdragon-docker/commit/37a9397d89324bd9d2dc8057704588444757d05c))
44 | * fix ENOENT issues on macOS ([f6c1296](https://github.com/anacierdem/libdragon-docker/commit/f6c129629deb14f1943a1950abdc6573b8049dcd))
45 |
46 |
47 | ### Code Refactoring
48 |
49 | * drop support for NPM dependency management ([84f02b3](https://github.com/anacierdem/libdragon-docker/commit/84f02b34a2f3a3eb68addff86628fb9e377b80d7)), closes [#77](https://github.com/anacierdem/libdragon-docker/issues/77) [#60](https://github.com/anacierdem/libdragon-docker/issues/60)
50 | * stop using container git ([ce4ae92](https://github.com/anacierdem/libdragon-docker/commit/ce4ae92eac51c9db0ec1b3939b61cdc261a05c19))
51 |
52 |
53 | ### BREAKING CHANGES
54 |
55 | * NPM dependency system will not work anymore
56 | * Host system must have git installed after this update.
57 |
58 | ## [11.4.4](https://github.com/anacierdem/libdragon-docker/compare/v11.4.3...v11.4.4) (2024-10-18)
59 |
60 |
61 | ### Bug Fixes
62 |
63 | * disable docker's What's new message ([22740c0](https://github.com/anacierdem/libdragon-docker/commit/22740c071d0fab62a2d906373eac9619214bcc97))
64 | * fix the name for macos tar file ([674afce](https://github.com/anacierdem/libdragon-docker/commit/674afcebf64011f7d4f424bbe7dd070b5e1ab9c3))
65 |
66 | ## [11.4.3](https://github.com/anacierdem/libdragon-docker/compare/v11.4.2...v11.4.3) (2024-09-30)
67 |
68 |
69 | ### Bug Fixes
70 |
71 | * print version string to stdout for version action ([8245649](https://github.com/anacierdem/libdragon-docker/commit/8245649fb215bceca8cebc3622a419bbe1c7a433))
72 |
73 | ## [11.4.2](https://github.com/anacierdem/libdragon-docker/compare/v11.4.1...v11.4.2) (2024-09-28)
74 |
75 |
76 | ### Bug Fixes
77 |
78 | * bundle the cli with the correct version ([2a74c7a](https://github.com/anacierdem/libdragon-docker/commit/2a74c7a71109c1d920c82e74db0fadd967cb5ed1))
79 |
80 | ## [11.4.1](https://github.com/anacierdem/libdragon-docker/compare/v11.4.0...v11.4.1) (2024-09-28)
81 |
82 |
83 | ### Bug Fixes
84 |
85 | * get rid of the experimental warning ([b0894e9](https://github.com/anacierdem/libdragon-docker/commit/b0894e96fab304f17415355261bfd36ec0908491))
86 |
87 | # [11.4.0](https://github.com/anacierdem/libdragon-docker/compare/v11.3.0...v11.4.0) (2024-09-27)
88 |
89 |
90 | ### Features
91 |
92 | * start packaging with node SEA ([9709082](https://github.com/anacierdem/libdragon-docker/commit/97090824d939b314ae19e0f3a0ae15241492b7cd))
93 |
94 | # [11.3.0](https://github.com/anacierdem/libdragon-docker/compare/v11.2.0...v11.3.0) (2024-09-27)
95 |
96 |
97 | ### Bug Fixes
98 |
99 | * **init.js:** ensure local development is working ([97cb111](https://github.com/anacierdem/libdragon-docker/commit/97cb111ebe8f1c674c8fd8065548d6dcdc826960))
100 | * **init.js:** fix issues with SEA init ([261cd0b](https://github.com/anacierdem/libdragon-docker/commit/261cd0bd78ed89b7b211a99f3aa42c913655a79f))
101 |
102 |
103 | ### Features
104 |
105 | * start packaging with node SEA ([fbe7055](https://github.com/anacierdem/libdragon-docker/commit/fbe70559d0d3ed541658e64be241ee9e71807ad0)), closes [#71](https://github.com/anacierdem/libdragon-docker/issues/71)
106 |
107 | # [11.2.0](https://github.com/anacierdem/libdragon-docker/compare/v11.1.3...v11.2.0) (2024-09-23)
108 |
109 |
110 | ### Bug Fixes
111 |
112 | * enable CVE-2024-27980 compatibility ([73bd360](https://github.com/anacierdem/libdragon-docker/commit/73bd360820236fdcbd70f3ce275e415c22b1e0d3))
113 | * fix --branch flag wasn't working on a non-Windows system ([dfc4be0](https://github.com/anacierdem/libdragon-docker/commit/dfc4be0bbfc9e9f667724528f20a94b86a465f73))
114 | * fix a potential condition causing an empty branch name ([ff86777](https://github.com/anacierdem/libdragon-docker/commit/ff8677748f679aa2cdbbc250c505980f46b5cf97))
115 | * output stderr of a command failure for better context for submodule creation ([17867e6](https://github.com/anacierdem/libdragon-docker/commit/17867e69a1bea517d8c3c5c1b6ea061117130a4b))
116 | * recover the branch only if the new project is initiated as a submodule ([c8cfd7a](https://github.com/anacierdem/libdragon-docker/commit/c8cfd7add01a70b658c49ff0deed5f3480f2ef29))
117 |
118 |
119 | ### Features
120 |
121 | * **init.js:** add a new --branch flag to override the libdragon branch ([20cd675](https://github.com/anacierdem/libdragon-docker/commit/20cd675621c8e57668dc4330db7cc7964d0dd1fa)), closes [#75](https://github.com/anacierdem/libdragon-docker/issues/75)
122 |
123 | ## [11.1.3](https://github.com/anacierdem/libdragon-docker/compare/v11.1.2...v11.1.3) (2024-09-17)
124 |
125 |
126 | ### Bug Fixes
127 |
128 | * streamline dependencies ([49aa08e](https://github.com/anacierdem/libdragon-docker/commit/49aa08e827febb612189989ba99fa5683a6f927b))
129 |
130 | ## [11.1.2](https://github.com/anacierdem/libdragon-docker/compare/v11.1.1...v11.1.2) (2024-09-17)
131 |
132 |
133 | ### Bug Fixes
134 |
135 | * include the installer in the release ([46299ba](https://github.com/anacierdem/libdragon-docker/commit/46299ba0c635227e363c23b3b58738ca8b01bad4))
136 |
137 | ## [11.1.1](https://github.com/anacierdem/libdragon-docker/compare/v11.1.0...v11.1.1) (2024-09-17)
138 |
139 |
140 | ### Bug Fixes
141 |
142 | * include the cli with the correct version in the installer ([a8a9804](https://github.com/anacierdem/libdragon-docker/commit/a8a9804e3d882f3f8435205f236c7ff8b2c17950))
143 |
144 | # [11.1.0](https://github.com/anacierdem/libdragon-docker/compare/v11.0.3...v11.1.0) (2024-09-16)
145 |
146 |
147 | ### Features
148 |
149 | * add experimental Windows installer ([83aab64](https://github.com/anacierdem/libdragon-docker/commit/83aab64f040eafc48f27c282e95de7477362f53c))
150 |
151 | ## [11.0.3](https://github.com/anacierdem/libdragon-docker/compare/v11.0.2...v11.0.3) (2024-02-16)
152 |
153 |
154 | ### Bug Fixes
155 |
156 | * **init.js:** do not run git init unnecessarily ([0a5688f](https://github.com/anacierdem/libdragon-docker/commit/0a5688f45ce0eb0ed91e5f5daedac24d24099afd)), closes [#69](https://github.com/anacierdem/libdragon-docker/issues/69)
157 |
158 | ## [11.0.2](https://github.com/anacierdem/libdragon-docker/compare/v11.0.1...v11.0.2) (2024-02-13)
159 |
160 |
161 | ### Bug Fixes
162 |
163 | * find parent git root in case running in a submodule ([e34c30b](https://github.com/anacierdem/libdragon-docker/commit/e34c30baea57cbd51e5330443dc54d5443662384))
164 |
165 | ## [11.0.1](https://github.com/anacierdem/libdragon-docker/compare/v11.0.0...v11.0.1) (2024-02-09)
166 |
167 |
168 | ### Bug Fixes
169 |
170 | * **init.js:** make sure providing --image flag uses the provided image ([965fe41](https://github.com/anacierdem/libdragon-docker/commit/965fe416322562a153bab9c5bf2f8c994fae562e)), closes [#66](https://github.com/anacierdem/libdragon-docker/issues/66)
171 |
172 | # [11.0.0](https://github.com/anacierdem/libdragon-docker/compare/v10.9.1...v11.0.0) (2023-11-11)
173 |
174 |
175 | ### Code Refactoring
176 |
177 | * **install.js:** remove docker image update support from install action ([8c768fd](https://github.com/anacierdem/libdragon-docker/commit/8c768fd1ec74e381a59acd28a6d1a69f75ed5de4))
178 | * **project-info.js:** drop support for legacy configuation file ([880f38d](https://github.com/anacierdem/libdragon-docker/commit/880f38da71aa0876cb04785e7c05814c406a4dbd))
179 |
180 |
181 | ### BREAKING CHANGES
182 |
183 | * **project-info.js:** The cli will no longer migrate the `.libdragon/docker-image` Either make sure you
184 | run the 10.x version once for your project or remove the file manually.
185 | * **install.js:** Providing --image/-i flag to the install action will now error out.
186 |
187 | ## [10.9.1](https://github.com/anacierdem/libdragon-docker/compare/v10.9.0...v10.9.1) (2023-06-01)
188 |
189 |
190 | ### Bug Fixes
191 |
192 | * release with the updated dependencies ([af7a8fb](https://github.com/anacierdem/libdragon-docker/commit/af7a8fb4eed767de37dcd98efc89e4ec584f77c4))
193 |
194 | # [10.9.0](https://github.com/anacierdem/libdragon-docker/compare/v10.8.2...v10.9.0) (2022-11-13)
195 |
196 |
197 | ### Features
198 |
199 | * add support for running inside a container ([ac9c80b](https://github.com/anacierdem/libdragon-docker/commit/ac9c80b9fa9edc1db5b58f64912add85fbfb369c))
200 |
201 | ## [10.8.2](https://github.com/anacierdem/libdragon-docker/compare/v10.8.1...v10.8.2) (2022-09-25)
202 |
203 |
204 | ### Bug Fixes
205 |
206 | * make ts more stricter and fix potential edge cases ([5890be0](https://github.com/anacierdem/libdragon-docker/commit/5890be0346a736611a60cf6ac01166a9a3179a34))
207 |
208 | ## [10.8.1](https://github.com/anacierdem/libdragon-docker/compare/v10.8.0...v10.8.1) (2022-09-23)
209 |
210 |
211 | ### Bug Fixes
212 |
213 | * **init.js:** prevent premature failure when the vendor target already exists ([9c76ece](https://github.com/anacierdem/libdragon-docker/commit/9c76eceb77b0e63effc8f6a2c8ca782eabd3f260))
214 | * **init.js:** show a custom error when the submodule operation fails ([bd40a9f](https://github.com/anacierdem/libdragon-docker/commit/bd40a9f38ef0e60e3c43e70b4d0e647a4db8a97a)), closes [#57](https://github.com/anacierdem/libdragon-docker/issues/57)
215 |
216 |
217 | ### Reverts
218 |
219 | * revert changelog removal ([e34b21d](https://github.com/anacierdem/libdragon-docker/commit/e34b21db8b17f5cdcebfdd45404aa1270d773595))
220 |
221 | # [10.8.0](https://github.com/anacierdem/libdragon-docker/compare/v10.7.1...v10.8.0) (2022-05-14)
222 |
223 |
224 | ### Features
225 |
226 | * **init.js:** add vendored library detection ([4465d08](https://github.com/anacierdem/libdragon-docker/commit/4465d08993b987d694de9675ea4bd9784e073cd5))
227 |
228 | ## [10.7.1] - 2022-04-20
229 |
230 | ### Fixed
231 |
232 | - Migrating from and old version was incorrectly erroring out to do an additional
233 | `init`, which is not necessarily required.
234 |
235 | ### Changed
236 |
237 | - Do not print usage information when a command fails.
238 |
239 | ## [10.7.0] - 2022-04-17
240 |
241 | ### Fixed
242 |
243 | - Logs properly goes to stderr now. Previously they were written to stdout. This
244 | means the id output of the `start` action is now written to stdout while we can
245 | also display other information on the terminal. This allowed enabling the docker
246 | logs for a more responsive experience. The output of `help` still goes to stdout.
247 |
248 | ### Added
249 |
250 | - Stdin consumption support. Now it is possible to pipe anything to `exec` and
251 | it will pass it through to the target. In case of no running container, it will
252 | keep a copy of the stdin stream until the docker process is ready. This enables
253 | piping in data from the host if ever needed for some reason. This enables usages
254 | like `cat file.txt | libdragon exec cat - | less`.
255 | - Automatically convert host paths into posix format so that the user can use
256 | the host's path autocompletion. It will also convert absolute host paths into
257 | relative container paths automatically. Previously all paths were assumed to be
258 | container paths relative to the location corresponding to the host cwd.
259 | Closes #24.
260 |
261 | ### Changed
262 |
263 | - Refactored process spawns.
264 | - Refactored main flow and separated parsing logic.
265 | - Reorder actions & correction on flag usage for help output.
266 | - Setting `--verbose` for `start` does not guarantee the only-id output anymore.
267 | - Refactored parameter parsing.
268 | - Update submdule for local development.
269 |
270 | ## [10.6.0] - 2022-04-09
271 | ### Fixed
272 |
273 | - Fix a path bug that would cause incorrect behaviour when the command is run
274 | deeper than a single level in the project folder.
275 | - Fix a potential issue where `build.sh`might be incorrectly found inexistant
276 | if the OS is picky about the paths to have native separators.
277 | - Only save project information when necessary. Previously actions like `help`
278 | were saving project info mistakenly.
279 |
280 | ### Added
281 |
282 | - `disasm` action to simplify disassembling ELF files generated by the toolchain.
283 | - `version` action to display current version.
284 | - `destroy` action to remove the libdragon project.
285 | - Additional documentation for flags.
286 | - Print duration information when verbose.
287 |
288 | ### Changed
289 |
290 | - Refactored out NPM related functions.
291 | - Moved usage parameters to respective actions files as a refactor.
292 | - It is possible to provide an absolute path to init `--directory` as long as it
293 | is inside the project directory. Previously it was possible to provide somewhere
294 | outside the project, but it would fail with an unexpected error.
295 | - Simplify saving mechanism. Each action now internally resolves into the data
296 | to save if any.
297 |
298 | ## [10.4.2] - 2022-04-03
299 |
300 | ### Fixed
301 |
302 | - Make sure actions depending on an `init` fail in a non-project directory to
303 | keep the project state consistent. This fixes #51.
304 | - `update` action now tries to update the toolchain image as well. Previously
305 | this was not the case contrary to what someone would expect. Considering it won't
306 | change the behaviour for non-latest images and the toolchain did not have any
307 | breaking changes for a long time, this is not considered a breaking change either.
308 | - `start` action was printing stuff other than the container id. It doesn't
309 | anymore.
310 | - Stop unnecessarily printing container id and a few messages related to updates.
311 | - Fix a potential race condition that might cause unexpected failures.
312 | - Correct some errors' exit codes.
313 | ### Added
314 |
315 | - A new exit code (`4`) to represent unexpected conditions.
316 |
317 | ### Changed
318 |
319 | - Deprecated providing the image flag for `install` action by displaying a
320 | warning and removing it from documentation, without changing behaviour even
321 | though it is higly unlikely this feature was ever used. It mainly exists for
322 | historical reasons and it wil be removed in next major release.
323 | - Update documentation to warn against changing strategy is a one way operation.
324 | - Update documentation to reflect `update` action changes.
325 | - Minor refactors.
326 | - Update submodule for local environment.
327 |
328 | ## [10.4.1] - 2022-03-23
329 |
330 | ### Fixed
331 |
332 | - Update the root makefile to utilize `SOURCE_DIR` for example builds. Then we are
333 | able to map container files to local files properly with a generic regex in the
334 | problem matcher. This fixes #13 and does not change any behaviour.
335 | - Add missing examples to the vscode run configurations.
336 | - Install and build libdragon related things in the container when `exec` and
337 | `make` causes a new container run. This was previously prevented on `v10.3.1`
338 | because it was unnecessarily delaying all exec operations when the container
339 | is started. Refactoring things allowed me to realize this can be improved
340 | instead of forcing the user to do a manual `install`.
341 | - Fix a potential issue that may cause the git commands to run in current folder
342 | instead of the project root.
343 | - Attach the error handler once for spawnProcess.
344 | - Update vulnerable dependencies.
345 |
346 | ### Added
347 |
348 | - `--directory` option to customize vendoring location.
349 | - `--strategy` option to select a vendoring strategy. Currently supported options
350 | are `submodule`, `subtree` and `manual`. The default is `submodule` and `manual`
351 | can be used to opt-out of auto vendoring. Useful if the user wants to utilize
352 | a different vendoring strategy and opt-out of the auto-managed git flows.
353 |
354 | ### Changed
355 |
356 | - Migrate to a json file for persistent project information.
357 | - Only save the configuration file on successful exit except for the initial
358 | migration.
359 | - Do not prevent init if there is a file named libdragon in the target folder.
360 | This used to cause problems on windows but I cannot reproduce it anymore
361 | with `2.33.1.windows.1`. It may be something caused by my old configuration.
362 | - Minor performance improvements.
363 |
364 | ## [10.3.1] - 2022-01-25
365 |
366 | ### Fixed
367 |
368 | - Do not try to parse arguments after exec/make. They used to get evaluated as
369 | libdragon paramaters previously, preventing passing down -v to the container
370 | make for example.
371 | - Docker image update issues
372 | - Attempt an image update whenever the image flag is provided. Previously this
373 | was only done if a different image name is provided, preventing the update of
374 | the latest image. Previously not providing an image name was behaving the same
375 | so this is not a breaking change.
376 | - Start action used to update the image if provided but this bug was already
377 | prevented by previous fixes by not accepting the image flag for actions other
378 | than init/update/install. Start action no longer tries to update the image
379 | regardless.
380 |
381 | ### Changed
382 |
383 | - Only accept the image flag for init, install, and update actions as documented.
384 | - Improve documentation for the `init` and `install` actions.
385 | - Do not attempt an `install` when running `exec`, just start the container. If
386 | there is a half-baked container, a manual `install` will potentially restore it.
387 | - Update libdragon.
388 |
389 | ### Added
390 |
391 | - Extra information for skipping the image flag when doing init for an already
392 | initialized project.
393 |
394 | ## [10.3.0] - 2022-01-20
395 |
396 | ### Changed
397 |
398 | - Update dependencies.
399 | - Detailed help output.
400 | - Move action descriptions to `libdragon help`.
401 | - Update libdragon to latest version for local build.
402 |
403 | ### Added
404 |
405 | - Shorthand flag support.
406 |
407 | ## [10.2.1] - 2021-10-14
408 |
409 | ### Changed
410 |
411 | - Updated ed64.
412 |
413 | ### Fixed
414 |
415 | - Fix skeleton project to match latest libdragon.
416 |
417 | ## [10.2.0] - 2021-10-10
418 |
419 | ### Added
420 |
421 | - Container discovery. The tool can now find a container even if `.git` is lost.
422 | - A few additional error messages for some potentially confusing cases such as
423 | already having a file with `libdragon` like name.
424 |
425 | ### Changed
426 |
427 | - Show more output for downloading the container and initial git operations.
428 | - Remove an extra log during initialization.
429 | - The submodule was always being initialized when a container is started. This
430 | was making some actions inconsistent. For example `install` or `make` action
431 | was trying to re-initialize the submodule unnecessarily. This is fixed by only
432 | initializing it with the `init` action. If any of those need to re-init the
433 | container, now they assume there is an intact libdragon folder to use.
434 | - Similarly a git repository is not initialized unnecessarily anymore.
435 | - `update` and `install` are now able to start containers if necessary.
436 | - Always try to copy skeleton files, they won't overwrite anything already.
437 | - Do not re-initialize if there is a `.libdragon` folder. We now only try to
438 | start it in this case. If it is not a complete container, it can probably be
439 | recovered by a `libdragon install` or `libdragon update`.
440 |
441 | ### Fixed
442 |
443 | - Fix wording for libdragon install on the container.
444 | - Improve image name persitence such that the tool finds and updates it more
445 | consistently.
446 |
447 | ## [10.1.0] - 2021-10-07
448 |
449 | ### Added
450 |
451 | - `exec` action to execute arbitrary commands in the container with TTY support.
452 | This also improves the color output support as docker now knows it is using TTY.
453 | - Add more verbose logging for skeleton project copy.
454 |
455 | ### Changed
456 |
457 | - Removed partially not working `--byte-swap` option. It does not already work
458 | with the new libdragon build system so there is no need keeping it in the tool.
459 |
460 | ### Fixed
461 |
462 | - Publish skeleton project files to NPM.
463 |
464 | ## [10.0.0] - 2021-10-04
465 |
466 | ### Changed
467 |
468 | - A complete re-write of the tool. Check documentation for the new usage. It is
469 | much more straightforward to use now. `libdragon make` behaves almost the same.
470 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2020 Ali Naci Erdem
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # TODO: I should also build the tools as a dependency here. Currently even if I
2 | # do that, the build system will not pick it up.
3 |
4 | .PHONY: bench
5 | bench: libdragon-install
6 | $(MAKE) -C ./src SOURCE_DIR=$(CURDIR)/src
7 | .PHONY: clean-bench
8 | clean-bench:
9 | $(MAKE) -C ./src clean
10 |
11 | examples: libdragon-install
12 | $(MAKE) -C ./libdragon/examples BASE_DIR=$(CURDIR)/libdragon/examples
13 |
14 | .PHONY: tests
15 | tests: libdragon-install
16 | $(MAKE) -C ./libdragon/tests SOURCE_DIR=$(CURDIR)/libdragon/tests
17 |
18 | .PHONY: libdragon-install
19 | libdragon-install:
20 | $(MAKE) -C ./libdragon install SOURCE_DIR=$(CURDIR)/libdragon/src
21 |
22 | .PHONY: clean
23 | clean: clean-bench
24 | $(MAKE) -C ./libdragon clean
25 | $(MAKE) -C ./libdragon/tests clean
26 | $(MAKE) -C ./libdragon/examples clean
27 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Docker Libdragon
2 |
3 | [](https://github.com/anacierdem/libdragon-docker/actions/workflows/release.yml)
4 |
5 | This is a wrapper for a docker container to make managing the libdragon toolchain easier. It has the additional advantage that libdragon toolchain and library can be installed on a per-project basis instead of managing system-wide installations.
6 |
7 | ## Prerequisites
8 |
9 | You should have [docker](https://www.docker.com/products/docker-desktop) (`>= 27.2.0`) and [git](https://git-scm.com/downloads) installed on your system.
10 |
11 | ## Installation
12 |
13 | This is primarily a node.js script which is also packaged as an executable. You have a few options:
14 |
15 | ### installer
16 |
17 | Download the [windows installer](https://github.com/anacierdem/libdragon-docker/releases/latest) and run it. This option is currently only available on Windows.
18 |
19 |
20 | Detailed instructions
21 |
22 | - Download the Windows installer and run it
23 | - It can show a "Windows protected your PC" warning. Click "More info" and "Run anyway".
24 | - You should now be able to use the `libdragon` on a command line or PowerShell.
25 | - To update it with a new version, download a newer installer and repeat the steps above.
26 |
27 |
28 |
29 | ### pre-built executable
30 |
31 | Download the [pre-built executable](https://github.com/anacierdem/libdragon-docker/releases/latest) for your system and put it somewhere on your PATH. It is discouraged to put it in your project folder.
32 |
33 |
34 | Windows instructions
35 |
36 | - Download the Windows executable and copy it to `C:\bin`
37 | - Press `Windows + R` key combination and then enter `rundll32 sysdm.cpl,EditEnvironmentVariables`
38 | - In the `Environment Variables` window find the `Path` variable under `User variables for `
39 | - Double click it and add a new entry as `C:\bin`
40 | - Restart your computer.
41 | - You should now be able to use the `libdragon` on a command line or PowerShell.
42 | - To update it with a new version, just replace the file in `C:\bin`
43 |
44 |
45 |
46 |
47 | MacOS instructions
48 |
49 | - Download the MacOS executable and copy it to `/usr/local/bin`
50 | - Right click it and choose `Open`.
51 | - It will show a warning, approve it by clicking `Open` again. You can close the newly opened terminal window.
52 | - You should now be able to use the `libdragon` command.
53 | - To update it with a new version, replace the file in `/usr/local/bin` and repeat the other steps.
54 |
55 |
56 |
57 |
58 | Linux instructions
59 |
60 | - You should already know this :)
61 |
62 |
63 |
64 | ### via NPM
65 |
66 | Install [node.js](https://nodejs.org/en/download/) (`>= 22`) and install `libdragon` as a global NPM package;
67 |
68 | ```bash
69 | npm install -g libdragon
70 | ```
71 |
72 | To update the tool to the latest, do `npm i -g libdragon@latest`.
73 |
74 | ## Quick Guide
75 |
76 | Navigate to the folder you want to initialize your project and invoke libdragon;
77 |
78 | ```bash
79 | libdragon init
80 | ```
81 |
82 | On first `init` an example project will be created, the container will be downloaded, started and latest libdragon will get installed on it with all the example ROMs built. You can find all the example ROMs in the `libdragon/examples` folder.
83 |
84 | The container's `make` can be invoked on your current working directory via;
85 |
86 | ```bash
87 | libdragon make
88 | ```
89 |
90 | Any additonal parameters are passed down to the actual make command. You can work on the files simultaneously with the docker container and any built artifacts will be available in project directory as it is mounted on the container.
91 |
92 | To update the library and rebuild/install all the artifacts;
93 |
94 | ```bash
95 | libdragon update
96 | ```
97 |
98 | In general, you can invoke libdragon as follows;
99 |
100 | libdragon [flags]
101 |
102 | Run `libdragon help [action]` for more details on individual actions.
103 |
104 | ## Recipes
105 |
106 | ### Using a different libdragon branch
107 |
108 | Use the `--branch` flag to set up a custom libdragon branch when initializing your project:
109 |
110 | ```bash
111 | libdragon init --branch unstable
112 | ```
113 |
114 | This will use the `unstable` toolchain and code.
115 |
116 | ### Switching to a different branch of libdragon
117 |
118 | On an already initialized project, switch the submodule to the desired branch:
119 |
120 | ```bash
121 | git -C ./libdragon checkout opengl
122 | libdragon install
123 | ```
124 |
125 | If your changes are on a different remote, then you will need to manage your git remotes as usual. If you also want to update the remote tracking branch for the submodule, run:
126 |
127 | ```bash
128 | git submodule set-branch -b opengl libdragon
129 | ```
130 |
131 | This will update the branch on `.gitmodules` and if you commit that change, subsequent initializations will use the `opengl` branch by default.
132 |
133 | ### Testing changes on libdragon
134 |
135 | As libdragon is an actively developed library, you may find yourself at a position where you want to change a few things on it and see how it works. In general, if you modify the files in `libdragon` folder of your project, you can install that version to the docker container by simply running:
136 |
137 | ```bash
138 | libdragon install
139 | ```
140 |
141 | This will update all the artifacts in your container and your new code will start linking against the new version when you re-build it via `libdragon make`. The build system should pick up the change in the library and re-compile the dependent files.
142 |
143 | Instead of depending on the above command, you can automatically re-build the library by making it a make dependency in your project:
144 |
145 | ```makefile
146 | libdragon-install:
147 | $(MAKE) -C ./libdragon install
148 | ```
149 |
150 | If your build now depends on `libdragon-install`, it will force an install (which should be pretty quick if you don't have changes) and force the build system to rebuild your project when necessary.
151 |
152 | If you clone this repository, this setup is pretty much ready for you. Make sure you have a working libdragon setup and you get the submodules (e.g `git submodule update --init`). Then you can run `libdragon make bench` to execute the code in `./src` with your library changes. Also see [test bench](#local-test-bench).
153 |
154 | When managing your changes to the library, you have a few options:
155 |
156 | #### **Using `submodule` vendor strategy.**
157 |
158 | To be able to share your project with the library change, you would need to push it somewhere public and make sure it is cloned properly by your contributors. This is not recommended for keeping track of your changes but is very useful if you plan to contribute it back to upstream. In the latter case, you can push your submodule branch to your fork and easily open a PR.
159 |
160 | #### **Using `subtree` vendor strategy.**
161 |
162 | To be able to share your project with the library change, you just commit your changes. This is very useful for keeping track of your changes specific to your project. On the other hand this is not recommended if you plan to contribute it back to upstream because you will need to make sure your libdragon commits are isolated and do the juggling when pushing it somewhere via `git subtree push`.
163 |
164 | ## Working on this repository
165 |
166 | After cloning this repository on a system with node.js (`>= 18`) & docker (`>= 27.2.0`), in this repository's root do;
167 |
168 | ```bash
169 | npm install
170 | ```
171 |
172 | This will install all necessary NPM dependencies. Now it is time to get the original libdragon repository. (you can also clone this repository with `--recurse-submodules`)
173 |
174 | ```bash
175 | git submodule update --init
176 | ```
177 |
178 | Then run;
179 |
180 | ```bash
181 | npm run libdragon -- init
182 | ```
183 |
184 | to download the pre-built toolchain image, start and initialize it. This will also install [test bench](#local-test-bench) dependencies into the container if any.
185 |
186 | Now you will be able to work on the files simultaneously with the docker container and any built binaries will be available in your workspace as it is mounted on the container.
187 |
188 | There is a root `Makefile` making deeper makefiles easier with these recipes;
189 |
190 | bench: build the test bench (see below)
191 | examples: build libdragon examples
192 | tests: build the test ROM
193 | libdragon-install: build and install libdragon
194 | clean-bench: clean the test bench (see below)
195 | clean: clean everything and start from scratch
196 |
197 | For example, to re-build the original libdragon examples do;
198 |
199 | ```bash
200 | npm run libdragon -- make examples
201 | ```
202 |
203 | Similarly to run the `clean` recipe, run;
204 |
205 | ```bash
206 | npm run libdragon -- make clean
207 | ```
208 |
209 | > [!IMPORTANT]
210 | > Keep in mind that `--` is necessary for actual arguments when using npm scripts.
211 |
212 |
213 | To update the submodule and re-build everything;
214 |
215 | ```bash
216 | npm run libdragon -- update
217 | ```
218 |
219 | ### Local test bench
220 |
221 | The root `bench` recipe is for building the code in root `src` folder. This is a quick way of testing your libdragon changes or a sample code built around libdragon, and is called the test bench. This recipe together with `examples` and `tests` recipes will build and install libdragon automatically as necessary. Thus, they will always produce a rom image using the libdragon code in the repository via their make dependencies, which is ideal for experimenting with libdragon itself.
222 |
223 | There are also vscode launch configurations to quickly build and run the examples, tests and the bench. If you have [ares](https://ares-emu.net/) on your `PATH`, the launch configurations ending in `(emu)` will start it automatically. For the examples configuration, you can navigate to the relevant `.c` file and `Run selected example` will start it most of the time. In some cases, the output ROM name may not match the `.c` file and in those cases, you can select the ROM file instead and it should work.
224 |
225 | > [!NOTE]
226 | > This repository also uses [UNFLoader](https://github.com/buu342/N64-UNFLoader), so you can use the launch configurations without `(emu)` to run the code if you have a supported flashcart plugged in and have `UNFLoader` executable on your `PATH`.
227 | >
228 | > The special `Debug Test Bench (emu)` configuration will start ares with remote debugging for the test bench if you have `gdb-multiarch` executable on your `PATH`. It should automatically break in your `main` function.
229 |
230 | You can clean everything with the `clean` recipe/task (open the command palette and choose `Run Task -> clean`).
231 |
232 | ### Developing the tool itself
233 |
234 | For a quick development loop it really helps linking the code in this repository as the global libdragon installation. To do this run;
235 |
236 | ```bash
237 | npm link
238 | ```
239 |
240 | in the root of the repository. Once you do this, running `libdragon` will use the code here rather than the actual npm installation. Then you can test your changes in the libdragon project here or elsewhere on your computer. This setup is automatically done if you use the [devcontainer](#experimental-devcontainer-support).
241 |
242 | When you are happy with your changes, you can verify you conform to the coding standards via:
243 |
244 | ```bash
245 | npm run format-check
246 | npm run lint-check
247 | ```
248 |
249 | You can auto-fix applicable errors by running `format` and `lint` scripts instead. Additionally, typescript is used as the type system. To be able to get away with transpiling the code during development, jsDoc flavor of types are used instead of inline ones. To check your types, run:
250 |
251 | ```bash
252 | npm run tsc
253 | ```
254 |
255 | To run the test suite:
256 |
257 | ```bash
258 | npm run test
259 | ```
260 |
261 | This repository uses [`semantic-release`](https://github.com/semantic-release/semantic-release) and manages releases from specially formatted commit messages. To simplify creating them you can use:
262 |
263 | ```bash
264 | npx cz
265 | ```
266 |
267 | It will create a `semantic-release` compatible commit from your current staged changes.
268 |
269 | ### Experimental devcontainer support
270 |
271 | The repository provides a configuration (in `.devcontainer`) so that IDEs that support it can create and run the Docker container for you. Then, you can start working on it as if you are working on a machine with libdragon installed.
272 |
273 | With the provided setup, you can continue using the cli in the container and it will work for non-container specific actions like `install`, `disasm` etc. You don't have to use the cli in the container, but you can. In general it will be easier and faster to just run `make` in the container but this setup is included to ease developing the cli as well.
274 |
275 | To create your own dev container backed project, you can use the contents of the `.devcontainer` folder as reference. You don't need to include nodejs or the cli and you can just run `build.sh` as `postCreateCommand`. See the `devcontainer.json` for more details. As long as your container have the `DOCKER_CONTAINER` environment variable, the tool can work inside a container.
276 |
277 | #### Caveats
278 |
279 | - In the devcontainer, uploading via USB will not work.
280 | - Error matching is not yet tested.
281 | - Ideally the necessary extensions should be automatically installed. This is not configured yet.
282 |
283 |
284 | vscode instructions
285 |
286 | - Make sure you have the [Dev container extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed and you fulfill its [requirements](https://code.visualstudio.com/docs/devcontainers/containers).
287 | - Clone this repository with `--recurse-submodules` or run `git submodule update --init`.
288 | - Open command palette and run `Dev Containers: Reopen in container`.
289 | - It will prepare the container and open it in the editor.
290 |
291 |
292 | ## As an NPM dependency
293 |
294 | You can install libdragon as an NPM dependency by `npm install libdragon --save` in order to use docker in your N64 projects. A `libdragon` command similar to global installation is provided that can be used in your NPM scripts as follows;
295 | ```json
296 | "scripts": {
297 | "prepare": "libdragon init"
298 | "build": "libdragon make",
299 | "clean": "libdragon make clean"
300 | }
301 | ```
302 |
303 | ## Funding
304 |
305 | If this tool helped you, consider supporting its development by sponsoring it!
306 |
307 |
--------------------------------------------------------------------------------
/bundle.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env zx
2 | // TODO: enable type checking for this file.
3 | import fs from 'node:fs/promises';
4 | import 'zx/globals';
5 |
6 | import { usePowerShell } from 'zx';
7 |
8 | /* eslint-disable no-undef */
9 |
10 | if (!process.argv[2]) {
11 | throw new Error('Please provide a version number.');
12 | }
13 |
14 | if (process.platform === 'win32') {
15 | usePowerShell();
16 | // for PowerShell compatibility
17 | $.prefix = '$ErrorActionPreference = "Stop";';
18 | $.postfix = '; exit $LastExitCode';
19 | }
20 |
21 | const { stdout: versionString } = await $`./build/libdragon.exe version`;
22 |
23 | if (versionString.trim() !== `libdragon-cli v${process.argv[2]} (sea)`) {
24 | throw new Error(
25 | `Version mismatch! Expected: libdragon-cli v${
26 | process.argv[2]
27 | } (sea), got: ${versionString.trim()}`
28 | );
29 | }
30 |
31 | await fs.mkdir('./tmp').catch(() => {});
32 |
33 | await fs.rename('./build/libdragon-linux', './tmp/libdragon');
34 | await $`tar -C ./tmp -cvzf libdragon-linux-x86_64.tar.gz libdragon`;
35 | await fs.rm('./tmp/libdragon');
36 |
37 | await fs.rename('./build/libdragon-macos', './tmp/libdragon');
38 | await $`tar -C ./tmp -cvzf libdragon-macos-arm64.tar.gz libdragon`;
39 | await fs.rm('./tmp/libdragon');
40 |
41 | await fs.rename('./build/libdragon.exe', './tmp/libdragon.exe');
42 | await $`Compress-Archive -Path ./tmp/libdragon.exe -DestinationPath libdragon-win-x86_64.zip`;
43 | // Do not remove the win executable, it will be used for building the installer
44 |
45 | await $`iscc setup.iss /dMyAppVersion=${process.argv[2]}`;
46 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from 'globals';
2 | import js from '@eslint/js';
3 |
4 | export default [
5 | js.configs.recommended,
6 | {
7 | languageOptions: {
8 | ecmaVersion: 2022,
9 | sourceType: 'module',
10 | globals: {
11 | ...globals.node,
12 | ...globals['2022'],
13 | },
14 | },
15 | rules: {
16 | 'no-console': 2,
17 | },
18 | },
19 | ];
20 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const chalk = require('chalk').stderr;
4 |
5 | const {
6 | STATUS_OK,
7 | STATUS_BAD_PARAM,
8 | STATUS_ERROR,
9 | STATUS_VALIDATION_ERROR,
10 | } = require('./modules/constants');
11 | const { globals } = require('./modules/globals');
12 | const {
13 | CommandError,
14 | ParameterError,
15 | ValidationError,
16 | log,
17 | } = require('./modules/helpers');
18 | const { parseParameters } = require('./modules/parameters');
19 | const { readProjectInfo, writeProjectInfo } = require('./modules/project-info');
20 |
21 | // Note: it is not possible to merge these type definitions in a single comment
22 | /**
23 | * @template {any} [U=any]
24 | * @typedef {(U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never} UnionToIntersection
25 | */
26 | /**
27 | * @template {any} [K=any]
28 | * never does not break the call `info.options.CURRENT_ACTION.fn`
29 | * @typedef {[K] extends [UnionToIntersection] ? any : unknown} NoUnion
30 | * @typedef {NoUnion[0], undefined>>} EitherCLIOrLibdragonInfo
31 | */
32 |
33 | // This is overridden when building for SEA. When running from local or NPM,
34 | // the package.json version is used by the version action. esbuild will give
35 | // a warning for this assignment, but it works as expected.
36 | globalThis.VERSION = '';
37 |
38 | parseParameters(process.argv)
39 | .then(readProjectInfo)
40 | .then((info) => {
41 | return info.options.CURRENT_ACTION.fn(
42 | /** @type {EitherCLIOrLibdragonInfo} */ (info)
43 | );
44 | // This type make sure a similar restriction to this code block is enforced
45 | // without adding unnecessary javascript.
46 | // return isProjectAction(info)
47 | // ? info.options.CURRENT_ACTION.fn(info)
48 | // : info.options.CURRENT_ACTION.fn(info);
49 | })
50 | .catch((e) => {
51 | if (e instanceof ParameterError) {
52 | log(chalk.red(e.message));
53 | process.exit(STATUS_BAD_PARAM);
54 | }
55 | if (e instanceof ValidationError) {
56 | log(chalk.red(e.message));
57 | process.exit(STATUS_VALIDATION_ERROR);
58 | }
59 |
60 | const userTargetedError = e instanceof CommandError && e.userCommand;
61 |
62 | // Show additional information to user if verbose or we did a mistake
63 | if (globals.verbose || !userTargetedError) {
64 | log(chalk.red(globals.verbose ? e.stack : e.message));
65 | }
66 |
67 | // Print the underlying error out only if not verbose and we did a mistake
68 | // user errors will already pipe their stderr. All command errors also go
69 | // to the stderr when verbose
70 | if (!globals.verbose && !userTargetedError && e.out) {
71 | process.stderr.write(chalk.red(`Command error output:\n${e.out}`));
72 | }
73 |
74 | // Try to exit with underlying code
75 | if (userTargetedError) {
76 | process.exit(e.code || STATUS_ERROR);
77 | }
78 |
79 | // We don't have a user targeted error anymore, we did a mistake for sure
80 | process.exit(STATUS_ERROR);
81 | })
82 | // Everything was done, update the configuration file if not exiting early
83 | .then(writeProjectInfo)
84 | .finally(() => {
85 | log(`Took: ${process.uptime()}s`, true);
86 | process.exit(STATUS_OK);
87 | });
88 |
--------------------------------------------------------------------------------
/jest.config.cjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | testMatch: [
3 | '**/__tests__/**/*.(m)[jt]s?(x)',
4 | '**/?(*.)+(spec|test).(m)[jt]s?(x)',
5 | ],
6 | transform: {},
7 | };
8 |
9 | module.exports = config;
10 |
--------------------------------------------------------------------------------
/libdragon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anacierdem/libdragon-docker/84e0c0615c264b3e48432abe6d44f030d2e6b0f2/libdragon.ico
--------------------------------------------------------------------------------
/modules/actions/destroy.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs/promises');
2 | const path = require('path');
3 |
4 | const { destroyContainer } = require('../utils');
5 | const { CONFIG_FILE, LIBDRAGON_PROJECT_MANIFEST } = require('../constants');
6 | const { fileExists, dirExists, log, ValidationError } = require('../helpers');
7 | const chalk = require('chalk');
8 |
9 | /**
10 | * @param {import('../project-info').LibdragonInfo} libdragonInfo
11 | */
12 | const destroy = async (libdragonInfo) => {
13 | if (process.env.DOCKER_CONTAINER) {
14 | throw new ValidationError(
15 | `Not possible to destroy the container from inside.`
16 | );
17 | }
18 |
19 | await destroyContainer(libdragonInfo);
20 |
21 | const projectPath = path.join(libdragonInfo.root, LIBDRAGON_PROJECT_MANIFEST);
22 | const configPath = path.join(projectPath, CONFIG_FILE);
23 |
24 | if (await fileExists(configPath)) {
25 | await fs.rm(configPath);
26 | }
27 | if (await dirExists(projectPath)) {
28 | await fs.rmdir(projectPath);
29 | }
30 |
31 | log(chalk.green('Done cleanup.'));
32 | };
33 |
34 | module.exports = /** @type {const} */ ({
35 | name: 'destroy',
36 | fn: destroy,
37 | forwardsRestParams: false,
38 | usage: {
39 | name: 'destroy',
40 | summary: 'Do clean-up for current project.',
41 | description: `Removes libdragon configuration from current project and removes any known containers but will not touch previously vendored files. \`libdragon\` will not work anymore for this project.`,
42 | },
43 | });
44 |
--------------------------------------------------------------------------------
/modules/actions/disasm.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs/promises');
3 |
4 | const { fn: exec } = require('./exec');
5 | const {
6 | ValidationError,
7 | toPosixPath,
8 | dirExists,
9 | fileExists,
10 | ParameterError,
11 | } = require('../helpers');
12 |
13 | /**
14 | * @param {string} stop
15 | * @param {string} start
16 | * @returns {Promise}
17 | */
18 | const findElf = async (stop, start = '.') => {
19 | start = path.resolve(start);
20 |
21 | const files = await fs.readdir(start);
22 |
23 | const buildDir = path.join(start, 'build');
24 | if (await dirExists(buildDir)) {
25 | const elfFile = await findElf(buildDir, buildDir);
26 | if (elfFile) return elfFile;
27 | }
28 |
29 | const elfFiles = files.filter((name) => name.endsWith('.elf'));
30 |
31 | if (elfFiles.length > 1) {
32 | throw new ValidationError(
33 | `Multiple ELF files found in ${path.resolve(
34 | start
35 | )}. Use --file to specify.`
36 | );
37 | }
38 |
39 | if (elfFiles.length === 1) {
40 | return path.join(start, elfFiles[0]);
41 | }
42 |
43 | const parent = path.join(start, '..');
44 | if (start !== stop) {
45 | return await findElf(stop, parent);
46 | } else {
47 | throw new ValidationError(`No ELF files found. Use --file to specify.`);
48 | }
49 | };
50 |
51 | /**
52 | * @param {import('../project-info').LibdragonInfo} info
53 | */
54 | const disasm = async (info) => {
55 | let elfPath;
56 | if (info.options.FILE) {
57 | if (path.relative(info.root, info.options.FILE).startsWith('..')) {
58 | throw new ParameterError(
59 | `Provided file ${info.options.FILE} is outside the project directory.`,
60 | info.options.CURRENT_ACTION.name
61 | );
62 | }
63 | if (!(await fileExists(info.options.FILE)))
64 | throw new ParameterError(
65 | `Provided file ${info.options.FILE} does not exist`,
66 | info.options.CURRENT_ACTION.name
67 | );
68 | elfPath = info.options.FILE;
69 | }
70 | elfPath = elfPath ?? (await findElf(info.root));
71 |
72 | const haveSymbol =
73 | info.options.EXTRA_PARAMS.length > 0 &&
74 | !info.options.EXTRA_PARAMS[0].startsWith('-');
75 |
76 | const finalArgs = haveSymbol
77 | ? [
78 | `--disassemble=${info.options.EXTRA_PARAMS[0]}`,
79 | ...info.options.EXTRA_PARAMS.slice(1),
80 | ]
81 | : info.options.EXTRA_PARAMS;
82 |
83 | const intermixSourceParams =
84 | info.options.EXTRA_PARAMS.length === 0 || haveSymbol ? ['-S'] : [];
85 |
86 | return await exec({
87 | ...info,
88 | options: {
89 | ...info.options,
90 | EXTRA_PARAMS: [
91 | 'mips64-elf-objdump',
92 | ...finalArgs,
93 | ...intermixSourceParams,
94 | toPosixPath(path.relative(process.cwd(), elfPath)),
95 | ],
96 | },
97 | });
98 | };
99 |
100 | module.exports = /** @type {const} */ ({
101 | name: 'disasm',
102 | fn: disasm,
103 | forwardsRestParams: true,
104 | usage: {
105 | name: 'disasm [symbol|flags]',
106 | summary: 'Disassemble the nearest *.elf file.',
107 | description: `Executes \`objdump\` for the nearest *.elf file starting from the working directory, going up. If there is a \`build\` directory in the searched paths, checks inside it as well. Any extra flags are passed down to \`objdump\` before the filename.
108 |
109 | If a symbol name is provided after the action, it is converted to \`--disassembly=\` and intermixed source* (\`-S\`) is automatically applied. This allows disassembling a single symbol by running;
110 |
111 | \`libdragon disasm main\`
112 |
113 | Again, any following flags are forwarded down. If run without extra symbol/flags, disassembles whole ELF with \`-S\` by default.
114 |
115 | Must be run in an initialized libdragon project.
116 |
117 | * Note that to be able to see the source, the code must be built with \`D=1\``,
118 |
119 | optionList: [
120 | {
121 | name: 'file',
122 | description:
123 | 'Provide a specific ELF file relative to current working directory and inside the libdragon project.',
124 | alias: 'f',
125 | typeLabel: ' ',
126 | },
127 | ],
128 | },
129 | });
130 |
--------------------------------------------------------------------------------
/modules/actions/exec.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const { PassThrough } = require('stream');
3 |
4 | const { CONTAINER_TARGET_PATH } = require('../constants');
5 | const {
6 | log,
7 | dockerExec,
8 | toPosixPath,
9 | fileExists,
10 | dirExists,
11 | CommandError,
12 | spawnProcess,
13 | } = require('../helpers');
14 |
15 | const { start } = require('./start');
16 | const { dockerHostUserParams } = require('../docker-utils');
17 | const { installDependencies } = require('../utils');
18 |
19 | /**
20 | * @param {import('../project-info').LibdragonInfo} libdragonInfo
21 | */
22 | function dockerRelativeWorkdir(libdragonInfo) {
23 | return (
24 | CONTAINER_TARGET_PATH +
25 | '/' +
26 | toPosixPath(path.relative(libdragonInfo.root, process.cwd()))
27 | );
28 | }
29 |
30 | /**
31 | * @param {import('../project-info').LibdragonInfo} libdragonInfo
32 | */
33 | function dockerRelativeWorkdirParams(libdragonInfo) {
34 | return ['--workdir', dockerRelativeWorkdir(libdragonInfo)];
35 | }
36 |
37 | /**
38 | * @param {import('../project-info').LibdragonInfo} info
39 | */
40 | const exec = async (info) => {
41 | const parameters = info.options.EXTRA_PARAMS.slice(1);
42 | log(
43 | `Running ${info.options.EXTRA_PARAMS[0]} at ${dockerRelativeWorkdir(
44 | info
45 | )} with [${parameters}]`,
46 | true
47 | );
48 |
49 | // Don't even bother here, we are already in a container.
50 | if (process.env.DOCKER_CONTAINER) {
51 | const enableTTY = Boolean(process.stdout.isTTY && process.stdin.isTTY);
52 | await spawnProcess(info.options.EXTRA_PARAMS[0], parameters, {
53 | userCommand: true,
54 | // Inherit stdin/out in tandem if we are going to disable TTY o/w the input
55 | // stream remains inherited by the node process while the output pipe is
56 | // waiting data from stdout and it behaves like we are still controlling
57 | // the spawned process while the terminal is actually displaying say for
58 | // example `less`.
59 | inheritStdout: enableTTY,
60 | inheritStdin: enableTTY,
61 | inheritStderr: true,
62 | });
63 | return info;
64 | }
65 |
66 | const stdin = new PassThrough();
67 |
68 | /** @type {string[]} */
69 | const paramsWithConvertedPaths = await Promise.all(
70 | parameters.map(async (item) => {
71 | if (item.startsWith('-')) {
72 | return item;
73 | }
74 | if (
75 | item.includes(path.sep) &&
76 | ((await fileExists(item)) || (await dirExists(item)))
77 | ) {
78 | return toPosixPath(
79 | path.isAbsolute(item) ? path.relative(process.cwd(), item) : item
80 | );
81 | }
82 | return item;
83 | })
84 | );
85 |
86 | /**
87 | *
88 | * @param {import('../project-info').LibdragonInfo} libdragonInfo
89 | * @param {import('../helpers').SpawnOptions} opts
90 | * @returns
91 | */
92 | const tryCmd = (libdragonInfo, opts = {}) => {
93 | const enableTTY = Boolean(process.stdout.isTTY && process.stdin.isTTY);
94 | return (
95 | libdragonInfo.containerId &&
96 | dockerExec(
97 | libdragonInfo,
98 | [
99 | ...dockerRelativeWorkdirParams(libdragonInfo),
100 | ...dockerHostUserParams(libdragonInfo),
101 | ],
102 | [libdragonInfo.options.EXTRA_PARAMS[0], ...paramsWithConvertedPaths],
103 | {
104 | userCommand: true,
105 | // Inherit stdin/out in tandem if we are going to disable TTY o/w the input
106 | // stream remains inherited by the node process while the output pipe is
107 | // waiting data from stdout and it behaves like we are still controlling
108 | // the spawned process while the terminal is actually displaying say for
109 | // example `less`.
110 | inheritStdout: enableTTY,
111 | inheritStdin: enableTTY,
112 | // spawnProcess defaults does not apply to dockerExec so we need to
113 | // provide these explicitly here.
114 | inheritStderr: true,
115 | ...opts,
116 | }
117 | )
118 | );
119 | };
120 |
121 | let started = false;
122 | /**
123 | * @param {import('fs').ReadStream=} stdin
124 | */
125 | const startOnceAndCmd = async (stdin) => {
126 | if (!started) {
127 | const newId = await start(info);
128 | const newInfo = {
129 | ...info,
130 | containerId: newId,
131 | };
132 | started = true;
133 |
134 | // Re-install vendors on new container if one was created upon start
135 | // Ideally we would want the consumer to handle dependencies and rebuild
136 | // libdragon if necessary. Currently this saves the day with a little bit
137 | // extra waiting when the container is deleted.
138 | if (info.containerId !== newId) {
139 | await installDependencies(newInfo);
140 | }
141 | await tryCmd(newInfo, { stdin });
142 | return newId;
143 | }
144 | };
145 |
146 | if (!info.containerId) {
147 | log(`Container does not exist for sure, restart`, true);
148 | await startOnceAndCmd();
149 | return info;
150 | }
151 |
152 | try {
153 | // Start collecting stdin data on an auxiliary stream such that we can pipe
154 | // it back to the container process if this fails the first time. Then the
155 | // initial failed docker process would eat up the input stream. Here, we pass
156 | // it to the target process eventually via startOnceAndCmd. If the input
157 | // stream is from a TTY, spawnProcess will already inherit it. Listening
158 | // to the stream here causes problems for unknown reasons.
159 | !process.stdin.isTTY && process.stdin.pipe(stdin);
160 | await tryCmd(info, {
161 | // Disable the error tty to be able to read the error message in case
162 | // the container is not running
163 | inheritStderr: false,
164 | // In the first run, pass the stdin to the process if it is not a TTY
165 | // o/w we loose a user input unnecesarily somehow.
166 | stdin:
167 | (!process.stdin.isTTY || undefined) &&
168 | /** @type {import('fs').ReadStream} */ (
169 | /** @type {unknown} */ (process.stdin)
170 | ),
171 | });
172 | } catch (e) {
173 | if (!(e instanceof CommandError)) {
174 | throw e;
175 | }
176 | if (
177 | // TODO: is there a better way?
178 | !e.out.toString().includes(info.containerId)
179 | ) {
180 | throw e;
181 | }
182 | await startOnceAndCmd(
183 | /** @type {import('fs').ReadStream} */ (/** @type {unknown} */ (stdin))
184 | );
185 | }
186 | return info;
187 | };
188 |
189 | module.exports = /** @type {const} */ ({
190 | name: 'exec',
191 | fn: exec,
192 | forwardsRestParams: true,
193 | showStatus: true,
194 | usage: {
195 | name: 'exec ',
196 | summary: 'Execute given command in the current directory.',
197 | description: `Executes the given command in the container passing down any arguments provided. If you change your host working directory, the command will be executed in the corresponding folder in the container as well.
198 |
199 | This action will first try to execute the command in the container and if the container is not accessible, it will attempt a complete \`start\` cycle.
200 |
201 | This will properly passthrough your TTY if you have one. So by running \`libdragon exec bash\` you can start an interactive bash session with full TTY support.
202 |
203 | Must be run in an initialized libdragon project.`,
204 | },
205 | });
206 |
--------------------------------------------------------------------------------
/modules/actions/help.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk');
2 | const commandLineUsage = require('command-line-usage');
3 |
4 | const { print } = require('../helpers');
5 |
6 | /**
7 | * @param {import('../project-info').CLIInfo} info
8 | */
9 | const printUsage = async (info) => {
10 | const actions = require('./');
11 | const globalOptionDefinitions = [
12 | {
13 | name: 'verbose',
14 | description:
15 | 'Be verbose. This will print all commands dispatched and their outputs as well. Will also start printing error stack traces.',
16 | alias: 'v',
17 | typeLabel: ' ',
18 | },
19 | ];
20 |
21 | const optionDefinitions = [
22 | {
23 | name: 'image',
24 | description:
25 | 'Use this flag to provide a custom image to use instead of the default. It should include the toolchain at `/n64_toolchain`. It will cause a re-initialization of the container if a different image is provided.\n',
26 | alias: 'i',
27 | typeLabel: '',
28 | group: 'docker',
29 | },
30 | {
31 | name: 'directory',
32 | description: `Directory where libdragon files are expected. It must be inside the libdragon project as it will be mounted on the docker container. The cli will create and manage it when using a non-manual strategy. Defaults to \`./libdragon\` if not provided.\n`,
33 | alias: 'd',
34 | typeLabel: '',
35 | group: 'vendoring',
36 | },
37 | {
38 | name: 'strategy',
39 | description: `libdragon Vendoring strategy. Defaults to \`submodule\`, which safely creates a git repository at project root and a submodule at \`--directory\` to automatically update the vendored libdragon files.
40 |
41 | With \`subtree\`, the cli will create a subtree at \`--directory\` instead. Keep in mind that git user name and email must be set up for this to work. Do not use if you are not using git yourself.
42 |
43 | To disable auto-vendoring, init with \`manual\`. With \`manual\`, libdragon files are expected at the location provided by \`--directory\` flag and the user is responsible for vendoring and updating them. This will allow using any other manual vendoring method.
44 |
45 | You can always switch strategy by re-running \`init\` with \`--strategy\`, though you will be responsible for handling the old vendored files as they will be kept as is.
46 |
47 | With the \`manual\` strategy, it is still recommended to have a git repository at project root such that container actions can execute faster by caching the container id inside the \`.git\` folder.\n`,
48 | alias: 's',
49 | typeLabel: '',
50 | group: 'vendoring',
51 | },
52 | ];
53 |
54 | const actionsToShow = /** @type {(keyof actions)[]} */ (
55 | info?.options.EXTRA_PARAMS?.filter(
56 | (action) =>
57 | Object.keys(actions).includes(action) && !['help'].includes(action)
58 | ) ?? (info ? [info.options.CURRENT_ACTION.name] : [])
59 | );
60 |
61 | const sections = [
62 | {
63 | header: chalk.green('Usage:'),
64 | content: `libdragon [flags]
65 |
66 | Joining flags not supported, provide them separately like \`-v -i\` `,
67 | },
68 | ...(actionsToShow?.length
69 | ? actionsToShow.flatMap((action) => [
70 | {
71 | header: chalk.green(`${action} action:`),
72 | content: actions[action].usage.description,
73 | },
74 | actions[action].usage.group || actions[action].usage.optionList
75 | ? {
76 | header: `accepted flags:`,
77 | optionList: [
78 | ...(actions[action].usage.group ? optionDefinitions : []),
79 | ...(actions[action].usage.optionList ?? []),
80 | ],
81 | group: actions[action].usage.group,
82 | }
83 | : {},
84 | ])
85 | : [
86 | {
87 | header: chalk.green('Available Commands:'),
88 | content: Object.values(actions).map(({ usage }) => ({
89 | name: usage.name,
90 | summary: usage.summary,
91 | })),
92 | },
93 | ]),
94 | {
95 | header: chalk.green('Global flags:'),
96 | optionList: globalOptionDefinitions,
97 | },
98 | ];
99 | const usage = commandLineUsage(sections);
100 | print(usage);
101 | };
102 |
103 | module.exports = /** @type {const} */ ({
104 | name: 'help',
105 | fn: printUsage,
106 | forwardsRestParams: true,
107 | usage: {
108 | name: 'help [action]',
109 | summary: 'Display this help information or details for the given action.',
110 | },
111 | });
112 |
--------------------------------------------------------------------------------
/modules/actions/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | init: require('./init'),
3 | make: require('./make'),
4 |
5 | update: require('./update'),
6 | exec: require('./exec'),
7 | disasm: require('./disasm'),
8 | install: require('./install'),
9 |
10 | start: require('./start'),
11 | stop: require('./stop'),
12 |
13 | help: require('./help'),
14 | version: require('./version'),
15 | destroy: require('./destroy'),
16 | };
17 |
--------------------------------------------------------------------------------
/modules/actions/init.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs/promises');
2 | const _fs = require('fs');
3 | const path = require('path');
4 | const sea = require('node:sea');
5 |
6 | const chalk = require('chalk').stderr;
7 |
8 | const { start } = require('./start');
9 | const {
10 | installDependencies,
11 | initGitAndCacheContainerId,
12 | updateImage,
13 | runGit,
14 | destroyContainer,
15 | ensureGit,
16 | } = require('../utils');
17 |
18 | const {
19 | LIBDRAGON_PROJECT_MANIFEST,
20 | LIBDRAGON_SUBMODULE,
21 | LIBDRAGON_GIT,
22 | } = require('../constants');
23 | const {
24 | log,
25 | CommandError,
26 | ParameterError,
27 | ValidationError,
28 | toPosixPath,
29 | toNativePath,
30 | getImageName,
31 | } = require('../helpers');
32 |
33 | // TODO: use https://nodejs.org/api/single-executable-applications.html#seagetassetkey-encoding &&
34 | // https://nodejs.org/api/single-executable-applications.html#assets instead
35 | const main_c = sea.isSea()
36 | ? // @ts-ignore-next-line
37 | /** @type {string} */ (require('../../skeleton/src/main.c'))
38 | : _fs.readFileSync(path.join(__dirname, '../../skeleton/src/main.c'), 'utf8');
39 |
40 | const makefile = sea.isSea()
41 | ? // @ts-ignore-next-line
42 | /** @type {string} */ require('../../skeleton/Makefile.mk')
43 | : _fs.readFileSync(
44 | path.join(__dirname, '../../skeleton/Makefile.mk'),
45 | 'utf8'
46 | );
47 |
48 | /**
49 | * @param {import('../project-info').LibdragonInfo} info
50 | * @returns {Promise<"submodule" | "subtree" | undefined>}
51 | */
52 | const autoDetect = async (info) => {
53 | const vendorTarget = path.relative(
54 | info.root,
55 | toNativePath(info.vendorDirectory)
56 | );
57 | const vendorTargetExists = await fs.stat(vendorTarget).catch((e) => {
58 | if (e.code !== 'ENOENT') throw e;
59 | return false;
60 | });
61 |
62 | if (
63 | vendorTargetExists &&
64 | (await runGit(info, ['submodule', 'status', info.vendorDirectory], {
65 | inheritStdin: false,
66 | inheritStdout: false,
67 | inheritStderr: false,
68 | }).catch((e) => {
69 | if (!(e instanceof CommandError)) {
70 | throw e;
71 | }
72 | }))
73 | ) {
74 | log(`${info.vendorDirectory} is a submodule.`);
75 | return 'submodule';
76 | }
77 |
78 | if (vendorTargetExists) {
79 | const gitLogs = await runGit(info, ['log'], {
80 | inheritStdin: false,
81 | inheritStdout: false,
82 | inheritStderr: false,
83 | }).catch((e) => {
84 | if (!(e instanceof CommandError)) {
85 | throw e;
86 | }
87 | });
88 |
89 | if (
90 | gitLogs &&
91 | gitLogs.includes(`git-subtree-dir: ${info.vendorDirectory}`)
92 | ) {
93 | log(`${info.vendorDirectory} is a subtree.`);
94 | return 'subtree';
95 | }
96 | }
97 | };
98 |
99 | /**
100 | * @param {import('../project-info').LibdragonInfo} info
101 | */
102 | const autoVendor = async (info) => {
103 | // Update the strategy information for the project if the flag is provided
104 | if (info.options.VENDOR_STRAT) {
105 | info.vendorStrategy = info.options.VENDOR_STRAT;
106 | }
107 |
108 | // Update the directory information for the project if the flag is provided
109 | if (info.options.VENDOR_DIR) {
110 | const relativeVendorDir = path.relative(info.root, info.options.VENDOR_DIR);
111 | // Validate vendoring path
112 | if (relativeVendorDir.startsWith('..')) {
113 | throw new ParameterError(
114 | `\`--directory=${info.options.VENDOR_DIR}\` is outside the project directory.`,
115 | info.options.CURRENT_ACTION.name
116 | );
117 | }
118 |
119 | // Immeditately convert it to a posix and relative path
120 | info.vendorDirectory = toPosixPath(relativeVendorDir);
121 | }
122 |
123 | // No need to do anything here
124 | if (info.vendorStrategy === 'manual') {
125 | return info;
126 | }
127 |
128 | // Container re-init breaks file modes assume there is git for this case.
129 | if (!process.env.DOCKER_CONTAINER) {
130 | await ensureGit(info);
131 | }
132 |
133 | // TODO: TS thinks this is already defined
134 | const detectedStrategy = await autoDetect(info);
135 |
136 | if (
137 | info.options.VENDOR_STRAT &&
138 | detectedStrategy &&
139 | detectedStrategy !== info.options.VENDOR_STRAT
140 | ) {
141 | throw new ValidationError(
142 | `${info.vendorDirectory} is a ${detectedStrategy} which is different from the provided strategy: ${info.options.VENDOR_STRAT}.`
143 | );
144 | }
145 |
146 | if (detectedStrategy) {
147 | log(
148 | `Using ${info.vendorDirectory} as a ${detectedStrategy} vendoring target.`
149 | );
150 | return {
151 | ...info,
152 | vendorStrategy: /** @type {import('../parameters').VendorStrategy} */ (
153 | detectedStrategy
154 | ),
155 | };
156 | }
157 |
158 | if (info.vendorStrategy === 'submodule') {
159 | try {
160 | await runGit(info, [
161 | 'submodule',
162 | 'add',
163 | '--force',
164 | '--name',
165 | LIBDRAGON_SUBMODULE,
166 | '--branch',
167 | info.activeBranchName,
168 | LIBDRAGON_GIT,
169 | info.vendorDirectory,
170 | ]);
171 | } catch (e) {
172 | if (!(e instanceof CommandError)) throw e;
173 | // We speculate this is caused by the user, so replace it with a more useful error message.
174 | e.message = `Unable to create submodule. If you have copied the executable in your project folder or you have a file named ${info.vendorDirectory}, removing it might solve this issue. Original error:\n${e.message}`;
175 | if (e.out) {
176 | e.message += `\n${e.out}`;
177 | }
178 | throw e;
179 | }
180 |
181 | return info;
182 | }
183 |
184 | if (info.vendorStrategy === 'subtree') {
185 | // Create a commit if it does not exist. This is required for subtree.
186 | try {
187 | await runGit(info, ['rev-parse', 'HEAD']);
188 | } catch (e) {
189 | if (!(e instanceof CommandError)) throw e;
190 |
191 | // This will throw if git user name/email is not set up. Let's not assume
192 | // anything for now. This means subtree is not supported for someone without
193 | // git on the host machine.
194 | // TODO: this is probably creating an unnecessary commit for someone who
195 | // just created a git repo and knows what they are doing.
196 | await runGit(info, [
197 | 'commit',
198 | '--allow-empty',
199 | '-n',
200 | '-m',
201 | '"Initial commit."',
202 | ]);
203 | }
204 |
205 | await runGit(info, [
206 | 'subtree',
207 | 'add',
208 | '--prefix',
209 | path.relative(info.root, info.vendorDirectory),
210 | LIBDRAGON_GIT,
211 | info.activeBranchName,
212 | '--squash',
213 | ]);
214 | return info;
215 | }
216 |
217 | return info;
218 | };
219 |
220 | /**
221 | * @param {import('../project-info').LibdragonInfo} info
222 | */
223 | async function copyFiles(info) {
224 | // TODO: make use of VFS, this is far from ideal
225 | const srcPath = path.join(info.root, 'src');
226 |
227 | const srcStat = await fs.stat(srcPath).catch((e) => {
228 | if (e.code !== 'ENOENT') throw e;
229 | return null;
230 | });
231 |
232 | if (srcStat && !srcStat.isDirectory()) {
233 | log(`${srcPath} is not a directory, skipping.`);
234 | return;
235 | }
236 |
237 | if (!srcStat) {
238 | log(`Creating a directory at ${srcPath}.`, true);
239 | await fs.mkdir(srcPath);
240 | }
241 |
242 | const makefilePath = path.join(info.root, 'Makefile');
243 | await fs
244 | .writeFile(makefilePath, makefile, {
245 | flag: 'wx',
246 | })
247 | .catch(() => {
248 | log(`${makefilePath} already exists, skipping.`);
249 | });
250 |
251 | const mainPath = path.join(info.root, 'src', 'main.c');
252 | await fs
253 | .writeFile(mainPath, main_c, {
254 | flag: 'wx',
255 | })
256 | .catch(() => {
257 | log(`${mainPath} already exists, skipping.`);
258 | });
259 | }
260 |
261 | /**
262 | * Initialize a new libdragon project in current working directory
263 | * Also downloads the image
264 | * @param {import('../project-info').LibdragonInfo} info
265 | */
266 | async function init(info) {
267 | log(`Initializing a libdragon project at ${info.root}`);
268 |
269 | // Validate manifest
270 | const manifestPath = path.join(info.root, LIBDRAGON_PROJECT_MANIFEST);
271 | const manifestStats = await fs.stat(manifestPath).catch((e) => {
272 | if (e.code !== 'ENOENT') throw e;
273 | return /** @type {const} */ (false);
274 | });
275 |
276 | if (manifestStats && !manifestStats.isDirectory()) {
277 | throw new ValidationError(
278 | 'There is already a `.libdragon` file and it is not a directory.'
279 | );
280 | }
281 |
282 | if (info.haveProjectConfig) {
283 | log(
284 | `${path.join(
285 | info.root,
286 | LIBDRAGON_PROJECT_MANIFEST
287 | )} exists. This is already a libdragon project, starting it...`
288 | );
289 | if (!process.env.DOCKER_CONTAINER) {
290 | if (info.options.DOCKER_IMAGE) {
291 | // The logic here resembles syncImageAndStart but it aims at being idempotent
292 | // w.r.t the container when called without any additional flag.
293 | // Download the new image and if it is different, re-create the container
294 | if (await updateImage(info, info.options.DOCKER_IMAGE)) {
295 | await destroyContainer(info);
296 | }
297 |
298 | info = {
299 | ...info,
300 | imageName: info.options.DOCKER_IMAGE,
301 | };
302 | }
303 | info = {
304 | ...info,
305 | containerId: await start(info),
306 | };
307 | }
308 |
309 | info = await autoVendor(info);
310 | await installDependencies(info);
311 | return info;
312 | }
313 |
314 | if (!process.env.DOCKER_CONTAINER) {
315 | let activeBranchName = info.options.BRANCH ?? info.activeBranchName;
316 | let shouldOverrideBranch = !!info.options.BRANCH;
317 |
318 | if ((await autoDetect(info)) === 'submodule') {
319 | try {
320 | const existingBranchName = await runGit(
321 | info,
322 | [
323 | '-C',
324 | path.relative(info.root, info.vendorDirectory),
325 | 'rev-parse',
326 | '--abbrev-ref',
327 | 'HEAD',
328 | ],
329 | {
330 | inheritStdin: false,
331 | inheritStdout: false,
332 | inheritStderr: false,
333 | }
334 | );
335 | activeBranchName = existingBranchName.trim();
336 | shouldOverrideBranch = !!activeBranchName;
337 | } catch {
338 | // If we can't get the branch name, we will use the default
339 | }
340 | }
341 |
342 | info = {
343 | ...info,
344 | activeBranchName,
345 | // We don't have a config yet, this is a fresh project, override with the
346 | // image flag if provided. Also see https://github.com/anacierdem/libdragon-docker/issues/66
347 | // Next, if `--branch` flag is provided use that or the one recovered from
348 | // the submodule.
349 | imageName:
350 | (info.options.DOCKER_IMAGE ??
351 | (shouldOverrideBranch && getImageName(activeBranchName))) ||
352 | info.imageName,
353 | };
354 | // Download image and start it
355 | await updateImage(info, info.imageName);
356 | info.containerId = await start(info);
357 | // We have created a new container, save the new info ASAP
358 | // When in a container, we should already have git
359 | // Re-initing breaks file modes anyways
360 | // TODO: if this fails, we leave the project in a bad state
361 | await initGitAndCacheContainerId(
362 | /** @type Parameters[0] */ (info)
363 | );
364 | }
365 |
366 | info = await autoVendor(info);
367 |
368 | log(`Preparing project files...`);
369 |
370 | await Promise.all([installDependencies(info), copyFiles(info)]);
371 |
372 | log(chalk.green(`libdragon ready at \`${info.root}\`.`));
373 | return info;
374 | }
375 |
376 | module.exports = /** @type {const} */ ({
377 | name: 'init',
378 | fn: init,
379 | forwardsRestParams: false,
380 | usage: {
381 | name: 'init',
382 | summary: 'Create a libdragon project in the current directory.',
383 | description: `Creates a libdragon project in the current directory. Every libdragon project will have its own docker container instance. If you are in a git repository or an NPM project, libdragon will be initialized at their root also marking there with a \`.libdragon\` folder. When running in a submodule, initializion will take place at that level rather than the superproject. Do not remove the \`.libdragon\` folder and commit its contents if you are using source control, as it keeps persistent libdragon project information.
384 |
385 | By default, a git repository and a submodule at \`./libdragon\` will be created to automatically update the vendored libdragon files on subsequent \`update\`s. If you intend to opt-out from this feature, see the \`--strategy manual\` flag to provide your self-managed libdragon copy. The default behaviour is intended for users who primarily want to consume libdragon as is.
386 |
387 | If this is the first time you are creating a libdragon project at that location, this action will also create skeleton project files to kickstart things with the given image, if provided. For subsequent runs without any parameter, it will act like \`start\` thus can be used to revive an existing project without modifying it.
388 |
389 | If you have an existing project with an already vendored submodule or subtree libdragon copy, \`init\` will automatically detect it at the provided \`--directory\`.`,
390 | group: ['docker', 'vendoring', '_none'],
391 |
392 | optionList: [
393 | {
394 | name: 'branch',
395 | description:
396 | 'Provide a different branch to initialize libdragon. It will also change the toolchain docker image to be based on the given branch but `--image` will have precedence. Defaults to `trunk` & `latest` image.',
397 | alias: 'b',
398 | typeLabel: '',
399 | },
400 | ],
401 | },
402 | });
403 |
--------------------------------------------------------------------------------
/modules/actions/install.js:
--------------------------------------------------------------------------------
1 | const { installDependencies } = require('../utils');
2 | const { start } = require('./start');
3 |
4 | /**
5 | * Updates the image if flag is provided and install vendors onto the container.
6 | * We should probably remove the image installation responsibility from this
7 | * action but it might be a breaking change.
8 | * @param {import('../project-info').LibdragonInfo} libdragonInfo
9 | */
10 | const install = async (libdragonInfo) => {
11 | let updatedInfo = libdragonInfo;
12 |
13 | if (!process.env.DOCKER_CONTAINER) {
14 | // Make sure existing one is running
15 | updatedInfo = {
16 | ...updatedInfo,
17 | containerId: await start(libdragonInfo),
18 | };
19 | }
20 |
21 | // Re-install vendors on new image
22 | // TODO: skip this if unnecessary
23 | await installDependencies(updatedInfo);
24 | return updatedInfo;
25 | };
26 |
27 | module.exports = /** @type {const} */ ({
28 | name: 'install',
29 | fn: install,
30 | forwardsRestParams: false,
31 | usage: {
32 | name: 'install',
33 | summary: 'Vendor libdragon as is.',
34 | description: `Attempts to build and install everything libdragon related into the container. This includes all the tools and third parties used by libdragon except for the toolchain. If you have made changes to libdragon, you can execute this action to build everything based on your changes. Requires you to have an intact vendoring target (also see the \`--directory\` flag). If you are not working on libdragon itself, you can just use the \`update\` action instead.
35 |
36 | Must be run in an initialized libdragon project. This can be useful to recover from a half-baked container.`,
37 | },
38 | });
39 |
--------------------------------------------------------------------------------
/modules/actions/make.js:
--------------------------------------------------------------------------------
1 | const { fn: exec } = require('./exec');
2 |
3 | /**
4 | * @param {import('../project-info').LibdragonInfo} info
5 | */
6 | const make = async (info) => {
7 | return await exec({
8 | ...info,
9 | options: {
10 | ...info.options,
11 | EXTRA_PARAMS: ['make', ...info.options.EXTRA_PARAMS],
12 | },
13 | });
14 | };
15 |
16 | module.exports = /** @type {const} */ ({
17 | name: 'make',
18 | fn: make,
19 | forwardsRestParams: true,
20 | usage: {
21 | name: 'make [params]',
22 | summary: 'Run the libdragon build system in the current directory.',
23 | description: `Runs the libdragon build system in the current directory. It will mirror your current working directory to the container.
24 |
25 | Must be run in an initialized libdragon project. This action is a shortcut to the \`exec\` action under the hood.`,
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/modules/actions/start.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk').stderr;
2 |
3 | const { CONTAINER_TARGET_PATH } = require('../constants');
4 | const {
5 | spawnProcess,
6 | log,
7 | print,
8 | dockerExec,
9 | assert,
10 | ValidationError,
11 | } = require('../helpers');
12 |
13 | const {
14 | checkContainerAndClean,
15 | checkContainerRunning,
16 | destroyContainer,
17 | } = require('../utils');
18 |
19 | /**
20 | * Create a new container
21 | * @param {import('../project-info').LibdragonInfo} libdragonInfo
22 | */
23 | const initContainer = async (libdragonInfo) => {
24 | assert(
25 | !process.env.DOCKER_CONTAINER,
26 | new Error('initContainer does not make sense in a container')
27 | );
28 |
29 | let newId;
30 | try {
31 | log('Creating new container...');
32 | newId = (
33 | await spawnProcess('docker', [
34 | 'run',
35 | '-d', // Detached
36 | '--mount',
37 | 'type=bind,source=' +
38 | libdragonInfo.root +
39 | ',target=' +
40 | CONTAINER_TARGET_PATH, // Mount files
41 | '-w=' + CONTAINER_TARGET_PATH, // Set working directory
42 | libdragonInfo.imageName,
43 | 'tail',
44 | '-f',
45 | '/dev/null',
46 | ])
47 | ).trim();
48 |
49 | // chown the installation folder once on init
50 | const { uid, gid } = libdragonInfo.userInfo;
51 | await dockerExec(
52 | {
53 | ...libdragonInfo,
54 | containerId: newId,
55 | },
56 | [
57 | 'chown',
58 | '-R',
59 | `${uid >= 0 ? uid : ''}:${gid >= 0 ? gid : ''}`,
60 | '/n64_toolchain',
61 | ]
62 | );
63 | } catch (e) {
64 | // Dispose the invalid container, clean and exit
65 | await destroyContainer({
66 | ...libdragonInfo,
67 | containerId: newId,
68 | });
69 | log(
70 | chalk.red(
71 | 'We were unable to initialize libdragon. Done cleanup. Check following logs for the actual error.'
72 | )
73 | );
74 | throw e;
75 | }
76 |
77 | const name = await spawnProcess('docker', [
78 | 'container',
79 | 'inspect',
80 | newId,
81 | '--format',
82 | '{{.Name}}',
83 | ]);
84 |
85 | log(chalk.green(`Successfully initialized docker container: ${name.trim()}`));
86 |
87 | return newId;
88 | };
89 |
90 | /**
91 | * @param {import('../project-info').LibdragonInfo} libdragonInfo
92 | */
93 | const start = async (libdragonInfo) => {
94 | assert(
95 | !process.env.DOCKER_CONTAINER,
96 | new Error('Cannot start a container when we are already in a container.')
97 | );
98 |
99 | const running =
100 | libdragonInfo.containerId &&
101 | (await checkContainerRunning(libdragonInfo.containerId));
102 |
103 | if (running) {
104 | log(`Container ${running} already running.`, true);
105 | return running;
106 | }
107 |
108 | let id = await checkContainerAndClean(libdragonInfo);
109 |
110 | if (!id) {
111 | log(`Container does not exist, re-initializing...`, true);
112 | id = await initContainer(libdragonInfo);
113 | return id;
114 | }
115 |
116 | log(`Starting container: ${id}`, true);
117 | await spawnProcess('docker', ['container', 'start', id]);
118 |
119 | return id;
120 | };
121 |
122 | module.exports = /** @type {const} */ ({
123 | name: 'start',
124 | /**
125 | * @param {import('../project-info').LibdragonInfo} libdragonInfo
126 | */
127 | fn: async (libdragonInfo) => {
128 | if (process.env.DOCKER_CONTAINER) {
129 | throw new ValidationError(`We are already in a container.`);
130 | }
131 |
132 | const containerId = await start(libdragonInfo);
133 | print(containerId);
134 | return { ...libdragonInfo, containerId };
135 | },
136 | start,
137 | forwardsRestParams: false,
138 | usage: {
139 | name: 'start',
140 | summary: 'Start the container for current project.',
141 | description: `Start the container assigned to the current libdragon project. Will first attempt to start an existing container if found, followed by a new container run and installation similar to the \`install\` action. Will always print out the container id to stdout on success except when verbose is set.
142 |
143 | Must be run in an initialized libdragon project.`,
144 | },
145 | });
146 |
--------------------------------------------------------------------------------
/modules/actions/stop.js:
--------------------------------------------------------------------------------
1 | const { spawnProcess, ValidationError } = require('../helpers');
2 |
3 | const { checkContainerRunning } = require('../utils');
4 |
5 | /**
6 | * @param {import('../project-info').LibdragonInfo} libdragonInfo
7 | * @returns {Promise}
8 | */
9 | const stop = async (libdragonInfo) => {
10 | if (process.env.DOCKER_CONTAINER) {
11 | throw new ValidationError(
12 | `Not possible to stop the container from inside.`
13 | );
14 | }
15 |
16 | const running =
17 | libdragonInfo.containerId &&
18 | (await checkContainerRunning(libdragonInfo.containerId));
19 | if (!running) {
20 | return;
21 | }
22 |
23 | await spawnProcess('docker', ['container', 'stop', running]);
24 | return libdragonInfo;
25 | };
26 |
27 | module.exports = /** @type {const} */ ({
28 | name: 'stop',
29 | fn: stop,
30 | forwardsRestParams: false,
31 | usage: {
32 | name: 'stop',
33 | summary: 'Stop the container for current project.',
34 | description: `Stop the container assigned to the current libdragon project.
35 |
36 | Must be run in an initialized libdragon project.`,
37 | },
38 | });
39 |
--------------------------------------------------------------------------------
/modules/actions/update.js:
--------------------------------------------------------------------------------
1 | const { log, assert } = require('../helpers');
2 | const { LIBDRAGON_GIT } = require('../constants');
3 | const {
4 | runGit,
5 | installDependencies,
6 | updateImage,
7 | destroyContainer,
8 | } = require('../utils');
9 | const { start } = require('./start');
10 |
11 | /**
12 | * @param {import('../project-info').LibdragonInfo} libdragonInfo
13 | */
14 | async function syncImageAndStart(libdragonInfo) {
15 | assert(
16 | !process.env.DOCKER_CONTAINER,
17 | new Error(
18 | '[syncImageAndStart] We should already know we are in a container.'
19 | )
20 | );
21 |
22 | const oldImageName = libdragonInfo.imageName;
23 | const imageName = libdragonInfo.options.DOCKER_IMAGE ?? oldImageName;
24 | // If an image is provided, always attempt to install it
25 | // See https://github.com/anacierdem/libdragon-docker/issues/47
26 | if (oldImageName !== imageName) {
27 | log(`Updating image from \`${oldImageName}\` to \`${imageName}\``);
28 | } else {
29 | log(`Updating image \`${oldImageName}\``);
30 | }
31 |
32 | // Download the new image and if it is different, re-create the container
33 | if (await updateImage(libdragonInfo, imageName)) {
34 | await destroyContainer(libdragonInfo);
35 | }
36 |
37 | return {
38 | ...libdragonInfo,
39 | imageName,
40 | containerId: await start({
41 | ...libdragonInfo,
42 | imageName,
43 | }),
44 | };
45 | }
46 |
47 | /**
48 | * @param {import('../project-info').LibdragonInfo} info
49 | */
50 | const update = async (info) => {
51 | info = await syncImageAndStart(info);
52 |
53 | if (info.vendorStrategy !== 'manual') {
54 | log(`Updating ${info.vendorStrategy}...`);
55 | }
56 |
57 | if (info.vendorStrategy === 'submodule') {
58 | await runGit(info, [
59 | 'submodule',
60 | 'update',
61 | '--remote',
62 | '--merge',
63 | info.vendorDirectory,
64 | ]);
65 | } else if (info.vendorStrategy === 'subtree') {
66 | await runGit(info, [
67 | 'subtree',
68 | 'pull',
69 | '--prefix',
70 | info.vendorDirectory,
71 | LIBDRAGON_GIT,
72 | info.activeBranchName,
73 | '--squash',
74 | ]);
75 | }
76 |
77 | await installDependencies(info);
78 | };
79 |
80 | module.exports = /** @type {const} */ ({
81 | name: 'update',
82 | fn: update,
83 | forwardsRestParams: false,
84 | usage: {
85 | name: 'update',
86 | summary: 'Update libdragon and do an install.',
87 | description: `Will update the docker image and if you are using auto-vendoring (see \`--strategy\`), will also update the submodule/subtree from the remote branch (\`trunk\`) with a merge/squash strategy and then perform a \`libdragon install\`. You can use the \`install\` action to only update all libdragon related artifacts in the container.
88 |
89 | Must be run in an initialized libdragon project.`,
90 | group: ['docker'],
91 | },
92 | });
93 |
--------------------------------------------------------------------------------
/modules/actions/version.js:
--------------------------------------------------------------------------------
1 | const sea = require('node:sea');
2 |
3 | const { print } = require('../helpers');
4 |
5 | const printVersion = async () => {
6 | // Version is set during build time. If it is not set, it is set to the
7 | // package.json version. This is done to avoid bundling the package.json
8 | // (which would be out of date) with the built one.
9 | if (!globalThis.VERSION) {
10 | const { version } = require('../../package.json');
11 | globalThis.VERSION = version;
12 | }
13 | print(`libdragon-cli v${globalThis.VERSION} ${sea.isSea() ? '(sea)' : ''}`);
14 | };
15 |
16 | module.exports = /** @type {const} */ ({
17 | name: 'version',
18 | fn: printVersion,
19 | forwardsRestParams: false,
20 | usage: {
21 | name: 'version',
22 | summary: 'Display cli version.',
23 | description: `Displays currently running cli version.`,
24 | },
25 | });
26 |
--------------------------------------------------------------------------------
/modules/constants.js:
--------------------------------------------------------------------------------
1 | module.exports = /** @type {const} */ ({
2 | DOCKER_HUB_IMAGE: 'ghcr.io/dragonminded/libdragon',
3 | LIBDRAGON_GIT: 'https://github.com/DragonMinded/libdragon',
4 | LIBDRAGON_BRANCH: 'trunk',
5 | LIBDRAGON_SUBMODULE: 'libdragon',
6 | CONTAINER_TARGET_PATH: '/libdragon',
7 | LIBDRAGON_PROJECT_MANIFEST: '.libdragon',
8 | CACHED_CONTAINER_FILE: 'libdragon-docker-container',
9 | CONFIG_FILE: 'config.json',
10 | DEFAULT_STRATEGY: 'submodule',
11 |
12 | ACCEPTED_STRATEGIES: ['submodule', 'subtree', 'manual'],
13 |
14 | // These do not need a project to exist and their actions do not need the whole
15 | // structure. Actions that need the full project information should not be
16 | // listed here.
17 | NO_PROJECT_ACTIONS: ['help', 'version'],
18 |
19 | // cli exit codes
20 | STATUS_OK: 0,
21 | STATUS_ERROR: 1,
22 | STATUS_BAD_PARAM: 2,
23 | STATUS_VALIDATION_ERROR: 4,
24 | });
25 |
--------------------------------------------------------------------------------
/modules/docker-utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | *
3 | * @param {import('./project-info').LibdragonInfo} libdragonInfo
4 | */
5 | function dockerHostUserParams(libdragonInfo) {
6 | const { uid, gid } = libdragonInfo.userInfo;
7 | return ['-u', `${uid >= 0 ? uid : ''}:${gid >= 0 ? gid : ''}`];
8 | }
9 |
10 | module.exports = {
11 | dockerHostUserParams,
12 | };
13 |
--------------------------------------------------------------------------------
/modules/globals.js:
--------------------------------------------------------------------------------
1 | const globals = {
2 | verbose: false,
3 | };
4 |
5 | module.exports = { globals };
6 |
--------------------------------------------------------------------------------
/modules/helpers.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs/promises');
3 | const chalk = require('chalk').stderr;
4 | const { spawn } = require('child_process');
5 |
6 | const { globals } = require('./globals');
7 | const {
8 | NO_PROJECT_ACTIONS,
9 | LIBDRAGON_BRANCH,
10 | DOCKER_HUB_IMAGE,
11 | } = require('./constants');
12 |
13 | /**
14 | * A structure to keep additional error information
15 | * @typedef {Object} ErrorState
16 | * @property {number} code
17 | * @property {string} out
18 | * @property {boolean} userCommand
19 | */
20 |
21 | /**
22 | * An error caused by a command exiting with a non-zero exit code
23 | * @class
24 | */
25 | class CommandError extends Error {
26 | /**
27 | * @param {string} message Error message to report
28 | * @param {ErrorState} errorState
29 | */
30 | constructor(message, { code, out, userCommand }) {
31 | super(message);
32 |
33 | /**
34 | * Exit code
35 | * @type {number}
36 | * @public
37 | */
38 | this.code = code;
39 |
40 | /**
41 | * Error output as a single concatanated string
42 | * @type {string}
43 | * @public
44 | */
45 | this.out = out;
46 |
47 | /**
48 | * true when the error caused by a command explicitly run by the end user
49 | * @type {boolean}
50 | * @public
51 | */
52 | this.userCommand = userCommand;
53 | }
54 | }
55 |
56 | /**
57 | * An error caused by the user providing an unexpected input
58 | * @class
59 | */
60 | class ParameterError extends Error {
61 | /**
62 | * @param {string} message Error message to report
63 | * @param {string} actionName
64 | */
65 | constructor(message, actionName) {
66 | super(message);
67 |
68 | /**
69 | * true when the error caused by a command explicitly run by the end user
70 | * @type {string}
71 | * @public
72 | */
73 | this.actionName = actionName;
74 | }
75 | }
76 |
77 | /**
78 | * Something was not as expected to continue the operation
79 | * @class
80 | */
81 | class ValidationError extends Error {
82 | /**
83 | * @param {string} message
84 | */
85 | constructor(message) {
86 | super(message);
87 | }
88 | }
89 |
90 | /**
91 | * @param {string} path
92 | */
93 | async function fileExists(path) {
94 | return fs
95 | .stat(path)
96 | .then((s) => s.isFile())
97 | .catch((e) => {
98 | if (e.code !== 'ENOENT') throw e;
99 | return false;
100 | });
101 | }
102 |
103 | /**
104 | * @param {string} path
105 | */
106 | async function dirExists(path) {
107 | return fs
108 | .stat(path)
109 | .then((s) => s.isDirectory())
110 | .catch((e) => {
111 | if (e.code !== 'ENOENT') throw e;
112 | return false;
113 | });
114 | }
115 |
116 | /**
117 | * @typedef {{
118 | * userCommand?: boolean,
119 | * inheritStdin?: boolean,
120 | * inheritStdout?: boolean,
121 | * inheritStderr?: boolean,
122 | * spawnOptions?: import('child_process').SpawnOptions,
123 | * stdin?: import('fs').ReadStream,
124 | * }} SpawnOptions
125 | */
126 |
127 | // A simple Promise wrapper for child_process.spawn. Return the err/out streams
128 | // from the process by default. Specify inheritStdout / inheritStderr to disable
129 | // this and inherit the parent process's stream, passing through the TTY if any.
130 | /**
131 | *
132 | * @param {string} cmd
133 | * @param {string[]} params
134 | * @param {SpawnOptions} options
135 | * @returns {Promise}
136 | */
137 | function spawnProcess(
138 | cmd,
139 | params = [],
140 | {
141 | // Used to decorate the potential CommandError with a prop such that we can
142 | // report this error back to the user.
143 | userCommand = false,
144 | // This is the stream where the process will receive its input.
145 | stdin,
146 | // If this is true, the related stream is inherited from the parent process
147 | // and we cannot read them anymore. So if you need to read a stream, you
148 | // should disable it. When disabled, the relevant stream cannot be a tty
149 | // anymore. By default, we expect the caller to read err/out.
150 | inheritStdin = true,
151 | inheritStdout = false,
152 | inheritStderr = false,
153 | // Passthrough to spawn
154 | spawnOptions = {},
155 | } = {
156 | userCommand: false,
157 | inheritStdin: true,
158 | inheritStdout: false,
159 | inheritStderr: false,
160 | spawnOptions: {},
161 | }
162 | ) {
163 | assert(
164 | cmd !== 'docker' || !process.env.DOCKER_CONTAINER,
165 | new Error('Trying to invoke docker inside a container.')
166 | );
167 | return new Promise((resolve, reject) => {
168 | /** @type {Buffer[]} */
169 | const stdout = [];
170 |
171 | /** @type {Buffer[]} */
172 | const stderr = [];
173 |
174 | log(chalk.grey(`Spawning: ${cmd} ${params.join(' ')}`), true);
175 |
176 | const enableInTTY = Boolean(process.stdin.isTTY) && inheritStdin;
177 | const enableOutTTY = Boolean(process.stdout.isTTY) && inheritStdout;
178 | const enableErrorTTY = Boolean(process.stderr.isTTY) && inheritStderr;
179 |
180 | const command = spawn(cmd, params, {
181 | stdio: [
182 | enableInTTY ? 'inherit' : 'pipe',
183 | enableOutTTY ? 'inherit' : 'pipe',
184 | enableErrorTTY ? 'inherit' : 'pipe',
185 | ],
186 | env: {
187 | ...process.env,
188 | // Prevent the annoying docker "What's next?" message. It messes up everything.
189 | DOCKER_CLI_HINTS: 'false',
190 | },
191 | ...spawnOptions,
192 | });
193 |
194 | // See a very old related issue: https://github.com/nodejs/node/issues/947
195 | // When the stream is not fully consumed by the pipe target and it exits,
196 | // an EPIPE or EOF is thrown. We don't care about those.
197 | /**
198 | * @param {Error & {code: string}} err
199 | */
200 | const eatEpipe = (err) => {
201 | if (err.code !== 'EPIPE' && err.code !== 'EOF') {
202 | throw err;
203 | }
204 | // No need to listen for close anymore
205 | command.off('close', closeHandler);
206 |
207 | // It was not fully consumed, just resolve into an empty string
208 | // No one should be using this anyways. Ideally we could clean a few
209 | // last bytes from the buffers to create a correct utf-8 string.
210 | resolve('');
211 | };
212 |
213 | if (!enableInTTY && stdin && command.stdin) {
214 | stdin.pipe(command.stdin);
215 | }
216 |
217 | if (!enableOutTTY && (globals.verbose || userCommand) && command.stdout) {
218 | command.stdout.pipe(process.stdout);
219 | process.stdout.once('error', eatEpipe);
220 | }
221 |
222 | if (!inheritStdout && command.stdout) {
223 | command.stdout.on('data', function (data) {
224 | stdout.push(Buffer.from(data));
225 | });
226 | }
227 |
228 | if (!enableErrorTTY && (globals.verbose || userCommand) && command.stderr) {
229 | command.stderr.pipe(process.stderr);
230 | }
231 |
232 | if (!inheritStderr && command.stderr) {
233 | command.stderr.on('data', function (data) {
234 | stderr.push(Buffer.from(data));
235 | });
236 | }
237 |
238 | /**
239 | * @param {Error} err
240 | */
241 | const errorHandler = (err) => {
242 | command.off('close', closeHandler);
243 | reject(err);
244 | };
245 |
246 | /**
247 | * @param {number} code
248 | */
249 | const closeHandler = function (code) {
250 | // The stream was fully consumed, if there is this an additional error on
251 | // stdout, it must be a legitimate error
252 | process.stdout.off('error', eatEpipe);
253 | command.off('error', errorHandler);
254 | if (code === 0) {
255 | resolve(Buffer.concat(stdout).toString());
256 | } else {
257 | const err = new CommandError(
258 | `Command ${cmd} ${params.join(' ')} exited with code ${code}.`,
259 | {
260 | code,
261 | out: Buffer.concat(stderr).toString(),
262 | userCommand,
263 | }
264 | );
265 | reject(err);
266 | }
267 | };
268 |
269 | command.once('error', errorHandler);
270 | command.once('close', closeHandler);
271 | });
272 | }
273 |
274 | /**
275 | * @typedef {{
276 | * (libdragonInfo: import('./project-info').LibdragonInfo, dockerParams: string[], cmdWithParams: string[], options?: SpawnOptions): Promise;
277 | * (libdragonInfo: import('./project-info').LibdragonInfo, cmdWithParams: string[], options?: SpawnOptions, unused?: unknown): Promise;
278 | * }} DockerExec
279 | */
280 | const dockerExec = /** @type {DockerExec} */ (
281 | function (
282 | libdragonInfo,
283 | dockerParams,
284 | cmdWithParams,
285 | /** @type {SpawnOptions | undefined} */
286 | options
287 | ) {
288 | // TODO: assert for invalid args
289 | const haveDockerParams =
290 | Array.isArray(dockerParams) && Array.isArray(cmdWithParams);
291 |
292 | if (!haveDockerParams) {
293 | options = /** @type {SpawnOptions} */ (cmdWithParams);
294 | }
295 |
296 | const finalCmdWithParams = haveDockerParams ? cmdWithParams : dockerParams;
297 | const finalDockerParams = haveDockerParams ? dockerParams : [];
298 |
299 | assert(
300 | finalDockerParams.findIndex(
301 | (val) => val === '--workdir=' || val === '-w='
302 | ) === -1,
303 | new Error('Do not use `=` syntax when setting working dir')
304 | );
305 |
306 | // Convert docker execs into regular commands in the correct cwd
307 | if (process.env.DOCKER_CONTAINER) {
308 | const workDirIndex = finalDockerParams.findIndex(
309 | (val) => val === '--workdir' || val === '-w'
310 | );
311 | const workDir =
312 | workDirIndex >= 0 ? finalDockerParams[workDirIndex + 1] : undefined;
313 | return spawnProcess(finalCmdWithParams[0], finalCmdWithParams.slice(1), {
314 | ...options,
315 | spawnOptions: {
316 | cwd: workDir,
317 | ...options?.spawnOptions,
318 | },
319 | });
320 | }
321 |
322 | assert(
323 | !!libdragonInfo.containerId,
324 | new Error('Trying to invoke dockerExec without a containerId.')
325 | );
326 |
327 | /** @type string[] */
328 | const additionalParams = [];
329 |
330 | // Docker TTY wants in & out streams both to be a TTY
331 | // If no options are provided, disable TTY as spawnProcess defaults to no
332 | // inherit as well.
333 | const enableTTY = options
334 | ? options.inheritStdout && options.inheritStdin
335 | : false;
336 | const ttyEnabled = enableTTY && process.stdout.isTTY && process.stdin.isTTY;
337 |
338 | if (ttyEnabled) {
339 | additionalParams.push('-t');
340 | }
341 |
342 | // Always enable stdin, also see; https://github.com/anacierdem/libdragon-docker/issues/45
343 | // Currently we run all exec commands in stdin mode even if the actual process
344 | // does not need any input. This will eat any user input by default.
345 | additionalParams.push('-i');
346 |
347 | return spawnProcess(
348 | 'docker',
349 | [
350 | 'exec',
351 | ...finalDockerParams,
352 | ...additionalParams,
353 | libdragonInfo.containerId,
354 | ...finalCmdWithParams,
355 | ],
356 | options
357 | );
358 | }
359 | );
360 |
361 | /**
362 | * @param {string} p
363 | */
364 | function toPosixPath(p) {
365 | return p.replace(new RegExp('\\' + path.sep, 'g'), path.posix.sep);
366 | }
367 |
368 | /**
369 | * @param {string} p
370 | */
371 | function toNativePath(p) {
372 | return p.replace(new RegExp('\\' + path.posix.sep, 'g'), path.sep);
373 | }
374 |
375 | /**
376 | * @param {any} condition
377 | * @param {Error} error
378 | * @returns {asserts condition}
379 | */
380 | function assert(condition, error) {
381 | if (!condition) {
382 | error.message = `[ASSERTION FAILED] ${error.message}`;
383 | throw error;
384 | }
385 | }
386 |
387 | /**
388 | * @param {string} text
389 | */
390 | function print(text) {
391 | // eslint-disable-next-line no-console
392 | console.log(text);
393 | return;
394 | }
395 |
396 | /**
397 | * @param {string} text
398 | * @param {boolean} verboseOnly
399 | */
400 | function log(text, verboseOnly = false) {
401 | if (!verboseOnly) {
402 | // New default color for console.err is red
403 | // Also see https://github.com/nodejs/node/issues/53661
404 | // eslint-disable-next-line no-console
405 | console.error(chalk.white(text));
406 | return;
407 | }
408 | if (globals.verbose) {
409 | // eslint-disable-next-line no-console
410 | console.error(chalk.gray(text));
411 | return;
412 | }
413 | }
414 |
415 | /**
416 | * @param {import('./project-info').CLIInfo} info
417 | * @returns {info is import('./project-info').ProjectInfo}
418 | */
419 | function isProjectAction(info) {
420 | return !NO_PROJECT_ACTIONS.includes(
421 | /** @type {import('./project-info').ActionsNoProject} */ (
422 | info.options.CURRENT_ACTION.name
423 | )
424 | );
425 | }
426 |
427 | /**
428 | * @param {string} branchName
429 | * @returns {string}
430 | */
431 | function getImageName(branchName) {
432 | return (
433 | DOCKER_HUB_IMAGE +
434 | ':' +
435 | (branchName === LIBDRAGON_BRANCH ? 'latest' : branchName)
436 | );
437 | }
438 |
439 | module.exports = {
440 | spawnProcess,
441 | toPosixPath,
442 | toNativePath,
443 | print,
444 | log,
445 | dockerExec,
446 | assert,
447 | fileExists,
448 | dirExists,
449 | CommandError,
450 | ParameterError,
451 | ValidationError,
452 | isProjectAction,
453 | getImageName,
454 | };
455 |
--------------------------------------------------------------------------------
/modules/npm-utils.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | const { fileExists, spawnProcess } = require('./helpers');
4 |
5 | async function findNPMRoot() {
6 | try {
7 | const root = path.resolve((await runNPM(['root'])).trim(), '..');
8 | // Only report if package.json really exists. npm fallbacks to cwd
9 | if (await fileExists(path.join(root, 'package.json'))) {
10 | return root;
11 | }
12 | } catch {
13 | // User does not have and does not care about NPM if it didn't work
14 | return undefined;
15 | }
16 | }
17 |
18 | /**
19 | * @param {string[]} params
20 | */
21 | function runNPM(params) {
22 | return spawnProcess(
23 | /^win/.test(process.platform) ? 'npm.cmd' : 'npm',
24 | params,
25 | {
26 | userCommand: false,
27 | inheritStdin: true,
28 | inheritStdout: false,
29 | inheritStderr: false,
30 | spawnOptions: {
31 | shell: true,
32 | },
33 | }
34 | );
35 | }
36 | module.exports = {
37 | findNPMRoot,
38 | };
39 |
--------------------------------------------------------------------------------
/modules/parameters.js:
--------------------------------------------------------------------------------
1 | const chalk = require('chalk').stderr;
2 |
3 | const { log } = require('./helpers');
4 | const actions = require('./actions');
5 | const { STATUS_BAD_PARAM, ACCEPTED_STRATEGIES } = require('./constants');
6 | const { globals } = require('./globals');
7 |
8 | /** @typedef { import('./actions') } Actions */
9 |
10 | /** @typedef { typeof import('./constants').ACCEPTED_STRATEGIES[number] } VendorStrategy */
11 |
12 | /**
13 | * @template {keyof Actions} [T=keyof Actions]
14 | * @typedef {{
15 | * EXTRA_PARAMS: string[];
16 | * CURRENT_ACTION: Actions[T];
17 | * VERBOSE?: boolean;
18 | * DOCKER_IMAGE?: string;
19 | * VENDOR_DIR?: string;
20 | * VENDOR_STRAT?: VendorStrategy;
21 | * FILE?: string;
22 | * BRANCH?: string;
23 | * }} CommandlineOptions
24 | */
25 |
26 | /**
27 | * @param {string[]} argv
28 | */
29 | const parseParameters = async (argv) => {
30 | /** @type Partial & {EXTRA_PARAMS: string[] } */
31 | const options = {
32 | EXTRA_PARAMS: [],
33 | CURRENT_ACTION: undefined,
34 | };
35 |
36 | for (let i = 2; i < argv.length; i++) {
37 | const val = argv[i];
38 |
39 | if (['--verbose', '-v'].includes(val)) {
40 | options.VERBOSE = true;
41 | globals.verbose = true;
42 | continue;
43 | }
44 |
45 | // TODO: we might move these to actions as well.
46 | if (['--image', '-i'].includes(val)) {
47 | options.DOCKER_IMAGE = argv[++i];
48 | continue;
49 | } else if (val.indexOf('--image=') === 0) {
50 | options.DOCKER_IMAGE = val.split('=')[1];
51 | continue;
52 | }
53 |
54 | if (['--directory', '-d'].includes(val)) {
55 | options.VENDOR_DIR = argv[++i];
56 | continue;
57 | } else if (val.indexOf('--directory=') === 0) {
58 | options.VENDOR_DIR = val.split('=')[1];
59 | continue;
60 | }
61 |
62 | if (['--strategy', '-s'].includes(val)) {
63 | options.VENDOR_STRAT = /** @type VendorStrategy */ (argv[++i]);
64 | continue;
65 | } else if (val.indexOf('--strategy=') === 0) {
66 | options.VENDOR_STRAT = /** @type VendorStrategy */ (val.split('=')[1]);
67 | continue;
68 | }
69 | if (
70 | options.VENDOR_STRAT &&
71 | !ACCEPTED_STRATEGIES.includes(options.VENDOR_STRAT)
72 | ) {
73 | log(chalk.red(`Invalid strategy \`${options.VENDOR_STRAT}\``));
74 | process.exit(STATUS_BAD_PARAM);
75 | }
76 |
77 | if (['--file', '-f'].includes(val)) {
78 | options.FILE = argv[++i];
79 | continue;
80 | } else if (val.indexOf('--file=') === 0) {
81 | options.FILE = val.split('=')[1];
82 | continue;
83 | }
84 |
85 | if (['--branch', '-b'].includes(val)) {
86 | options.BRANCH = argv[++i];
87 | continue;
88 | } else if (val.indexOf('--branch=') === 0) {
89 | options.BRANCH = val.split('=')[1];
90 | continue;
91 | }
92 |
93 | if (val.indexOf('-') == 0) {
94 | log(chalk.red(`Invalid flag \`${val}\``));
95 | process.exit(STATUS_BAD_PARAM);
96 | }
97 |
98 | if (options.CURRENT_ACTION) {
99 | log(chalk.red(`Expected only a single action, found: \`${val}\``));
100 | process.exit(STATUS_BAD_PARAM);
101 | }
102 |
103 | options.CURRENT_ACTION = actions[/** @type {keyof Actions} */ (val)];
104 |
105 | if (!options.CURRENT_ACTION) {
106 | log(chalk.red(`Invalid action \`${val}\``));
107 | process.exit(STATUS_BAD_PARAM);
108 | }
109 |
110 | if (options.CURRENT_ACTION.forwardsRestParams) {
111 | options.EXTRA_PARAMS = argv.slice(i + 1);
112 | break;
113 | }
114 | }
115 |
116 | if (!options.CURRENT_ACTION) {
117 | log(chalk.red('No action provided'));
118 | process.exit(STATUS_BAD_PARAM);
119 | }
120 |
121 | if (
122 | options.CURRENT_ACTION === actions.exec &&
123 | options.EXTRA_PARAMS.length === 0
124 | ) {
125 | log(chalk.red('You should provide a command to exec'));
126 | process.exit(STATUS_BAD_PARAM);
127 | }
128 |
129 | if (
130 | !(
131 | /** @type {typeof actions[keyof actions][]} */ ([
132 | actions.init,
133 | actions.update,
134 | ]).includes(options.CURRENT_ACTION)
135 | ) &&
136 | options.DOCKER_IMAGE
137 | ) {
138 | log(chalk.red('Invalid flag: --image'));
139 | process.exit(STATUS_BAD_PARAM);
140 | }
141 |
142 | if (
143 | !(
144 | /** @type {typeof actions[keyof actions][]} */ ([
145 | actions.disasm,
146 | ]).includes(options.CURRENT_ACTION)
147 | ) &&
148 | options.FILE
149 | ) {
150 | log(chalk.red('Invalid flag: --file'));
151 | process.exit(STATUS_BAD_PARAM);
152 | }
153 |
154 | if (
155 | !(
156 | /** @type {typeof actions[keyof actions][]} */ ([actions.init]).includes(
157 | options.CURRENT_ACTION
158 | )
159 | ) &&
160 | options.BRANCH
161 | ) {
162 | log(chalk.red('Invalid flag: --branch'));
163 | process.exit(STATUS_BAD_PARAM);
164 | }
165 |
166 | return { options: /** @type CommandlineOptions */ (options) };
167 | };
168 |
169 | module.exports = {
170 | parseParameters,
171 | };
172 |
--------------------------------------------------------------------------------
/modules/project-info.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const os = require('os');
3 | const fs = require('fs/promises');
4 |
5 | const {
6 | checkContainerAndClean,
7 | initGitAndCacheContainerId,
8 | } = require('./utils');
9 |
10 | const { findNPMRoot } = require('./npm-utils');
11 |
12 | const {
13 | LIBDRAGON_PROJECT_MANIFEST,
14 | CONFIG_FILE,
15 | DEFAULT_STRATEGY,
16 | LIBDRAGON_SUBMODULE,
17 | CACHED_CONTAINER_FILE,
18 | CONTAINER_TARGET_PATH,
19 | LIBDRAGON_BRANCH,
20 | } = require('./constants');
21 |
22 | const {
23 | fileExists,
24 | log,
25 | spawnProcess,
26 | toPosixPath,
27 | assert,
28 | ParameterError,
29 | isProjectAction,
30 | getImageName,
31 | } = require('./helpers');
32 |
33 | /**
34 | * @typedef { typeof import('./constants').NO_PROJECT_ACTIONS[number] } ActionsNoProject
35 | * @typedef { Exclude } ActionsWithProject
36 | * @typedef { import('./parameters').CommandlineOptions } ActionsNoProjectOptions
37 | * @typedef { import('./parameters').CommandlineOptions } ActionsWithProjectOptions
38 | *
39 | * This is all the potential CLI combinations
40 | * @typedef { {
41 | * options: import('./parameters').CommandlineOptions
42 | * } } CLIInfo
43 | *
44 | * Then readProjectInfo creates two possible set of outputs. One is for actions
45 | * that don't need a project and one with project. This setup forces the actions
46 | * to not use detailed information if they are listed in NO_PROJECT_ACTIONS
47 | * @typedef { {
48 | * options: ActionsNoProjectOptions
49 | * } } NoProjectInfo
50 | *
51 | * @typedef { {
52 | * options: ActionsWithProjectOptions
53 | * } } ProjectInfo
54 | *
55 | * @typedef { {
56 | * options: ActionsWithProjectOptions
57 | * root: string;
58 | * userInfo: os.UserInfo;
59 | * haveProjectConfig: boolean;
60 | * imageName: string;
61 | * vendorDirectory: string;
62 | * vendorStrategy: import('./parameters').VendorStrategy;
63 | * containerId?: string;
64 | * activeBranchName: string;
65 | * } } LibdragonInfo
66 | */
67 |
68 | /**
69 | * @param {LibdragonInfo} libdragonInfo
70 | */
71 | async function findContainerId(libdragonInfo) {
72 | assert(
73 | !process.env.DOCKER_CONTAINER,
74 | new Error('[findContainerId] We should already know we are in a container.')
75 | );
76 |
77 | const idFile = path.join(libdragonInfo.root, '.git', CACHED_CONTAINER_FILE);
78 | if (await fileExists(idFile)) {
79 | const id = (await fs.readFile(idFile, { encoding: 'utf8' })).trim();
80 | log(`Read containerId: ${id}`, true);
81 | return id;
82 | }
83 |
84 | // TODO: use docker container ls -a --format "{{.Labels}}" instead
85 | // from what I remember this used to not work (i.e the property was not exposed before)
86 | const candidates = (
87 | await spawnProcess('docker', [
88 | 'container',
89 | 'ls',
90 | '-a',
91 | '--format',
92 | '{{.}}{{.ID}}',
93 | '-f',
94 | 'volume=' + CONTAINER_TARGET_PATH,
95 | ])
96 | )
97 | .split('\n')
98 | // docker seem to save paths with posix separators but make sure we look for
99 | // both, just in case
100 | .filter(
101 | (s) =>
102 | s.includes(`${toPosixPath(libdragonInfo.root)} `) ||
103 | s.includes(`${libdragonInfo.root} `)
104 | );
105 |
106 | if (candidates.length > 0) {
107 | const str = candidates[0];
108 | const shortId = str.slice(-12);
109 | const idIndex = str.indexOf(shortId);
110 | const longId = str.slice(idIndex, idIndex + 64);
111 | if (longId.length === 64) {
112 | // This shouldn't happen but if the user somehow deleted the .git folder
113 | // (we don't have the container id file at this point) we can recover the
114 | // project. This is safe and it is not executed if strategy is `manual`
115 | await initGitAndCacheContainerId({
116 | ...libdragonInfo,
117 | containerId: longId,
118 | });
119 | return longId;
120 | }
121 | }
122 | }
123 |
124 | /**
125 | * @param {string} start
126 | * @param {string} relativeFile
127 | * @returns {Promise}
128 | */
129 | async function findLibdragonRoot(
130 | start = '.',
131 | relativeFile = path.join(LIBDRAGON_PROJECT_MANIFEST, CONFIG_FILE)
132 | ) {
133 | const fileToCheck = path.join(start, relativeFile);
134 | log(`Checking if file exists: ${path.resolve(fileToCheck)}`, true);
135 | if (await fileExists(fileToCheck)) {
136 | return path.resolve(start);
137 | }
138 |
139 | const parent = path.resolve(start, '..');
140 | if (parent !== path.resolve(start)) {
141 | return findLibdragonRoot(parent, relativeFile);
142 | } else {
143 | return;
144 | }
145 | }
146 |
147 | async function findGitRoot() {
148 | try {
149 | return (await spawnProcess('git', ['rev-parse', '--show-toplevel'])).trim();
150 | } catch {
151 | // No need to do anything if the user does not have git
152 | return undefined;
153 | }
154 | }
155 |
156 | /**
157 | * @param {CLIInfo} optionInfo
158 | * @returns {Promise}
159 | */
160 | const readProjectInfo = async function (optionInfo) {
161 | // No need to do anything here if the action does not depend on the project
162 | // The only exception is the init and destroy actions, which do not need an
163 | // existing project but readProjectInfo must always run to analyze the situation
164 | if (!isProjectAction(optionInfo)) {
165 | return /** @type {NoProjectInfo} */ (optionInfo);
166 | }
167 |
168 | const projectRoot = await findLibdragonRoot();
169 |
170 | if (
171 | !projectRoot &&
172 | !['init', 'destroy'].includes(optionInfo.options.CURRENT_ACTION.name)
173 | ) {
174 | throw new ParameterError(
175 | 'This is not a libdragon project. Initialize with `libdragon init` first.',
176 | optionInfo.options.CURRENT_ACTION.name
177 | );
178 | }
179 |
180 | const foundRoot =
181 | projectRoot ?? (await findNPMRoot()) ?? (await findGitRoot());
182 | if (!foundRoot) {
183 | log('Could not find project root, set as cwd.', true);
184 | }
185 |
186 | /** @type {LibdragonInfo} */
187 | let info = {
188 | ...optionInfo,
189 | root: foundRoot ?? process.cwd(),
190 | userInfo: os.userInfo(),
191 |
192 | // Use this to discriminate if there is a project when the command is run
193 | // Only used for the init action ATM, and it is not ideal to have this here
194 | haveProjectConfig: !!projectRoot,
195 |
196 | vendorDirectory: toPosixPath(path.join('.', LIBDRAGON_SUBMODULE)),
197 | vendorStrategy: DEFAULT_STRATEGY,
198 |
199 | activeBranchName: LIBDRAGON_BRANCH,
200 |
201 | // This is invalid at this point, but is overridden below from config file
202 | // or container if it doesn't exist in the file. If a flag is provided, it
203 | // will be overridden later.
204 | imageName: '',
205 | };
206 |
207 | log(`Project root: ${info.root}`, true);
208 |
209 | if (projectRoot) {
210 | info = {
211 | ...info,
212 | ...JSON.parse(
213 | await fs.readFile(
214 | path.join(info.root, LIBDRAGON_PROJECT_MANIFEST, CONFIG_FILE),
215 | { encoding: 'utf8' }
216 | )
217 | ),
218 | };
219 | }
220 |
221 | if (!process.env.DOCKER_CONTAINER) {
222 | info.containerId = await findContainerId(info);
223 | log(`Active container id: ${info.containerId}`, true);
224 | }
225 |
226 | // If still have the container, read the image name from it
227 | // No need to do anything if we are in a container
228 | if (
229 | !process.env.DOCKER_CONTAINER &&
230 | !info.imageName &&
231 | info.containerId &&
232 | (await checkContainerAndClean(info))
233 | ) {
234 | info.imageName = (
235 | await spawnProcess('docker', [
236 | 'container',
237 | 'inspect',
238 | info.containerId,
239 | '--format',
240 | '{{.Config.Image}}',
241 | ])
242 | ).trim();
243 | }
244 |
245 | // For imageName, flag has the highest priority followed by the one read from
246 | // the file and then if there is any matching container, name is read from it.
247 | // Next if `--branch` flag and as last option fallback to default value.
248 | // This represents the current value, so the flag should be processed later.
249 | // If we were to update it here, it would be impossible to know the change
250 | // with how we structure the code right now.
251 | info.imageName = info.imageName || getImageName(info.activeBranchName);
252 |
253 | log(`Active image name: ${info.imageName}`, true);
254 | log(`Active vendor directory: ${info.vendorDirectory}`, true);
255 | log(`Active vendor strategy: ${info.vendorStrategy}`, true);
256 | log(`Active branch: ${info.activeBranchName}`, true);
257 |
258 | return info;
259 | };
260 |
261 | /**
262 | * @param { LibdragonInfo | void } info This is only the base info without options
263 | * fn and command line options
264 | */
265 | async function writeProjectInfo(info) {
266 | // Do not log anything here as it may litter the output being always run on exit
267 | if (!info) return;
268 |
269 | const projectPath = path.join(info.root, LIBDRAGON_PROJECT_MANIFEST);
270 |
271 | const pathExists = await fs.stat(projectPath).catch((e) => {
272 | if (e.code !== 'ENOENT') throw e;
273 | return false;
274 | });
275 |
276 | if (!pathExists) {
277 | log(`Creating libdragon project configuration at \`${info.root}\`.`, true);
278 | await fs.mkdir(projectPath);
279 | }
280 |
281 | assert(
282 | toPosixPath(info.vendorDirectory) === info.vendorDirectory,
283 | new Error('vendorDirectory should always be in posix format')
284 | );
285 |
286 | await fs.writeFile(
287 | path.join(projectPath, CONFIG_FILE),
288 | JSON.stringify(
289 | {
290 | imageName: info.imageName,
291 | vendorDirectory: info.vendorDirectory,
292 | vendorStrategy: info.vendorStrategy,
293 | },
294 | null,
295 | ' '
296 | )
297 | );
298 | log(`Configuration file updated`, true);
299 | }
300 |
301 | module.exports = { readProjectInfo, writeProjectInfo };
302 |
--------------------------------------------------------------------------------
/modules/utils.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const fs = require('fs/promises');
3 |
4 | const { CONTAINER_TARGET_PATH, CACHED_CONTAINER_FILE } = require('./constants');
5 |
6 | const {
7 | fileExists,
8 | log,
9 | spawnProcess,
10 | dockerExec,
11 | dirExists,
12 | assert,
13 | ValidationError,
14 | toNativePath,
15 | } = require('./helpers');
16 |
17 | const { dockerHostUserParams } = require('./docker-utils');
18 |
19 | /**
20 | * @param {import('./project-info').LibdragonInfo} libdragonInfo
21 | */
22 | const installDependencies = async (libdragonInfo) => {
23 | const buildScriptPath = path.join(
24 | libdragonInfo.root,
25 | toNativePath(libdragonInfo.vendorDirectory),
26 | 'build.sh'
27 | );
28 | if (!(await fileExists(buildScriptPath))) {
29 | throw new ValidationError(
30 | `build.sh not found. Make sure you have a vendored libdragon copy at ${libdragonInfo.vendorDirectory}`
31 | );
32 | }
33 |
34 | log('Installing libdragon to the container...');
35 |
36 | await dockerExec(
37 | libdragonInfo,
38 | [
39 | '--workdir',
40 | CONTAINER_TARGET_PATH + '/' + libdragonInfo.vendorDirectory,
41 | ...dockerHostUserParams(libdragonInfo),
42 | ],
43 | ['/bin/bash', './build.sh']
44 | );
45 | };
46 |
47 | /**
48 | * Downloads the given docker image. Returns false if the local image is the
49 | * same, true otherwise.
50 | * @param {import('./project-info').LibdragonInfo} libdragonInfo
51 | * @param {string} newImageName
52 | */
53 | const updateImage = async (libdragonInfo, newImageName) => {
54 | assert(
55 | !process.env.DOCKER_CONTAINER,
56 | new Error('[updateImage] should not be called in a container')
57 | );
58 |
59 | // Will not take too much time if already have the same
60 | const download = async () => {
61 | log(`Downloading docker image: ${newImageName}`);
62 | await spawnProcess('docker', ['pull', newImageName], {
63 | // We don't need to read them, let it show the user
64 | inheritStdout: true,
65 | inheritStderr: true,
66 | });
67 | };
68 |
69 | const getDigest = async () =>
70 | await spawnProcess('docker', ['images', '-q', '--no-trunc', newImageName]);
71 |
72 | // Attempt to compare digests if the new image name is the same
73 | // Even if they are not the same tag, it is possible to have a different
74 | // image but we already attempt a download in any case. It would just take
75 | // less time as we already have the layers.
76 | if (libdragonInfo.imageName === newImageName) {
77 | const existingDigest = await getDigest();
78 | await download();
79 | const newDigest = await getDigest();
80 |
81 | if (existingDigest === newDigest) {
82 | log(`Image is the same: ${newImageName}`, true);
83 | return false;
84 | }
85 | } else {
86 | await download();
87 | }
88 |
89 | log(`Image is different: ${newImageName}`, true);
90 | return true;
91 | };
92 |
93 | /**
94 | * @param {import('./project-info').LibdragonInfo} libdragonInfo
95 | */
96 | const destroyContainer = async (libdragonInfo) => {
97 | assert(
98 | !process.env.DOCKER_CONTAINER,
99 | new Error('[destroyContainer] should not be called in a container')
100 | );
101 |
102 | if (libdragonInfo.containerId) {
103 | await spawnProcess('docker', [
104 | 'container',
105 | 'rm',
106 | libdragonInfo.containerId,
107 | '--force',
108 | ]);
109 | }
110 |
111 | await checkContainerAndClean({
112 | ...libdragonInfo,
113 | containerId: undefined, // We just destroyed it
114 | });
115 | };
116 |
117 | /**
118 | * Invokes host git with provided params.
119 | */
120 |
121 | /**
122 | *
123 | * @param {import('./project-info').LibdragonInfo} libdragonInfo
124 | * @param {string[]} params
125 | * @param {import('./helpers').SpawnOptions} options
126 | */
127 | async function runGit(libdragonInfo, params, options = {}) {
128 | assert(
129 | libdragonInfo.vendorStrategy !== 'manual',
130 | new Error('Should never run git if vendoring strategy is manual.')
131 | );
132 | const isWin = /^win/.test(process.platform);
133 |
134 | return await spawnProcess(
135 | 'git',
136 | ['-C', libdragonInfo.root, ...params],
137 | // Windows git is breaking the TTY somehow - disable TTY for now
138 | // We are not able to display progress for the initial clone b/c of this
139 | // Enable progress otherwise.
140 | isWin
141 | ? { inheritStdin: false, ...options }
142 | : { inheritStdout: true, inheritStderr: true, ...options }
143 | );
144 | }
145 |
146 | /**
147 | * @param {import('./project-info').LibdragonInfo} libdragonInfo
148 | */
149 | async function checkContainerAndClean(libdragonInfo) {
150 | assert(
151 | !process.env.DOCKER_CONTAINER,
152 | new Error(
153 | '[checkContainerAndClean] We should already know we are in a container.'
154 | )
155 | );
156 |
157 | const id =
158 | libdragonInfo.containerId &&
159 | (
160 | await spawnProcess('docker', [
161 | 'container',
162 | 'ls',
163 | '-qa',
164 | '-f id=' + libdragonInfo.containerId,
165 | ])
166 | ).trim();
167 |
168 | // Container does not exist, clean the id up
169 | if (!id) {
170 | const containerIdFile = path.join(
171 | libdragonInfo.root,
172 | '.git',
173 | CACHED_CONTAINER_FILE
174 | );
175 | if (await fileExists(containerIdFile)) {
176 | await fs.rm(containerIdFile);
177 | }
178 | }
179 | return id ? libdragonInfo.containerId : undefined;
180 | }
181 |
182 | /**
183 | * @param {string} containerId
184 | */
185 | async function checkContainerRunning(containerId) {
186 | assert(
187 | !process.env.DOCKER_CONTAINER,
188 | new Error(
189 | '[checkContainerRunning] We should already know we are in a container.'
190 | )
191 | );
192 |
193 | const running = (
194 | await spawnProcess('docker', [
195 | 'container',
196 | 'ls',
197 | '-q',
198 | '-f id=' + containerId,
199 | ])
200 | ).trim();
201 | return running ? containerId : undefined;
202 | }
203 |
204 | /**
205 | * @param {import('./project-info').LibdragonInfo & {containerId: string}} libdragonInfo
206 | */
207 | async function initGitAndCacheContainerId(libdragonInfo) {
208 | if (!libdragonInfo.containerId) {
209 | return;
210 | }
211 |
212 | // If there is managed vendoring, make sure we have a git repo.
213 | if (libdragonInfo.vendorStrategy !== 'manual') {
214 | await ensureGit(libdragonInfo);
215 | }
216 |
217 | const rootGitFolder = (
218 | await spawnProcess('git', [
219 | 'rev-parse',
220 | '--show-superproject-working-tree',
221 | ]).catch(() => {
222 | // Probably host does not have git, can ignore
223 | return '';
224 | })
225 | ).trim();
226 |
227 | // Fallback to the potential git root on the project root if there is no parent
228 | // git project.
229 | const gitFolder = path.join(rootGitFolder || libdragonInfo.root, '.git');
230 | if (await dirExists(gitFolder)) {
231 | await fs.writeFile(
232 | path.join(gitFolder, CACHED_CONTAINER_FILE),
233 | libdragonInfo.containerId
234 | );
235 | }
236 | }
237 |
238 | /**
239 | * Makes sure there is a parent git repository. If not, it will create one at
240 | * project root.
241 | * @param {import('./project-info').LibdragonInfo} info
242 | */
243 | async function ensureGit(info) {
244 | const gitRoot = (
245 | await runGit(info, ['rev-parse', '--show-toplevel'], {
246 | inheritStdin: false,
247 | inheritStdout: false,
248 | inheritStderr: false,
249 | }).catch(() => {
250 | // Probably host does not have git, can ignore
251 | return '';
252 | })
253 | ).trim();
254 |
255 | // If the host does not have git installed, this will not run unless we
256 | // have already initialized it via the container, in which case we would
257 | // have it as the git root. This is not expected to mess with host git flows
258 | // where there is a git working tree higher in the host filesystem, which
259 | // the container does not have access to.
260 | if (!gitRoot) {
261 | await runGit(info, ['init']);
262 | }
263 | }
264 |
265 | module.exports = {
266 | installDependencies,
267 | updateImage,
268 | destroyContainer,
269 | checkContainerRunning,
270 | checkContainerAndClean,
271 | initGitAndCacheContainerId,
272 | runGit,
273 | ensureGit,
274 | };
275 |
--------------------------------------------------------------------------------
/pack.mjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env zx
2 | // TODO: enable type checking for this file.
3 | import path from 'node:path';
4 | import { copyFileSync } from 'node:fs';
5 | import * as esbuild from 'esbuild';
6 | import 'zx/globals';
7 |
8 | /* eslint-disable no-undef */
9 |
10 | if (!process.argv[2]) {
11 | // eslint-disable-next-line no-console
12 | console.error(
13 | 'No version is provided, building cli with the existing version.'
14 | );
15 | }
16 |
17 | if (process.platform === 'win32') {
18 | usePowerShell();
19 | // for PowerShell compatibility
20 | $.prefix = '$ErrorActionPreference = "Stop";';
21 | $.postfix = '; exit $LastExitCode';
22 | }
23 |
24 | // TODO: Enable useSnapshot for a faster startup in the future
25 |
26 | let executableName = 'libdragon';
27 | if (process.platform === 'win32') {
28 | executableName = 'libdragon.exe';
29 | } else if (process.platform === 'darwin') {
30 | executableName = 'libdragon-macos';
31 | } else if (process.platform === 'linux') {
32 | executableName = 'libdragon-linux';
33 | }
34 |
35 | const executablePath = path.join('build', executableName);
36 |
37 | await esbuild.build({
38 | entryPoints: ['index.js'],
39 | bundle: true,
40 | format: 'esm',
41 | platform: 'node',
42 | target: 'node22',
43 | outfile: path.join('build', 'main.js'),
44 | minify: true,
45 | loader: {
46 | '.c': 'text',
47 | '.mk': 'text',
48 | },
49 | define: {
50 | ...(process.argv[2] && {
51 | 'globalThis.VERSION': JSON.stringify(process.argv[2]),
52 | }),
53 | },
54 | });
55 |
56 | await $`node --experimental-sea-config sea-config.json`;
57 |
58 | copyFileSync(process.execPath, executablePath);
59 |
60 | if (process.platform === 'win32') {
61 | await $`signtool remove /s ${executablePath}`.nothrow();
62 | } else if (process.platform === 'darwin') {
63 | await $`codesign --remove-signature ${executablePath}`.nothrow();
64 | }
65 |
66 | await $([
67 | `npx postject ${executablePath} NODE_SEA_BLOB ${path.join(
68 | 'build',
69 | 'sea-prep.blob'
70 | )} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 --macho-segment-name NODE_SEA`,
71 | ]);
72 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "libdragon",
3 | "version": "12.0.4",
4 | "description": "This is a docker wrapper for libdragon",
5 | "main": "index.js",
6 | "engines": {
7 | "node": ">=22",
8 | "npm": ">=10"
9 | },
10 | "bin": {
11 | "libdragon": "./index.js"
12 | },
13 | "scripts": {
14 | "test": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js",
15 | "test:watch": "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --watch",
16 | "libdragon": "node index.js",
17 | "start": "node index.js start",
18 | "stop": "node index.js stop",
19 | "pack": "node pack.mjs",
20 | "bundle": "node bundle.mjs",
21 | "format": "prettier **/*.js **/*.mjs **/*.cjs --write",
22 | "format-check": "prettier **/*.js **/*.mjs **/*.cjs --check",
23 | "lint": "eslint --fix modules/**/*.js *.js *.mjs *.cjs",
24 | "lint-check": "eslint modules/**/*.js *.js *.mjs *.cjs",
25 | "tsc": "tsc"
26 | },
27 | "repository": {
28 | "type": "git",
29 | "url": "git+https://github.com/anacierdem/libdragon-docker.git"
30 | },
31 | "files": [
32 | "modules/**",
33 | "skeleton/**"
34 | ],
35 | "author": "Ali Naci Erdem",
36 | "license": "MIT",
37 | "bugs": {
38 | "url": "https://github.com/anacierdem/libdragon-docker/issues"
39 | },
40 | "homepage": "https://github.com/anacierdem/libdragon-docker#readme",
41 | "dependencies": {
42 | "chalk": "^4.1.0",
43 | "command-line-usage": "^6.1.1",
44 | "zx": "^8.1.8"
45 | },
46 | "devDependencies": {
47 | "@semantic-release/changelog": "^6.0.1",
48 | "@semantic-release/exec": "^6.0.3",
49 | "@semantic-release/git": "^10.0.1",
50 | "@types/command-line-usage": "^5.0.2",
51 | "commitizen": "^4.2.4",
52 | "cz-conventional-changelog": "^3.3.0",
53 | "esbuild": "^0.20.0",
54 | "eslint": "^9.11.0",
55 | "jest": "^29.5.0",
56 | "postject": "^1.0.0-alpha.6",
57 | "prettier": "^2.4.0",
58 | "ref-napi": "^3.0.3",
59 | "semantic-release": "^24.0.0",
60 | "typescript": "^4.7.4",
61 | "zx": "^8.1.8"
62 | },
63 | "overrides": {
64 | "minimist": "1.2.6"
65 | },
66 | "release": {
67 | "plugins": [
68 | "@semantic-release/commit-analyzer",
69 | "@semantic-release/release-notes-generator",
70 | "@semantic-release/changelog",
71 | "@semantic-release/npm",
72 | [
73 | "@semantic-release/exec",
74 | {
75 | "verifyReleaseCmd": "echo \"${nextRelease.version}\" >> version.txt",
76 | "prepareCmd": "npm run bundle ${nextRelease.version}"
77 | }
78 | ],
79 | [
80 | "@semantic-release/github",
81 | {
82 | "assets": [
83 | {
84 | "path": "libdragon-linux-x86_64.tar.gz",
85 | "label": "Linux executable"
86 | },
87 | {
88 | "path": "libdragon-macos-arm64.tar.gz",
89 | "label": "MacOS executable"
90 | },
91 | {
92 | "path": "libdragon-win-x86_64.zip",
93 | "label": "Windows executable"
94 | },
95 | {
96 | "path": "Output/libdragon-installer.exe",
97 | "label": "Windows installer"
98 | }
99 | ]
100 | }
101 | ],
102 | "@semantic-release/git"
103 | ]
104 | },
105 | "config": {
106 | "commitizen": {
107 | "path": "./node_modules/cz-conventional-changelog"
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/sea-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "build/main.js",
3 | "output": "build/sea-prep.blob",
4 | "useCodeCache": true,
5 | "disableExperimentalSEAWarning": true
6 | }
7 |
--------------------------------------------------------------------------------
/setup.iss:
--------------------------------------------------------------------------------
1 | ; Script generated by the Inno Setup Script Wizard.
2 | ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
3 |
4 | #define MyAppName "libdragon"
5 | #define MyAppPublisher "Libdragon"
6 | #define MyAppURL "https://libdragon.dev/"
7 | #define MyAppExeName "./tmp/libdragon.exe"
8 |
9 | [Setup]
10 | ; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
11 | ; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
12 | AppId={{B9EE00C4-986A-4E99-BE51-2730EFB899FF}
13 | AppName={#MyAppName}
14 | AppVersion={#MyAppVersion}
15 | AppVerName={#MyAppName} {#MyAppVersion}
16 | AppPublisher={#MyAppPublisher}
17 | AppPublisherURL={#MyAppURL}
18 | AppSupportURL={#MyAppURL}
19 | AppUpdatesURL={#MyAppURL}
20 | DefaultDirName={autopf}\{#MyAppName}
21 | DefaultGroupName={#MyAppName}
22 | DisableProgramGroupPage=yes
23 | LicenseFile=.\LICENSE.md
24 | OutputBaseFilename=libdragon-installer
25 | Compression=lzma
26 | SolidCompression=yes
27 | WizardStyle=modern
28 | ChangesEnvironment=yes
29 | ; TODO: It is possible to enable this if the executable itself does the
30 | ; PrivilegesRequiredOverridesAllowed=dialog
31 | SetupIconFile=libdragon.ico
32 | UninstallDisplayIcon=libdragon.ico
33 |
34 | [Languages]
35 | Name: "english"; MessagesFile: "compiler:Default.isl"
36 |
37 | [Files]
38 | Source: ".\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
39 | Source: ".\libdragon.ico"; DestDir: "{app}"; Flags: ignoreversion
40 | ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
41 |
42 | [Icons]
43 | Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}"; IconFilename: "{app}libdragon.ico"
44 |
45 | [Registry]
46 | Root: HKLM; Subkey: "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"; \
47 | ValueType: expandsz; ValueName: "Path"; ValueData: "{olddata};{app}"; \
48 | Check: NeedsAddPath(ExpandConstant('{app}'))
49 |
50 | [Code]
51 |
52 | const
53 | EnvironmentKey = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment';
54 |
55 | function NeedsAddPath(Param: string): boolean;
56 | var
57 | OrigPath: string;
58 | begin
59 | if not RegQueryStringValue(HKEY_LOCAL_MACHINE,
60 | EnvironmentKey,
61 | 'Path', OrigPath)
62 | then begin
63 | Result := True;
64 | exit;
65 | end;
66 | { look for the path with leading and trailing semicolon }
67 | { Pos() returns 0 if not found }
68 | Result := Pos(';' + Param + ';', ';' + OrigPath + ';') = 0;
69 | end;
70 |
71 |
72 | procedure RemovePath(Path: string);
73 | var
74 | Paths: string;
75 | P: Integer;
76 | begin
77 | if not RegQueryStringValue(HKLM, EnvironmentKey, 'Path', Paths) then begin
78 | Log('PATH not found');
79 | end else begin
80 | Log(Format('PATH is [%s]', [Paths]));
81 |
82 | P := Pos(';' + Uppercase(Path) + ';', ';' + Uppercase(Paths) + ';');
83 | if P = 0 then begin
84 | Log(Format('Path [%s] not found in PATH', [Path]));
85 | end else begin
86 | if P > 1 then P := P - 1;
87 | Delete(Paths, P, Length(Path) + 1);
88 | Log(Format('Path [%s] removed from PATH => [%s]', [Path, Paths]));
89 |
90 | if RegWriteStringValue(HKLM, EnvironmentKey, 'Path', Paths) then begin
91 | Log('PATH written');
92 | end
93 | end
94 | end
95 | end;
96 |
97 |
98 | procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
99 | begin
100 | if CurUninstallStep = usUninstall then
101 | begin
102 | RemovePath(ExpandConstant('{app}'));
103 | end;
104 | end;
--------------------------------------------------------------------------------
/skeleton/Makefile.mk:
--------------------------------------------------------------------------------
1 | V=1
2 | SOURCE_DIR=src
3 | BUILD_DIR=build
4 | include $(N64_INST)/include/n64.mk
5 |
6 | all: hello.z64
7 | .PHONY: all
8 |
9 | OBJS = $(BUILD_DIR)/main.o
10 |
11 | hello.z64: N64_ROM_TITLE="Hello World"
12 |
13 | $(BUILD_DIR)/hello.elf: $(OBJS)
14 |
15 | clean:
16 | rm -f $(BUILD_DIR)/* *.z64
17 | .PHONY: clean
18 |
19 | -include $(wildcard $(BUILD_DIR)/*.d)
--------------------------------------------------------------------------------
/skeleton/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module './src/main.c' {
2 | type tmp = string;
3 | export = tmp;
4 | }
5 |
--------------------------------------------------------------------------------
/skeleton/src/main.c:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include
4 |
5 | int main(void)
6 | {
7 | console_init();
8 |
9 | debug_init_usblog();
10 | console_set_debug(true);
11 |
12 | printf("Hello world!\n");
13 |
14 | while(1) {}
15 | }
--------------------------------------------------------------------------------
/src/Makefile:
--------------------------------------------------------------------------------
1 | V=1
2 | D=1
3 | BUILD_DIR=$(CURDIR)/build
4 | include $(N64_INST)/include/n64.mk
5 |
6 | all: test_bench.z64
7 | .PHONY: all
8 |
9 | OBJS = $(BUILD_DIR)/main.o
10 |
11 | test_bench.z64: N64_ROM_TITLE="Testbench"
12 | $(BUILD_DIR)/test_bench.elf: $(OBJS)
13 |
14 | clean:
15 | rm -f $(BUILD_DIR)/* test_bench.z64
16 | .PHONY: clean
17 |
18 | -include $(wildcard $(BUILD_DIR)/*.d)
--------------------------------------------------------------------------------
/src/main.c:
--------------------------------------------------------------------------------
1 | #include
2 |
3 | #include
4 |
5 | int main(void)
6 | {
7 | console_init();
8 |
9 | debug_init_usblog();
10 | console_set_debug(true);
11 |
12 | printf("Hello world!\n");
13 |
14 | while(1) {}
15 | }
--------------------------------------------------------------------------------
/test.mjs:
--------------------------------------------------------------------------------
1 | // TODO: enable type checking for this file.
2 | // import '@jest/globals';
3 | import fsp from 'fs/promises';
4 | import path from 'path';
5 | import os from 'os';
6 | import 'zx/globals';
7 |
8 | /* eslint-disable no-undef */
9 |
10 | if (process.platform === 'win32') {
11 | // ADDITONAL Windows weirdness: docker is not found in the PATH when running
12 | // tests. I fixed this by moving the docker bin path to one up in the list.
13 | // Either there is a limit on path with this jest+zx setup or something else
14 | // was messing with the path. The other path was:
15 | // C:\Program Files (x86)\Windows Kits\10\Windows Performance Toolkit\
16 | // and it is definitely not the only one with spaces in it.
17 |
18 | // Default to cmd.exe on windows. o/w zx will try to use wsl etc. which the
19 | // user may not have node installed. Theorerically, we could use the node from
20 | // the wsl container but if it is on the host system you'd need to include the
21 | // extension like node.exe
22 | // The reason why I don't use powershell is because it does not support the `--`
23 | // syntax for passing arguments to the script due to a bug? in npm.ps1 that gets
24 | // installed locally via @semantic-release. It is introduced in node_modules\.bin
25 | // and that path is added to the child process' PATH causing it to take precedence
26 | // on powershell. Also see https://github.com/npm/cli/issues/3136
27 | $.shell = true;
28 | // Defaults to "set -euo pipefail;" o/w
29 | $.prefix = '';
30 | }
31 |
32 | let repositoryDir;
33 | beforeAll(async () => {
34 | repositoryDir = process.cwd();
35 |
36 | // Make sure the cli is linked
37 | await $`npm link`;
38 | // Inside the project, the local cli is also inserted into the path
39 | // so we need to link it as well
40 | await $`npm link libdragon`;
41 | }, 60000);
42 |
43 | let projectDir;
44 | let lastCommand;
45 | let stopped = false;
46 | afterEach(async () => {
47 | // Stop further execution before giving control to the continuation.
48 | stopped = true;
49 | // Wait until dead, then the next iteration will not happen as we `stopped` it.
50 | // This is only necessary for the timeout condition
51 | try {
52 | await lastCommand?.kill();
53 | } catch {
54 | // ignore
55 | }
56 | try {
57 | await $`libdragon --verbose destroy`;
58 | } catch {
59 | // ignore
60 | }
61 | try {
62 | await fsp.rm(projectDir, {
63 | recursive: true,
64 | maxRetries: 3,
65 | retryDelay: 1000,
66 | });
67 | } catch {
68 | // ignore
69 | }
70 | lastCommand = undefined;
71 | }, 60000);
72 |
73 | beforeEach(async () => {
74 | stopped = false;
75 |
76 | // Windows' tmpdir returns a short path which is not compatible when doing one
77 | // of the internal comparisons in the libdragon cli. So we use the parent
78 | // directory instead.
79 | const tmpDir =
80 | process.platform === 'win32' ? path.join(repositoryDir, '..') : os.tmpdir();
81 | projectDir = await fsp.mkdtemp(path.join(tmpDir, 'libdragon-test-project-'));
82 |
83 | await cd(projectDir);
84 | });
85 |
86 | const runCommands = async (commands, beforeCommand) => {
87 | for (const command of commands) {
88 | await beforeCommand?.();
89 | // Do not invoke it as a tagged template literal. This will cause a parameter
90 | // replacement, which we don't want here.
91 | lastCommand = $([`libdragon -v ${command}`]);
92 | await lastCommand;
93 | if (stopped) break;
94 | }
95 | };
96 |
97 | describe('Smoke tests', () => {
98 | // A set of commands that should not fail when run in sequence.
99 | const commands = [
100 | 'make',
101 | 'exec ls',
102 | 'stop',
103 | 'start',
104 | 'update',
105 | 'install',
106 | '--file=./build/hello.elf disasm main',
107 | 'destroy',
108 | 'help',
109 | 'version',
110 | ];
111 |
112 | test('Can run a standard set of commands without failure', async () => {
113 | await runCommands(['init', 'init', ...commands, 'init -s submodule']);
114 | }, 240000);
115 |
116 | test('Can run a standard set of commands with subtree strategy', async () => {
117 | await runCommands([
118 | 'init -s subtree -d ./libdragon',
119 | 'init -s subtree -d ./libdragon',
120 | ...commands,
121 | 'init -s subtree -d ./libdragon',
122 | ]);
123 | }, 240000);
124 |
125 | // TODO: this will fail if the image is not compatible with the local libdragon
126 | test('Can run a standard set of commands with a manual libdragon vendor', async () => {
127 | // Copy the libdragon files to the project directory with a non-standard name
128 | await fsp.cp(path.join(repositoryDir, 'libdragon'), './libdragon_test', {
129 | recursive: true,
130 | });
131 |
132 | await runCommands([
133 | 'init -s manual -d ./libdragon_test',
134 | 'init -s manual -d ./libdragon_test',
135 | ...commands,
136 | 'init -s manual -d ./libdragon_test',
137 | ]);
138 | }, 240000);
139 |
140 | test('Can still run make even if stopped', async () => {
141 | await $`libdragon init`;
142 | await $`libdragon stop`;
143 | await $`libdragon make`;
144 | }, 120000);
145 | });
146 |
147 | describe('Additional verification', () => {
148 | // TODO: find a better way to test such stuff. Integration is good but makes
149 | // these really difficult to test. Or maybe we can have a flag to skip the build
150 | // step
151 |
152 | // TODO: this is the critical path, the performance can also be tested
153 | test('Can still start same container even if the container id is lost', async () => {
154 | await $`libdragon init`;
155 | const { stdout: containerId } = await $`libdragon start`;
156 | await fsp.rm('.git/libdragon-docker-container');
157 | const { stdout: newContainerId } = await $`libdragon start`;
158 | expect(newContainerId.trim()).toEqual(containerId.trim());
159 | }, 240000);
160 |
161 | test('should not start a new docker container on a second init', async () => {
162 | await $`libdragon init`;
163 | const { stdout: containerId } = await $`libdragon start`;
164 | // Ideally this second init should not re-install libdragon file if they exist
165 | await $`libdragon init`;
166 | expect((await $`libdragon start`).stdout.trim()).toEqual(
167 | containerId.trim()
168 | );
169 | }, 200000);
170 |
171 | test('should update only the image when --image flag is present', async () => {
172 | await $`libdragon init`;
173 | const { stdout: containerId } = await $`libdragon start`;
174 | const { stdout: image } =
175 | await $`docker container inspect ${containerId.trim()} --format "{{.Config.Image}}"`;
176 | const { stdout: branch } =
177 | await $`git -C ./libdragon rev-parse --abbrev-ref HEAD`;
178 |
179 | expect(branch.trim()).toEqual('trunk');
180 | expect(image.trim()).toEqual('ghcr.io/dragonminded/libdragon:latest');
181 |
182 | await $`libdragon init --image="ghcr.io/dragonminded/libdragon:unstable"`;
183 | const { stdout: newContainerId } = await $`libdragon start`;
184 | const { stdout: newBranch } =
185 | await $`git -C ./libdragon rev-parse --abbrev-ref HEAD`;
186 |
187 | expect(newBranch.trim()).toEqual('trunk');
188 |
189 | expect(newContainerId.trim()).not.toEqual(containerId);
190 | expect(
191 | (
192 | await $`docker container inspect ${newContainerId.trim()} --format "{{.Config.Image}}"`
193 | ).stdout.trim()
194 | ).toBe('ghcr.io/dragonminded/libdragon:unstable');
195 | }, 200000);
196 |
197 | test('should use the provided branch', async () => {
198 | await $`libdragon init --branch=unstable`;
199 | const { stdout: branch } =
200 | await $`git -C ./libdragon rev-parse --abbrev-ref HEAD`;
201 |
202 | expect(branch.trim()).toEqual('unstable');
203 | }, 120000);
204 |
205 | test('should recover the submodule branch after a destroy', async () => {
206 | await $`libdragon init --branch=unstable`;
207 | await $`libdragon destroy`;
208 | await $`libdragon init`;
209 | const { stdout: newContainerId } = await $`libdragon start`;
210 | const { stdout: branch } =
211 | await $`git -C ./libdragon rev-parse --abbrev-ref HEAD`;
212 |
213 | expect(
214 | (
215 | await $`docker container inspect ${newContainerId.trim()} --format "{{.Config.Image}}"`
216 | ).stdout.trim()
217 | ).toBe('ghcr.io/dragonminded/libdragon:unstable');
218 |
219 | expect(branch.trim()).toEqual('unstable');
220 | }, 200000);
221 | });
222 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "noImplicitAny": true,
5 | "experimentalDecorators": true,
6 | "rootDir": ".",
7 | "sourceMap": true,
8 | "listFiles": true,
9 | "allowJs": true,
10 | "checkJs": true,
11 | "resolveJsonModule": true,
12 | "noEmit": true,
13 | "strict": true
14 | },
15 | "include": ["index.js", "modules/**/*"]
16 | }
--------------------------------------------------------------------------------