본문으로 건너뛰기

CLI

빌드하고 실행하는 방법, 그리고 명령줄 옵션입니다.

문서 목록으로

오류 메시지별 대처는 트러블슈팅에 있습니다.


빌드

build/ 폴더에 플랫폼별 빌드 스크립트가 있습니다.

플랫폼빌드 스크립트산출 위치
Windowsbuild-win64.ps1bin/win-x64/tabbit.exe · bin/win-arm64/tabbit.exe
Linuxbuild-linux64.shbin/linux-x64/tabbit · bin/linux-arm64/tabbit
macOSbuild-osx64.shbin/osx-arm64/tabbit · bin/osx-x64/tabbit

빌드 전에 .NET 10 SDK를 설치해야 합니다.

아키텍처

아키텍처는 실행한 머신에서 판정합니다.

셸은 uname -m, PowerShell은 RuntimeInformation.OSArchitecture를 사용합니다. 애플 실리콘에서 build-osx64.sh를 실행하면 osx-arm64가 생성됩니다.

self-contained 산출물은 네이티브 코드이므로 아키텍처가 어긋나면 실행 자체가 되지 않습니다.

다른 아키텍처가 필요하면 런타임 식별자를 인자로 넘깁니다.

./build/build-osx64.sh osx-x64 # 인텔 맥
./build/build-linux64.sh linux-arm64
.\build\build-win64.ps1 win-arm64
.\build\build-osx64.ps1 osx-x64 # 윈도우에서 맥용으로 크로스 퍼블리시

build/의 PowerShell 스크립트는 각 플랫폼용으로 크로스 퍼블리시하는 용도이기도 합니다. 리눅스와 맥에서는 같은 이름의 .sh를 사용하면 되고, 그쪽이 아키텍처까지 판정합니다.

런타임 식별자마다 디렉터리가 분리됩니다

self-contained 퍼블리시는 네이티브 의존 파일을 실행 파일 옆에 둡니다.

예전에는 전부 bin/ 하나를 공유했는데, 그러면 두 플랫폼을 한 머신에서 빌드했을 때 나중 것이 앞의 것의 의존 파일을 덮어씁니다.

트리밍을 사용하지 않는 이유

생성되는 실행 파일은 self-contained 단일 파일입니다.

PublishTrimmed는 의도적으로 사용하지 않습니다. NPOI, Newtonsoft.Json, Google.Apis가 모두 리플렉션으로 타입을 찾으므로, 트리밍이 런타임에 필요한 멤버를 제거합니다.

배포

dotnet publish src/Tabbit.csproj -c Release -r win-x64 --self-contained true -o out

-rwin-x64, linux-x64, osx-arm64 등으로 바꿉니다.

결과는 실행 파일 하나(약 60MB)와 네이티브 의존 두 개뿐이며, .NET이 설치되지 않은 머신에서 그대로 동작합니다.

프레임워크 의존(--self-contained false)으로 배포한다면 대상 머신에 ASP.NET Core 런타임이 필요합니다.

기본 .NET 런타임만으로는 --serve뿐 아니라 빌드도 시작되지 않습니다. 웹서버가 프레임워크 참조로 들어가 있기 때문입니다.

대상 머신에 무엇이 설치되어 있을지 확신할 수 없다면 self-contained가 안전합니다.

CI가 매 실행마다 linux-x64로 self-contained 퍼블리시를 만들고, 그 결과물로 빌드를 한 번 돌립니다. 위 문장은 주장이 아니라 검증된 사실입니다.

실행

tabbit --recipe recipe.json

recipe와 실행 범위

옵션설명
-r, --recipe사용할 recipe 파일
--new-recipe <파일>시작용 recipe를 만들고 종료합니다
--template <이름>--new-recipe가 사용할 시작점. 시작점 고르기
--target-side <side>실행 전체를 한쪽으로 좁힙니다. client / server / both(기본)
--time-zone <시간대>시트의 datetime 셀을 읽을 시간대를 강제 지정합니다
-e, --env <이름>이 실행이 어느 환경의 것인지. 아래
@<파일>인자를 파일에서 읽습니다. 한 줄에 옵션 하나이며, 인자를 이것 하나만 줄 때 동작합니다

--new-recipe가 만드는 파일에는 모든 목록에 기본값이 채워진 항목이 하나씩 들어 있습니다.

어떤 설정이 있는지 파일만 보고 알 수 있게 하기 위한 것입니다. 필요 없는 항목은 지우면 되고, 경로가 빈 항목은 꺼진 것으로 취급하므로 그냥 두어도 됩니다.

--time-zone은 recipe의 전역 설정과 소스별 설정을 둘 다 덮어씁니다.

recipe가 시간대를 잘못 적고 있는 실행을 위한 것이므로, 덮어쓸 대상이 바로 소스별 설정입니다. 적용한 값은 실행 로그에 한 줄로 남습니다. 시트의 값

검증

옵션설명
--validate-only검증까지만 실행하고 종료합니다. 산출물을 만들지 않으므로 PR 검사가 사용합니다
--skip-runtime-validation외부 저장소를 읽는 rules/runtime/ 규칙만 건너뜁니다. 건너뛴 규칙 수가 기록에 남습니다
--new-validator <테이블>해당 테이블의 시작 검증 규칙을 만들고 종료합니다. 이미 있으면 덮어쓰지 않습니다
--list-validators검증 규칙을 실행 순서대로 출력하고 종료합니다. 시트를 하나도 읽지 않습니다

우선순위는 규칙마다 붙이므로 전체 순서가 한곳에 모이지 않습니다.

--list-validators의 출력이 그 자리입니다. 목록 파일과 달리 출력한 것이 곧 실행되는 것입니다.

자세한 내용은 검증에 있습니다.

기록과 조회

옵션설명
--commit <id>이 빌드가 어느 커밋의 것인지. 생략하면 워킹카피에서 git으로 읽습니다
--branch <name>스냅샷이 속할 브랜치. 생략하면 git에서 읽습니다
--commit-author "Name <email>"작성자를 직접 지정합니다. git 값을 덮어씁니다
--commit-date <ISO8601>변경 시각을 직접 지정합니다. git 값을 덮어씁니다
--repository <경로>커밋 정보를 읽을 워킹카피. 생략하면 시트의 소스 디렉터리, 그다음 현재 디렉터리를 봅니다
--history빌드 대신 변경 내역을 조회하고 종료합니다
--stats빌드 대신 한 커밋의 통계를 조회하고 종료합니다
--serve빌드 대신 HTTP로 히스토리를 서비스하고 계속 실행합니다
--prune빌드 대신 오래된 스냅샷의 변경 상세를 정리하고 종료합니다

자세한 내용은 Summary와 히스토리에 있습니다.

캐시

옵션설명
--full캐시를 보지 않고 전부 빌드합니다. 캐시의 판정 자체를 의심할 때 사용합니다
--force-output입력 판정은 캐시를 그대로 쓰고 출력 항목만 전부 실행합니다
--cache-dir <경로>캐시를 둘 위치. 생략하면 작업 디렉터리의 .tabbit/

다시 하지 않기 — 빌드 캐시에서 무엇이 전체 실행을 만드는지 설명합니다.

출력과 진단

옵션설명
--detailed-exit-code할 일이 없어서 끝난 실행을 종료 코드로 구분합니다. 종료 코드
-v, --verbose디버그 로그까지 출력합니다
-q, --silentERROR 아래로는 출력하지 않습니다
--debug오류 발생 시 콜스택까지 출력합니다
-h, --help도움말을 출력하고 종료합니다. 종료 코드는 0입니다
--version버전, 커밋, 런타임을 출력하고 종료합니다. 아래
--show-report이 recipe의 마지막 빌드 리포트를 열고 종료합니다. 아래
--new-encryption-key바이너리 암호화 키를 새로 만들고 종료합니다

--new-encryption-key는 운영체제의 난수원에서 키를 뽑습니다.

표준 출력에는 키 한 줄만 나가므로(안내문은 표준 오류로) 시크릿 저장소에 그대로 파이프할 수 있고, --out을 주면 파일로 씁니다. 이미 있는 파일은 덮어쓰지 않습니다. 내보내기

실행이 여는 첫 줄

어느 빌드가 무엇으로 돌고 있는지를 먼저 적습니다.

[I] [Tabbit ] Tabbit 1.5.0+8b2a884d84bf
[I] [Tabbit ] .tcb v106 · .NET 10.0.11 (win-x64)

[I] [Loading ] Start working with recipe 전량 변환 recipe

두 줄로 나눠 적고 아래에 빈 줄을 둡니다.

버전과 형식 번호와 런타임과 플랫폼을 한 줄에 담으면 아무것도 찾을 수 없는 줄이 되고, 빈 줄이 없으면 이 줄들이 그 뒤 수백 줄의 앞 두 줄이 됩니다.

넷을 적는 이유는 받은 로그에 대해 묻게 되는 것이 대개 이 넷 중 하나이기 때문입니다. 어느 빌드인지, 어느 런타임인지, 어느 운영체제인지, 그리고 바이너리 형식이 몇 번인지입니다.

마지막 것이 유일하게 읽는 쪽과 어긋날 수 있는 값입니다. 생성된 리더는 각자 형식 상수의 사본을 가지므로, 클라이언트가 파일을 거부하면 그것은 다른 무엇보다 먼저 버전 불일치입니다.

여기 적히는 빌드 문자열은 빌드 캐시의 키에 들어가는 것과 같은 값입니다. 「다른 빌드가 쓴 캐시입니다」라는 보고와 이 줄이 어긋나면 아무도 무엇을 해야 할지 알 수 없으므로, 한 곳에서 생성합니다.

표준 출력이 다른 프로그램의 입력인 실행에는 적지 않습니다.

--new-encryption-key는 키 한 줄만, --out 없는 --history--stats는 JSON만 표준 출력에 내보내므로 그대로 파이프할 수 있어야 합니다. 파일 로그에는 언제나 남습니다.

--version — 손에 든 바이너리가 무엇인가

같은 두 줄에, 릴리스 빌드면 빌드 시각이 한 줄 더 붙습니다.

$ tabbit --version
Tabbit 1.5.0+8b2a884d84bf
.tcb v107 · .NET 10.0.11 (win-x64)
built 2026-08-22T14:41:52Z

로컬 빌드에는 마지막 줄이 없습니다.

$ tabbit --version
Tabbit 0.0.0+c401cb366657
.tcb v107 · .NET 10.0.11 (win-x64)

없다는 것이 답입니다. 버전 0.0.0과 함께, 이 바이너리가 릴리스가 아니라는 뜻입니다.

빌드 시각은 릴리스 워크플로만 넘깁니다. 빌드 중에 시각을 평가하면 그것이 매번 달라지는 컴파일 입력이 되어, 결정론적 빌드가 없어지고 아무것도 고치지 않은 dotnet build가 전부 재컴파일이 됩니다.

+ 뒤는 커밋 해시입니다.

SDK가 넣어 주므로 로컬 빌드에도 붙고, 빌드 중에 git을 호출하지 않습니다. 다만 해시로는 앞뒤를 읽을 수 없어서 커밋 날짜는 아직 넣지 않았습니다. 넣는 방법은 CLI 도움말 스펙 §7.4에 적어 두었습니다.

빌드 시각은 실행 첫 줄에는 넣지 않습니다.

그 줄이 알려야 하는 것은 「어느 빌드가 돌고 있나」이고 버전과 커밋이 이미 그것을 정합니다. 빌드 시각을 원하는 사람은 바이너리를 손에 들고 이것이 무엇인지 확인하려는 사람이고, 그 사람이 치는 명령이 --version입니다.

로그 한 줄의 생김새

콘솔과 logs/tabbit<날짜>.log 모두 [수준] [단계] 메시지 순으로 출력합니다.

콘솔은 지켜보는 동안 읽는 것이라 수준을 한 글자로, 파일은 나중에 읽는 것이라 시각과 함께 세 글자로 적습니다.

[I] [Importing ] Optimize sheet: goods.xlsx : TownMatrix : A1
[W] [Cooking ] Defined name `INGEX` refers to ``, which is not a range. Skipped.
[I] [Exporting ] Generating codes for C++ into `out/cpp`

단계는 그 줄을 쓴 클래스가 스스로 밝힌 것입니다.

단계무엇을 하는 중인가
Loadingrecipe 읽기
Importing소스 읽기
Cooking모델 만들기
Validating검증
Exporting내보내기와 코드 생성
Committing스테이징 파일 반영
Recording기록과 조회

밝히지 않은 줄은 Tabbit으로 나갑니다.

무엇이 어디서 어긋났는지 찾을 때 이 칸을 먼저 봅니다. 이름이 풀리지 않는다는 같은 경고라도 Importing에서 나온 것과 Cooking에서 나온 것은 다른 이야기입니다.

터미널에서는 수준 표시에 색이 붙습니다. 경고는 노랑, 오류와 치명은 빨강입니다. 출력을 파일로 보내거나 파이프로 넘기면 색은 붙지 않으므로, 받아서 검사하는 쪽이 신경 쓸 것이 없습니다.

다시 하지 않기 — 빌드 캐시

변경된 것이 없으면 아무것도 하지 않습니다.

실행마다 워크북을 다시 디코딩하고 테이블을 다시 쿠킹하고 모든 타깃을 다시 내는 것이 기본 동작이었고, 지금은 지난 실행이 무엇을 읽고 무엇을 썼는지 기록해 두었다가 비교합니다.

설계와 근거는 빌드 캐시에 있습니다.

$ tabbit --recipe recipe.jsonc
[I] [Caching ] Nothing to do. 36 input files, 1 source directory and 7 output entries are unchanged.
[I] [Caching ] 9,470 output files are intact. Pass --full to convert anyway.

전체를 다시 하게 되면 그 이유를 함께 출력합니다.

캐시가 왜 동작하지 않았는지 알 수 없으면 --full이 습관이 되고, 그러면 캐시가 없는 것과 같아집니다.

[I] [Caching ] Converting everything: 2 input files changed (교역.xlsx, 경매.xlsb).
[I] [Caching ] Converting everything: the recipe changed, outside its `Validation` and `Targets` sections.
[I] [Caching ] Converting everything: this cache was written by a different build (`1.4.0`, now `1.5.0`).

무엇이 전체 실행을 만드는가

바뀐 것다시 하는 것
워크북의 내용, 또는 디렉터리에 워크북이 추가·삭제된 것전부
recipe의 Sources와 전역 설정전부
recipe의 Validation검증
Targets 항목 하나의 설정그 항목만
그 항목이 쓴 파일이 없어졌거나 달라진 것그 항목만
도구의 빌드 버전전부. 생성기와 템플릿이 실행 파일 안에 있기 때문입니다
--target-side · --time-zone · --env전부
recipe의 주석·공백·설정 순서아무것도
워크북의 수정 시각만 바뀐 것아무것도. 내용을 해시해서 확인합니다
--verbose · --silent · --debug아무것도

수정 시각만 바뀐 것을 걸러내는 것이 실제로 중요합니다.

git checkout은 받은 파일 전부의 수정 시각을 지금으로 바꾸고, 엑셀은 열었다 저장만 해도 그렇게 합니다. 시각만 보는 캐시는 체크아웃 한 번을 넘기지 못합니다.

캐시가 적용되지 않는 것

검증은 캐시를 쓰지 않습니다.

--validate-only는 게이트이고, 「지난번에 통과했다」로 통과시키면 게이트가 아닙니다. 다만 빌드에서 인장이 맞으면 검증도 함께 건너뛰므로, 매 실행 외부 저장소를 확인해야 하는 곳은 --validate-only를 따로 실행합니다.

데이터베이스 타깃과 히스토리는 건너뛰지 않습니다.

파일 타깃의 산출물은 해시해서 그대로인지 확인할 수 있지만 데이터베이스의 상태는 확인할 수 없습니다. 파일을 하나도 쓰지 않는 항목은 확인할 것이 없으므로 매번 실행됩니다.

건너뛴 실행도 낡은 생성 파일은 지웁니다.

시트에서 테이블을 지웠으면 그 파일은 없어져야 하고, 그것이 「그날 다른 것이 바뀌었는지」에 달려 있을 이유가 없습니다.

지우는 조건은 늘 같습니다. 타깃이 스윕을 요청한 디렉터리 아래에 있고, 파일 스스로 Generated by Tabbit이라고 적고 있는 것입니다.

구글 시트는 스코프 하나를 더 사용합니다

구글 문서는 크기도 수정 시각도 없으므로, Drive에서 version을 조회합니다. 서버의 모든 변경에 증가하는 카운터입니다.

drive.metadata.readonly이고 셀은 한 칸도 읽지 않습니다. 조회할 수 없으면 바뀐 것으로 보고 가져오며, 무엇을 허용하면 되는지 함께 출력합니다.

캐시는 커밋 대상이 아닙니다

절대 경로와 그 기계의 파일 상태를 담으므로 .gitignore에 있습니다.

CI에서 재사용하려면 --cache-dir로 마운트된 경로를 지정합니다.

종료 코드

코드무엇
0요청받은 것을 했습니다
1실패했고, 이유를 출력했습니다
2할 일이 없었습니다. --detailed-exit-code를 준 실행에서만

2가 플래그 뒤에 있는 이유는 호환입니다.

명령줄 도구를 호출하는 것들은 대개 0이 아닌 코드를 실패로 취급하므로, 기본으로 0이 아닌 값을 내면 캐시가 처음 동작한 날 뒤에 이어지는 단계가 전부 멈춥니다.

새 결함으로 보이기에 가장 나쁜 날입니다.

다음 단계가 배포인 파이프라인은 이렇게 사용합니다.

tabbit --recipe recipe.jsonc --detailed-exit-code
case $? in
0) ./publish.sh ;;
2) echo "데이터가 그대로입니다. 배포를 건너뜁니다." ;;
*) exit 1 ;;
esac

단계별 소요 시간

실행이 끝나면 각 단계가 얼마나 걸렸는지 출력합니다.

어디를 고쳐야 빨라지는지는 짐작이 아니라 이 숫자로 판단할 일입니다.

[I] [Timing ] Importing 64.20 s 40%
[I] [Timing ] Cooking 17.60 s 11%
[I] [Timing ] Validating 8.90 s 6%
[I] [Timing ] Output 68.70 s 43%
[I] [Timing ] Committing 2.60 s 2%

타깃별 내역은 --verbose에서 함께 나옵니다. 파일 로그에는 항상 남습니다.

빌드 리포트 — 찾은 것을 고칠 사람에게

실행이 끝나면 찾은 것을 HTML 한 장과 JSON 한 장으로 남기고, 경로를 출력합니다.

[I] [Reporting ] Build report: C:/work/game/.tabbit/recipe-f9a4be06a56b.report.html - 3 error(s), 7 warning(s)

멈춘 실행도 리포트를 남깁니다. 오히려 그쪽이 본론입니다.

콘솔은 스크롤이 지나가면 끝이고, 시트를 고칠 수 있는 사람은 대개 그 화면을 보고 있지 않습니다.

무엇어디에
사람이 읽는 것.report.html. 워크북과 시트로 묶여 접히고, 심각도 토글과 검색이 있습니다. 구글 시트의 위치는 그 셀로 가는 링크입니다
CI가 읽는 것.report.json. 항목의 키가 메시지 ID이므로 문구를 다듬어도 필터가 깨지지 않습니다. 페이지와 달리 언제나 전량입니다

경고 이상이면 기본 브라우저로 열립니다.

열지 않는 경우가 셋 있고, 셋 다 「보는 사람이 그 자리에 없다」의 신호입니다. CI 환경 변수가 설정됨, 출력이 터미널이 아님, --silent입니다.

설정은 recipe의 Report.OpenInBrowser이며 never, problems, always 중 하나입니다.

직전 실행 것을 다시 열려면 빌드 없이 이것만으로 됩니다.

tabbit --recipe recipe.jsonc --show-report

리포트는 종료 코드에 관여하지 않습니다. 쓰지 못했으면 경고 한 줄이 나가고 실행의 결과는 그대로입니다.

설계와 근거는 빌드 리포트에 있습니다.

--env — 이 실행이 어느 환경의 것인가

어떤 상황무엇을 하나
혼자, 한 기계에서아무것도 하지 않습니다. recipe에 경로를 그대로 적고 tabbit --recipe recipe.jsonc로 끝입니다
여러 사람, 여러 환경recipe가 경로를 ${TABBIT_ENV}로 적고, 실행이 --env로 어느 환경인지 지정합니다

아래는 두 번째 경우입니다.

한 낱말이 두 가지 일을 합니다.

summary에 기록되어 산출물만 보고 어느 환경의 빌드인지 판정할 수 있게 하고, 동시에 recipe의 ${TABBIT_ENV}가 그 값으로 채워집니다.

// recipe.jsonc
{
"Sources": { "Xlsx": [ { "Path": "./sheets/${TABBIT_ENV}" } ] },
"Targets": [ { "Type": "binary", "Path": "./build/${TABBIT_ENV}/data" } ]
}
tabbit --recipe recipe.jsonc --env live

둘을 갈라 두지 않은 이유가 이 옵션의 요점입니다.

라벨용 플래그와 경로용 환경 변수를 따로 두면 둘이 어긋날 수 있고, 어긋난 결과는 개발 시트로 만들어 놓고 live라고 적힌 산출물입니다.

라벨이 없는 것보다 나쁩니다. 질문을 열어두는 것이 아니라 틀리게 답하기 때문입니다.

  • TABBIT_ENV가 이미 다른 값으로 설정되어 있으면 거부합니다. 어느 쪽을 택하든 한쪽이 라벨을 정하고 다른 쪽이 경로를 정하는 상태가 됩니다.
  • 같은 값이면 통과합니다. 변수를 내보내면서 플래그도 넘기는 CI 잡은 잘못한 것이 없습니다.
  • 변수만 설정하고 --env를 생략해도 됩니다. 결과는 같습니다.
  • 적지 않으면 summary의 environmentnull입니다. 기본값을 적어 넣지 않는 것은 아무도 하지 않은 주장을 기록하지 않기 위해서입니다.
  • 이름은 영문자, 숫자, ., _, -만 받습니다. 이 낱말이 경로에 들어가므로 구분자나 ..가 들어가면 recipe가 적지 않은 곳에 산출물이 쓰입니다. 변수로 넣은 값도 같게 검사합니다.

인자가 많아지면

파일로 빼서 @로 넘길 수 있습니다. 한 줄에 인자 하나씩 적습니다.

tabbit @args.txt

성공하면 0, 실패하면 0이 아닌 값을 반환하므로 빌드 파이프라인에서 그대로 사용할 수 있습니다.