├── .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 | [![Build](https://github.com/anacierdem/libdragon-docker/actions/workflows/release.yml/badge.svg?branch=master)](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 | } --------------------------------------------------------------------------------