macOS 언어 런타임(asdf) 및 오픈소스 생태계 도구(@oss) 통합 버전 매니저
단 하나의 .tool-versions 규격으로 언어 컴파일러·런타임과 언어별 결합된 오픈소스(opensource/in-use)의 브랜치·태그를 함께 관리합니다.
curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 \
https://raw.githubusercontent.com/coralstay/dev-toolchain/main/install.sh | sh
⚠️ macOS 전용, 개인 툴링입니다. Linux/Windows(WSL 포함) 지원 계획이 없고, 범용 프로덕션 도구로 만든 게 아니라 제 개인 Mac 워크플로우에 맞춰 만든 도구입니다 — 다른 환경에서의 동작이나 일반적인 사용 사례를 보장하지 않습니다.
새 Mac마다 Homebrew·asdf 설치, 플러그인 추가, .zshrc 손보기, 그리고 언어마다 따라붙는 수십 개의 오픈소스 도구(ruff, shellspec, bats, googletest, terraform, trivy, yq 등) 버전 맞추기를 반복하는 게 지겨워서 만들었습니다.
- 🍺 Homebrew 없어도 OK — 없으면 공식 스크립트로 자동 설치 (sudo 비밀번호만 직접 입력)
- ☑️ 체크박스 같은 대화형 설치 — 언어별로 설치 여부와 버전을 확인하고 골라서 설치
- 🔗 언어 + 오픈소스(
@oss) 단일 파일 통합 —.tool-versions파일 하나에 언어 런타임 버전과 오픈소스 브랜치/태그 종속성을 함께 선언 - 🌍 글로벌 SSOT + 프로젝트별 로컬 오버라이드 — 전역 기본값(
dev-tools/dev-toolchain/.tool-versions)과 프로젝트별 로컬(.tool-versions) 2계층 관리 - 🔙 깔끔한 제거 — 설치한 건 전부 되돌릴 수 있음 (
.bak백업까지 남김) - 🖥️ POSIX sh 호환 — macOS 기본 셸(
/bin/sh)에서도 그대로 동작, bash 특수문법 없음
~/githubs/bin/dev-toolchain 심볼릭 링크를 통해 어디서든 바로 실행할 수 있습니다.
# 글로벌 SSOT(언어 8종 + 오픈소스 48종) 및 모든 프로젝트의 로컬 .tool-versions 현황 출력
dev-toolchain status
# 현재 작업 디렉토리($PWD)에 적용된 로컬 .tool-versions 규칙 확인
dev-toolchain current
# 특정 프로젝트(이름 부분 일치)의 로컬 언어·오픈소스 종속성 조회
dev-toolchain project handcrafted-cpp
dev-toolchain project react-next
# 글로벌 .tool-versions의 @oss 카탈로그 기준으로 ~/githubs/opensource/{in-use,to-study} 동기화
dev-toolchain sync
# 전체 명령어 및 옵션 도움말 출력
dev-toolchain --help# 대화형 언어·버전 선택 설치기 실행
dev-toolchain
# 선택 화면 없이 기본 언어·버전 전체 자동 설치
dev-toolchain --all --yes
# 실제 변경 없이 실행 계획만 미리보기 (Dry-run)
dev-toolchain --dry-run --all --yes
# 전역(~/.tool-versions) 대신 특정 프로젝트 폴더에만 버전 고정
dev-toolchain --local=/path/to/project
# dev-toolchain이 설치한 런타임·플러그인·셸 설정 제거
dev-toolchain-uninstall
dev-toolchain-uninstall --dry-run --yesasdf는 #으로 시작하는 줄을 주석으로 무시하므로, 표준 .tool-versions 파일 안에 # @oss 지시자를 함께 적어 언어와 오픈소스 종속성을 단일 파일로 관리합니다.
# [1] Global Language Runtimes (asdf)
nodejs lts
pnpm 10.33.0
java temurin-25.0.2+10.0.LTS
gradle 9.4.1
python 3.12.13
uv 0.12.9
rust 1.94.0
golang 1.26.1
# [2] Global Coupled Open-Source Tooling (# @oss <소속언어> <저장소> <기본태그/브랜치> <원본URL>)
# @oss core Backlog.md v1.55.3 https://github.com/MrLesk/Backlog.md.git
# @oss core fzf v0.74.5 https://github.com/junegunn/fzf.git
# @oss core yq v4.54.1 https://github.com/mikefarah/yq.git
# @oss sh shellspec 0.29.0-dev https://github.com/shellspec/shellspec.git
# @oss python ruff 0.16.5 https://github.com/astral-sh/ruff.git
# @oss cpp googletest v1.15.2 https://github.com/google/googletest.git
# @oss infra terraform v1.16.0 https://github.com/hashicorp/terraform.git
# 예: freeride/doctree/.tool-versions
python 3.12.13
uv 0.12.9
# @oss python ruff 0.16.5
# @oss python vulture v2.16
# @oss core Backlog.md v1.55.3
다른 프로젝트가 오픈소스를 심볼릭 링크로 참조할 때 특정 버전을 가져올 수 있도록 아래 구조로 관리합니다:
opensource/in-use/<오픈소스명>/
├── upstream/ # 원본 upstream git clone (fetch/업데이트 모체)
├── bundles/
│ └── <버전태그>.bundle # 특정 버전의 git bundle 아카이브 파일
├── <버전태그>/ # git worktree로 풀려 있는 버전 폴더 (심볼릭 링크 타겟)
└── current -> <버전태그> # 전역 기본 버전 심볼릭 링크
dev-toolchain sync를 실행하면:- 글로벌 및 로컬
.tool-versions에 선언된 모든<버전태그>에 대해bundles/<버전태그>.bundle과<버전태그>/디렉터리를 생성합니다. - 각 프로젝트 폴더 안에
.opensource/<오픈소스명> -> ~/githubs/opensource/in-use/<오픈소스명>/<버전태그>심볼릭 링크를 자동 연결합니다.
- 글로벌 및 로컬
- CLI 명령어 사용법 (
dev-toolchain) - 글로벌 / 로컬
.tool-versions통합 명세 규격 - 빠른 시작
- 사전 요구사항
- 설치/제거되는 것
- 아키텍처
- 저장소 구조
- 알려진 한계 / 기술적 한계
- 기여하기
- License
curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 \
https://raw.githubusercontent.com/coralstay/dev-toolchain/main/install.sh | sh로컬에 이미 클론해뒀다면:
git clone https://github.com/coralstay/dev-toolchain.git && cd dev-toolchain
./install.sh언어별 설치 여부(↑/↓로 고르고 Enter로 확정, 숫자 키로 바로 선택도 가능)와 버전을 확인한 뒤, 마지막 확인을 거쳐 설치를 시작합니다. 각 질문에 답하면 그 자리에서 한 줄로 접혀서 다음 질문으로 넘어갑니다 — 아래는 실제 실행 화면입니다.
== Select languages to install ==
✔ Install nodejs (node)? Yes
✔ Version: lts (default)
✔ Also install pnpm (companion to nodejs)? Yes
✔ Version: 10.33.0 (default)
✔ Install java (java)? No
...
== Install list ==
nodejs lts
pnpm 10.33.0
python 3.12.13
rust 1.94.0
golang 1.26.1
✔ Pin these versions: Globally
✔ Install? Yes
마지막 프롬프트는 이번에 설치한 버전을 전역(
~/.tool-versions)에 고정할지, 지금 디렉토리에만 로컬로 고정할지 고릅니다(asdf의 표준 메커니즘 그대로 — nvm의.nvmrc같은 걸 언어 상관없이.tool-versions하나로 통일한 것뿐입니다).동반 도구(pnpm/gradle) 질문은 그 부모 언어(nodejs/java)를 수락했을 때만 따라 나옵니다 — 위 예시에서 java를
n으로 거절하니 gradle 질문 자체가 안 뜨는 걸 볼 수 있습니다.
옵션 플래그 (curl | sh -s -- <옵션> 형태로 전달 가능)
| 플래그 | 대상 | 동작 |
|---|---|---|
--all |
install | 언어 선택 화면 없이 .tool-versions에 있는 걸 전부 설치 |
--yes |
install / uninstall | 마지막 확인 프롬프트를 건너뜀 |
--dry-run |
install / uninstall | 실제로 아무것도 바꾸지 않고, 뭘 할지만 출력 |
--local / --local=<dir> |
install | 전역 대신 현재(또는 지정한) 디렉토리에만 버전 고정 |
여러 개를 조합할 수 있습니다: curl ... | sh -s -- --all --yes --dry-run처럼.
터미널(tty)이 없는 환경(CI 등)에서 실행하면 자동으로 --all처럼 동작합니다 — 입력을 기다리다 멈추지 않습니다.
| 필요한 것 | 없으면? |
|---|---|
| macOS | 이 도구는 macOS 전용입니다 |
git (원격 설치 시) |
Xcode Command Line Tools에 기본 포함 |
| 없으면 설치기가 알아서 설치합니다 (sudo 비밀번호는 직접 입력 필요) | |
없으면 brew install asdf로 알아서 설치합니다 |
.tool-versions에 정의된 기본 언어/버전 — 설치 화면에서 개별적으로 켜고 끄거나 버전을 바꿀 수 있습니다.
동반 도구(pnpm/gradle)는 각각 그 부모 언어를 설치할 때만 물어보는 선택 사항입니다.
| 언어 / 동반 도구 | 기본 버전 |
|---|---|
| 🟩 Node.js | lts |
| └ pnpm (동반) | 10.33.0 |
| ☕ Java (Temurin) | temurin-25.0.2+10.0.LTS |
| └ gradle (동반) | 9.4.1 |
| 🐍 Python | 3.12.13 |
| 🦀 Rust | 1.94.0 |
| 🐹 Go | 1.26.1 |
Python 컴파일에 필요한 Homebrew 패키지(openssl, readline, sqlite3, xz, zlib, tcl-tk)도 함께 설치됩니다.
이 도구는 위 언어(+동반 도구)와 그걸 위한 Homebrew 패키지 6개만 건드립니다 — 이미 있거나 앞으로
설치할 다른 brew 패키지, 이 도구가 깔지 않은 다른 asdf 플러그인은 install 쪽에서든 uninstall
쪽에서든 손대지 않습니다. uninstall은 langtoolchain이 설치하지 않은 asdf 플러그인 각각도,
asdf 자체와 ~/.asdf/(asdf 데이터 디렉토리) 전체 삭제도 모두 설치 시점 스냅샷을 기준으로
판단합니다 — 개별 플러그인이든 ~/.asdf/ 전체든, 설치 시점에 이미 있었다는 게 확인되면(또는
확인할 수 없으면) 삭제를 건너뛰고 경고만 남깁니다 — 다른 asdf 플러그인이 있어도 무조건 함께
지워지던 예전 동작과 다릅니다. uninstall이 phase 전체를 에러 없이 마치면 이 설치 시점 스냅샷
자체도 지워집니다 — 다음 install이 그 시점의 실제 상태로 새 기준선을 잡습니다.
⚠️ 동작 변경 안내(m-13): 이 변경 이전 버전으로 이미 설치해 둔 상태에서 langtoolchain을 업데이트만 한 경우, install 시점 스냅샷이 없으므로 uninstall을 실행해도 안전을 위해 asdf 플러그인 개별 삭제와~/.asdf/전체 삭제가 모두 기본적으로 스킵됩니다(설치 전부터 있던 상태인지 판단할 근거가 없기 때문 — "모르면 지우지 않는다"는 원칙). langtoolchain이 설치한 런타임까지 포함해 완전히 지우고 싶다면, uninstall 실행 후 화면에 안내되는 대로rm -rf ~/.asdf를 직접 실행하세요.
curl -fsSL --proto '=https' --proto-redir '=https' --tlsv1.2 \
https://raw.githubusercontent.com/coralstay/dev-toolchain/main/uninstall.sh | sh실행 전 한 번 확인을 물으며(--yes로 생략 가능), --dry-run도 동일하게 지원합니다. 설치/제거
후에는 exec $SHELL로 새 셸 세션을 열어야 PATH 등 캐시된 상태가 완전히 사라집니다.
install.sh/uninstall.sh가 각각 어떤 phase를 순서대로 거치는지, uninstall의 안전장치 (설치 전부터 있던 asdf 상태는 안 지운다)가 정확히 어느 지점에서 개입하는지를 다이어그램 으로 정리해뒀습니다 — docs/architecture.md.
| 경로 | 역할 |
|---|---|
install.sh / uninstall.sh |
CLI(dev-toolchain, dev-toolchain-uninstall) 및 curl-pipe 진입점 |
.tool-versions |
글로벌 언어 런타임(asdf) + 오픈소스 카탈로그(# @oss) 단일 SSOT 명세 |
scripts/oss_sync.sh |
글로벌/로컬 .tool-versions 파싱 및 opensource/{in-use,to-study} 조회·동기화(status/current/project/sync) |
scripts/install/, scripts/uninstall/ |
번호가 매겨진 phase 스크립트(00, 01, 02, ...) + 각각의 main.sh |
scripts/lib.sh |
설치/제거 전체가 공유하는 함수 라이브러리 (버전 fetch/캐시, 락, 타임아웃 등) |
scripts/lint/ |
shellcheck 외에 이 저장소 자체 lint 스크립트(하드코딩 경로 검사 등) |
spec/ |
shellspec 테스트 스위트 |
docs/ |
사람이 읽는 프로덕트 문서(아키텍처, 스타일 가이드, 조사 기록 등) |
backlog/ |
Backlog.md CLI가 관리하는 태스크/의사결정/마일스톤 — 폴더별 역할은 backlog/readme.md 참고 |
이 저장소의 backlog/config.yml은 filesystem_only: true, remote_operations: false로
설정돼 있습니다 — 원격 동기화 없이 로컬 git 저장소 안에서만 동작하는 구성입니다. 더 자세한
내용(각 폴더 안내, 의사결정 이력)은 backlog decision list나 backlog task view <ID>로
직접 조회하는 걸 권장합니다 — 여기서 전부 옮겨 적지는 않습니다.
- macOS 전용 — Linux/Windows 미지원.
- 언어 5개 고정 — Node.js/Java/Python/Rust/Go 외 언어는 코드를 직접 고쳐야 추가 가능. 순수 asdf처럼 임의 플러그인을 자유롭게 추가하는 기능은 없음.
- 동반 도구는 nodejs/java/python 셋뿐 — pnpm(nodejs), gradle(java), uv(python, poetry 대신 채택 — decision-5) 외 언어는 동반 도구 개념이 없음(Rust/Go는 cargo/모듈 시스템이 이미 내장이라 필요 없다고 판단). 언어당 동반 도구는 항상 1개만 제안하는 UX라 poetry를 쓰고 싶다면 "직접 입력" 경로로 pin하거나 저장소 밖에서 따로 관리해야 함.
- 핵심 목적("컴파일러 설치")보다 넓은 기능이 있음 — 전역/로컬 버전 고정, 대화형 선택기는 사실 asdf 버전 관리를 감싼 부가 기능. 한 번 검토를 거쳐 "유지"로 결정했고, 걷어낼 계획은 없음.
- Homebrew/asdf 외 도구체인은 지원 대상 아님 — MacPorts(
/opt/local)는 경로 자체가 겹치지 않아 파일 충돌은 없지만, 같은 이름의 바이너리를 깔았다면 rc 파일에서 나중에 소싱되는 쪽이 이긴다.mise처럼.tool-versions를 직접 읽고 자체 PATH 훅으로 셸을 활성화하는 도구는 더 실질적인 위험 — rc 파일에서 langtoolchain보다 나중에 로드되면 asdf shim을 조용히 가릴 수 있음. 둘 다 감지/경고 로직은 없음. - 동적 기본값이 asdf가 아직 못 따라잡은 최신 버전을 제안할 수 있음 — 언어 버전 기본값을 언어 공식 소스(예: cpython 태그)에서 실시간으로 가져오는데(m-12), asdf 플러그인이 그 버전을 아직 지원 안 하면 설치가 "version not installable"로 실패할 수 있음(decision-12). m-15(실제 설치 가능한 버전 목록 기반 선택 UI)가 완료되면 애초에 asdf 미지원 버전은 선택지에도 안 뜨게 되어 구조적으로 해소될 예정.
이 저장소가 실제로 무결성을 검증하는 지점과, Homebrew/asdf 같은 상위 도구에 신뢰를 위임했거나 저장소가 직접 손댈 수 없는 지점을 구분해 명시합니다. 지점별 상세(파일:라인, 현재 검증 수준)는 docs/download-points-inventory.md, 검증 기법 자체의 조사는 docs/download-integrity-techniques.md를 참고하세요.
이 저장소가 직접 검증하는 지점
| 지점 | 방식 |
|---|---|
install.sh/uninstall.sh의 self-clone |
플로팅 브랜치 대신 고정 커밋 SHA로 pin — 브랜치가 강제 push돼도 이미 pin된 커밋만 받음 |
Homebrew 공식 설치 스크립트(curl | bash) |
고정 커밋으로 pin + 이 프로젝트가 직접 계산한 SHA-256 체크섬을 fetch 직후 대조, 불일치 시 실행 거부 |
위임/통제 밖 지점 (이 저장소가 직접 검증하지 않음)
| 지점 | 위임 대상 / 통제 밖인 이유 |
|---|---|
brew install asdf, brew install <시스템 의존성 6개> |
Homebrew 자체의 bottle 서명/체크섬 체계에 위임 |
asdf plugin add(asdf-nodejs/asdf-python 등 플러그인 소스) |
asdf CLI(0.20.0)가 커밋 고정을 지원하지 않음 — 검토 후 미고정 결정, 매 install 실행마다 각 플러그인 저장소의 최신 HEAD로 갱신됨 |
asdf install(실제 언어 런타임 다운로드) |
각 asdf 플러그인 내부 로직 — 이 저장소가 직접 관여할 수 없음 |
명시적으로 범위 밖: GitHub 저장소/계정 자체의 탈취. 공격자가 저장소나 메인테이너 계정을 완전히 장악하면 install.sh에 박힌 고정 커밋 SHA 자체도 함께 바꿀 수 있어, 위 pin들로는 막을 수 없습니다. 이건 GitHub 쪽 계정 레벨 통제(브랜치 보호 규칙, 서명된 커밋 요구, 2FA)의 영역이며, 셸 설치 스크립트 하나가 풀 수 있는 문제가 아니라고 판단해 이 마일스톤의 범위 밖으로 뒀습니다.
- 로컬 shellspec은 실제 Homebrew/asdf를 건드리지 않음 —
spec/의 183개 예제는 전부 실제brew/asdf명령을 모킹하거나DRY_RUN=true로 실행되도록 설계됨(진짜 컴파일/설치는 느리고 개발 머신을 오염시키므로). 그래서 "진짜로 설치가 되는가" 자체는 로컬 스위트가 보장하지 않고,.github/workflows/e2e-verify.yml(GitHub 호스팅 macOS 러너, arm64+Intel)이 유일한 실기기 검증 경로임 —main브랜치에scripts/**/install.sh/uninstall.sh/.tool-versions가 바뀔 때만 push/PR로 자동 실행(그 외 커밋은workflow_dispatch로 수동 실행). - 화살표 키 TUI는 표준 터미널 기준으로만 검증 —
stty/dd로 raw 모드를 읽는lt_arrow_menu()는expect로 구동한 실제 pty(및 일반 터미널 앱)에서는 확인했지만, 모든 터미널 에뮬레이터·멀티플렉서(tmux/screen 등)·SSH 경유 세션 조합까지 다 검증하지는 않음 — 표준 3바이트 ANSI 이스케이프(ESC [ A/B)를 벗어나는 비표준 키 전송 방식에서는 예상과 다르게 동작할 수 있음. curl | sh의 "기본" 원격 clone 경로(진짜 GitHub 대상)는 로컬에서 재현하기 어려움 —LANGTOOLCHAIN_REPO_URL/LANGTOOLCHAIN_BRANCH환경변수로 fork/다른 브랜치를 가리켜 override하는 경로 자체는 TASK-117.6 이후 로컬 bare 저장소(file://)를 대상으로spec/repo_override_spec.sh가 커버함(pinned-fetch 메커니즘의 exact-ref 동작까지 포함). 다만 override 없이 기본값(고정 커밋 SHA, 실제 GitHub)을 타는 경로 자체는 여전히 로컬 스위트 대상이 아니며, 실제curl | sh로 이 저장소를 직접 당겨 검증한 것과.github/workflows/e2e-verify.yml의no-git-curl-pipe잡(실제 GitHub의mainraw 파일을 당김)이 유일한 검증 경로.
- MacPorts/mise 같은 경쟁 도구체인에 대한 감지·경고 로직(지금은 문서화만 하고 자동 감지는 없음).
- Linux 지원 — 지금은 계획이 없지만, 만약 하게 된다면 Homebrew macOS 전용 경로 판단 로직과 rc 파일 목록부터 다시 봐야 함.
- 언어/버전을 바꾸려면
.tool-versions한 줄만 수정하면 됩니다. - 각 설치/제거 단계는
scripts/install/,scripts/uninstall/아래 역할별로 분리돼 있고, 개별 실행도 가능합니다:DRY_RUN=true sh scripts/install/05_install_runtimes.sh - 코드를 고쳤으면:
shellcheck -s sh→dash -n(macOS 기본/bin/sh는 posix 모드 bash라 진짜 POSIX 위반을 놓침) →shellspec/shellspec --shell dash로spec/스위트 실행. - 스타일 규칙(들여쓰기/네이밍/따옴표 등)은 docs/shell-style-guide.md 참고 — Google Shell Style Guide를 기반으로 이 저장소의 POSIX sh 제약에 맞게 조정한 버전입니다.
- 경로/환경변수 하드코딩 재발 방지 감사(docs/hardcoded-paths-patterns.md)와
sed의 BSD/GNU 이식성 감사(docs/sed-portability-audit.md)는 각각scripts/lint/check-hardcoded-paths.sh와scripts/lib.sh/scripts/uninstall/03_clean_env_vars.sh의 실제 회귀 사례를 근거로 정리한 문서입니다. - POSIX sh를 계속 유지하기로 한 근거(bash로 옮겼을 때의 실제 이점/함정 조사)는 docs/posix-sh-vs-bash-research.md(decision-19) 참고.
- 전체 흐름만 확인:
./install.sh --dry-run --all --yes,./uninstall.sh --dry-run --yes - 실기기 검증(Homebrew 부트스트랩, Intel Mac 등)은
.github/workflows/e2e-verify.yml—scripts/**/install.sh/uninstall.sh/.tool-versions가 바뀐 채로main에 push/PR되면 자동 실행되고, 그 외에는workflow_dispatch로 수동 실행. GitHub 호스팅 macOS 러너(arm64+Intel)에서 검증, 공개 저장소라 무료.
GNU AGPL-3.0-only — 2026-10-31 MIT에서 변경했습니다(저작권자 단독). 변경 전 커밋까지의 사본은 계속 MIT입니다.
docs/shell-style-guide.md는 Google Shell Style Guide를 바탕으로 한 2차 저작물로, 원문 라이선스인 CC BY 3.0을 따릅니다.
Made with 🧉 by coralstay