ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Go 개발할 때 필요한 Makefile 작성해보기
    Go 2024. 3. 25. 22:41

     

    안녕하세요?

     

    Go를 사용하여 제품을 만들 때, Makefile을 사용하여 빌드를 지정하는 경우가 많습니다.

     

    이유는 다음과 같습니다.

    • 버전 정보 등을 삽입하기에 편리합니다.
    • 여러 바이너리를 출력할 때 편리합니다.
    • Go의 빌드 옵션을 지정하는 데 여러 가지가 있어 정리해두고 싶습니다.
    • 코드 생성기로 미리 작성해야 하는 부분이 있고, 그것을 고려하면 Makefile 등으로 정리하고 싶습니다.

    그래서 이번에는 프로젝트가 커져가는 중에 어떤 Makefile 작성 방법을 사용하고 있는지 소개하고자 합니다.

     

    ** 목 차 **


    Step 1. 버전 정보를 삽입하기

    이번에는 서버와 클라이언트로 두 개의 바이너리를 출력하므로, 저장소의 구조는 다음과 같습니다.

    - ./          # 저장소 루트. package gochat
    - bin/        # 바이너리를 출력하는 곳. gitignore 합니다.
    - cmd/        # 바이너리의 패키지
      - server/   # 서버 바이너리. package main
        - main.go # 엔트리 포인트
      - client/   # 클라이언트 바이너리. package main
        - main.go # 엔트리 포인트
    - proto/      # .proto 파일 저장소
    - .gitignore  # gitignore 파일
    - go.mod      # go.mod
    - go.sum      # go.sum
    - Makefile    # 핵심적인 것
    - gochat.go   # 버전 정보 등을 삽입할 대상

     

    gochat.go의 내용은 다음과 같습니다.

    gochat.go

    package gochat
    
    var (
        VERSION  = "0.0.0"
        REVISION = ""
    )

    버전 정보를 어느 파일에 작성할지에 대한 논의

    gochat.go에 VERSION이 있으므로, 바로 거기에 작성하면 되지 않을까 생각할 수도 있지만, 이번에는 VERSION이라는 파일을 저장소 루트에 따로 만들어 관리하는 방법을 사용합니다.

     

    단순히 그 방법이 관리하기 쉽기 때문입니다.

    첫 번째 Makefile

    build로 바이너리를 출력하고 싶으므로, make build로 목표하는 server와 client의 바이너리가 bin 아래에 출력되도록 합니다.

     

    패키지 이름이 변경되어도 사용할 수 있도록, Go의 기능을 활용하여 대상 파일을 선별하는 것을 잊지 마세요.

     

    Makefile

    # 출력 대상 디렉토리
    BINDIR:=bin
    
    # 루트 패키지 이름 가져오기
    ROOT_PACKAGE:=$(shell go list .)
    # 커맨드로 작성될 패키지 이름 가져오기
    COMMAND_PACKAGES:=$(shell go list ./cmd/...)
    
    # 출력 대상 바이너리 파일 이름(bin/server 등)
    BINARIES:=$(COMMAND_PACKAGES:$(ROOT_PACKAGE)/cmd/%=$(BINDIR)/%)
    
    # 빌드 시 확인할 .go 파일
    GO_FILES:=$(shell find . -type f -name '*.go' -print)
    
    # 빌드 작업
    .PHONY: build
    build: $(BINARIES)
    
    # 실제 빌드 작업
    $(BINARIES): $(GO_FILES)
        @go build -o $@ $(@:$(BINDIR)/%=$(ROOT_PACKAGE)/cmd/%)

     

    여기에 VERSION과 git 리비전을 삽입할 수 있도록 합니다.

     

    Makefile

    # 버전
    VERSION:=$(shell cat VERSION)
    # 리비전
    REVISION:=$(shell git rev-parse --short HEAD)

     

    버전이나 리비전이 변경되면 다시 빌드되기를 원하므로, 그 부분도 의존 관계에 추가해둡니다.

     

    Makefile

    $(BINARIES): $(GO_FILES) VERSION .git/HEAD
        @go build -o $@ $(@:$(BINDIR)/%=$(ROOT_PACKAGE)/cmd/%)

     

    핵심적인 ldflags를 삽입하는 부분을 작성합니다.

     

    Makefile

    # ldflag
    GO_LDFLAGS_VERSION:=-X '${ROOT_PACKAGE}.VERSION=${VERSION}' -X '${ROOT_PACKAGE}.REVISION=${REVISION}'
    GO_LDFLAGS:=$(GO_LDFLAGS_VERSION)
    
    # go build
    GO_BUILD:=-ldflags "$(GO_LDFLAGS)"
    
    $(BINARIES): $(GO_FILES) VERSION .git/HEAD
        @go build -o $@ $(GO_BUILD) $(@:$(BINDIR)/%=$(ROOT_PACKAGE)/cmd/%)

     

    이렇게 하면 어떤 커밋을 한 후(git rev-parse를 하는 관계로, 최소한 1개의 커밋이 필요) 빌드하면, 빌드 내에 버전 정보와 리비전이 들어간 것이 출력됩니다.

    $ make build
    $ ./bin/server
    v0.0.1-15cc88f

    Step 2. -race나 -installsuffix 등 옵션을 환경 변수로 지정하기

    디버그 빌드나 릴리스 빌드에서 옵션을 전환하고 싶을 때가 있으므로, 그러한 옵션을 지정하도록 합니다.

     

    이번에는 RELEASE 환경 변수가 켜져 있으면 릴리스 빌드, 그렇지 않으면 디버그 빌드라고 하고, make build RELEASE=1과 같이 하여 빌드를 전환합니다.

     

    Makefile

    # 버전 ldflag
    GO_LDFLAGS_VERSION:=-X '${ROOT_PACKAGE}.VERSION=${VERSION}' -X '${ROOT_PACKAGE}.REVISION=${REVISION}'
    # 심볼 테이블과 dwarf
    GO_LDFLAGS_SYMBOL:=
    ifdef RELEASE
        GO_LDFLAGS_SYMBOL:=-w -s
    endif
    # 정적 ldflag
    GO_LDFLAGS_STATIC:=
    ifdef RELEASE
        GO_LDFLAGS_STATIC:=-extldflags '-static'
    endif
    # 빌드 ldflags
    GO_LDFLAGS:=$(GO_LDFLAGS_VERSION) $(GO_LDFLAGS_SYMBOL) $(GO_LDFLAGS_STATIC)
    # 빌드 태그
    GO_BUILD_TAGS:=debug
    ifdef RELEASE
        GO_BUILD_TAGS:=release
    endif
    # 레이스 탐지기
    GO_BUILD_RACE:=-race
    ifdef RELEASE
        GO_BUILD_RACE:=
    endif
    # 정적 빌드 플래그
    GO_BUILD_STATIC:=
    ifdef RELEASE
        GO_BUILD_STATIC:=-a -installsuffix netgo
        GO_BUILD_TAGS:=$(GO_BUILD_TAGS),netgo
    endif
    # go build
    GO_BUILD:=-tags=$(GO_BUILD_TAGS) $(GO_BUILD_RACE) $(GO_BUILD_STATIC) -ldflags "$(GO_LDFLAGS)"

     

    사용할지는 모르겠지만 빌드 태그로 release나 debug를 붙여서, Go의 소스 코드 측에서도 행동을 제어할 수 있도록 합니다.

     

    그리고 단순히 make build RELEASE=1을 하거나 하지 않는 선택을 할 수 있습니다.


    Step 3. 외부 도구에 대한 의존성

    Ruby에서는 bundle exec를 사용하면 편리하지만, Go의 도구들은 보통 go install로 전역에 설치됩니다.

     

    저장소마다 의존하는 버전이 조금씩 다른 경우(sqlboiler 등이 좋은 예) 환경마다 조금씩 다른 결과가 나와서 매번 차이가 발생하고 까다로운 경우도 있습니다.

    이번에는 protobuf는 전역적으로 사용하지만, protoc-gen-go는 저장소 로컬로 관리하기로 했습니다.

     

    그래서 먼저 저장소에 다음과 같은 내용으로 파일을 만듭니다.

     

    tools/tools.go

    // +build tools
    package gochat
    
    import (
      _ "google.golang.org/protobuf/cmd/protoc-gen-go"
    )

     

    이렇게 하면 google.golang.org/protobuf가 go.mod로 관리되므로, 이후에는 그것을 대상으로 하면 됩니다.

     

    그 다음은 protobuf를 수행하고, go build로 bin 아래에 protoc-gen-go를 배치하는 부분을 작성합니다.

     

    Makefile

    # gRPC 파일
    PB_FILES:=$(shell find . -type f -name '*.proto' -print)
    # proto에서 생성되는 .go 파일
    GOPB_FILES:=$(PB_FILES:%.proto=%.pb.go)
    
    # 실제 빌드 작업
    $(BINARIES): $(GO_FILES) $(GOPB_FILES) VERSION .git/HEAD
    
    # protoc 빌드
    $(GOPB_FILES): $(PB_FILES) $(BINDIR)/protoc-gen-go
        @protoc \
            --plugin=protoc-gen-go=$(BINDIR)/protoc-gen-go \
            -I ./proto \
            --go_out=./proto \
            --go_opt=paths=source_relative \
            $(@:%.pb.go=%.proto)
    
    $(BINDIR)/protoc-gen-go: go.sum
        @go build -o $@ google.golang.org/protobuf/cmd/protoc-gen-go

     

    이번 서비스에만 집중하기 때문에, .proto에서 생성되는 .go 파일은 같은 디렉토리에 넣고, 또한 ignore 처리합니다.

     

    .gitignore

    /proto/*.go

    작업을 진행하면서 지속적으로 확인

    make로 캐시가 작동하지 않는 경우나, 특정 환경에서 빌드가 제대로 되지 않는 등의 경우에 대비하여, 지속적으로 시도하는 것이 중요합니다. 특히 아래와 같은 옵션을 활용하는 것이 좋습니다.

     

    -B : 대상 파일이 소스 파일보다 새로워도 작업을 강제로 실행
    -n : 명령을 실행하지 않고, 실행할 예정이었던 명령을 표시


    Step 4. clean을 올바르게 구현하기

    make clean이 없어도 git으로 관리되지 않는 파일을 삭제할 수 있지만, 있으면 편리하므로 만들어두는 것이 좋습니다.

     

    Makefile

    # 청소
    .PHONY: clean
    clean:
        @$(RM) $(GOPB_FILES) $(BINARIES) $(BINDIR)/protoc-gen-go

    Step 5. docker build 등을 위해 .git이 없는 상황에 대비하기

    .git은 .dockerignore로 전송하지 않지만, REVISION이라는 파일로 기록해두는 경우를 위해 대비책을 작성합니다.

     

    Makefile

    # 리비전
    REVISION:=$(shell git rev-parse --short HEAD 2> /dev/null || cat REVISION)

    완성본

    "가능한 한 복잡하지 않게"라는 것을 목표로 하면 다음과 같은 Makefile이 됩니다.

     

    Makefile

    # 버전
    VERSION:=$(shell cat VERSION)
    # 리비전
    REVISION:=$(shell git rev-parse --short HEAD 2> /dev/null || cat REVISION)
    
    # 출력 대상 디렉토리
    BINDIR:=bin
    
    # 루트 패키지 이름 가져오기
    ROOT_PACKAGE:=$(shell go list .)
    # 커맨드로 작성될 패키지 이름 가져오기
    COMMAND_PACKAGES:=$(shell go list ./cmd/...)
    
    # 출력 대상 바이너리 파일 이름(bin/server 등)
    BINARIES:=$(COMMAND_PACKAGES:$(ROOT_PACKAGE)/cmd/%=$(BINDIR)/%)
    
    # 빌드 시 확인할 .go 파일
    GO_FILES:=$(shell find . -type f -name '*.go' -print)
    
    # gRPC 파일
    PB_FILES:=$(shell find . -type f -name '*.proto' -print)
    # proto에서 생성되는 .go 파일
    GOPB_FILES:=$(PB_FILES:%.proto=%.pb.go)
    
    # 버전 ldflag
    GO_LDFLAGS_VERSION:=-X '${ROOT_PACKAGE}.VERSION=${VERSION}' -X '${ROOT_PACKAGE}.REVISION=${REVISION}'
    # 심볼 테이블과 dwarf
    GO_LDFLAGS_SYMBOL:=
    ifdef RELEASE
        GO_LDFLAGS_SYMBOL:=-w -s
    endif
    # 정적 ldflag
    GO_LDFLAGS_STATIC:=
    ifdef RELEASE
        GO_LDFLAGS_STATIC:=-extldflags '-static'
    endif
    # 빌드 ldflags
    GO_LDFLAGS:=$(GO_LDFLAGS_VERSION) $(GO_LDFLAGS_SYMBOL) $(GO_LDFLAGS_STATIC)
    # 빌드 태그
    GO_BUILD_TAGS:=debug
    ifdef RELEASE
        GO_BUILD_TAGS:=release
    endif
    # 레이스 탐지기
    GO_BUILD_RACE:=-race
    ifdef RELEASE
        GO_BUILD_RACE:=
    endif
    # 정적 빌드 플래그
    GO_BUILD_STATIC:=
    ifdef RELEASE
        GO_BUILD_STATIC:=-a -installsuffix netgo
        GO_BUILD_TAGS:=$(GO_BUILD_TAGS),netgo
    endif
    # go build
    GO_BUILD:=-tags=$(GO_BUILD_TAGS) $(GO_BUILD_RACE) $(GO_BUILD_STATIC) -ldflags "$(GO_LDFLAGS)"
    
    # 빌드 작업
    .PHONY: build
    build: $(BINARIES)
    
    # 청소
    .PHONY: clean
    clean:
        @$(RM) $(GOPB_FILES) $(BINARIES) $(BINDIR)/protoc-gen-go
    
    # 실제 빌드 작업
    $(BINARIES): $(GO_FILES) $(GOPB_FILES) VERSION .git/HEAD
        @go build -o $@ $(GO_BUILD) $(@:$(BINDIR)/%=$(ROOT_PACKAGE)/cmd/%)
    
    # protoc 빌드
    $(GOPB_FILES): $(PB_FILES) $(BINDIR)/protoc-gen-go
        @protoc \
            --plugin=protoc-gen-go=$(BINDIR)/protoc-gen-go \
            -I ./proto \
            --go_out=./proto \
            --go_opt=paths=source_relative \
            $(@:%.pb.go=%.proto)
    
    $(BINDIR)/protoc-gen-go: go.sum
        @go build -o $@ google.golang.org/protobuf/cmd/protoc-gen-go

     

    이 정도면 충분히 길지만 읽을 수 있는 범위입니다.

     

    여기서 더 make help를 추가하거나 여러 가지로 개조할 수 있지만, 일단 이 정도로 만들어 두면 어떤 프로젝트에서든 사용할 수 있어 편리합니다.

     

    끝.

Designed by Tistory.