CI revamp part 2 (#1875)
* Added Makefile * Updated README with the new information * Updated Github Actions * Added FreeBSD ARM builds * Prepared to our internal CI * Improved the auto-update version check
This commit is contained in:
parent
ff23d7b6d7
commit
632a47d56f
|
@ -1,9 +1,13 @@
|
|||
name: build
|
||||
|
||||
env:
|
||||
GO_VERSION: 1.14
|
||||
NODE_VERSION: 13
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- '*'
|
||||
tags:
|
||||
- v*
|
||||
pull_request:
|
||||
|
@ -26,16 +30,19 @@ jobs:
|
|||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
|
||||
-
|
||||
name: Set up Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 13
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
-
|
||||
name: Set up Go modules cache
|
||||
uses: actions/cache@v2
|
||||
|
@ -58,33 +65,14 @@ jobs:
|
|||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
-
|
||||
name: Run node build-prod
|
||||
name: Run make ci
|
||||
shell: bash
|
||||
run: |
|
||||
npm --prefix client ci
|
||||
npm --prefix client run build-prod
|
||||
-
|
||||
name: Download modules and generate
|
||||
shell: bash
|
||||
run: |
|
||||
go mod download
|
||||
go generate ./...
|
||||
-
|
||||
name: Golangci-lint
|
||||
if: matrix.os != 'windows-latest'
|
||||
shell: bash
|
||||
run: |
|
||||
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s -- -b $(go env GOPATH)/bin v1.23.8
|
||||
golangci-lint --version
|
||||
golangci-lint run
|
||||
-
|
||||
name: Go test
|
||||
run: |
|
||||
go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
make ci
|
||||
-
|
||||
name: Upload coverage
|
||||
uses: codecov/codecov-action@v1
|
||||
if: success()
|
||||
if: success() && matrix.os == 'ubuntu-latest'
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ./coverage.txt
|
||||
|
@ -96,26 +84,18 @@ jobs:
|
|||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
echo ::set-output name=tag_name::${GITHUB_REF#refs/tags/}
|
||||
echo ::set-output name=channel::release
|
||||
else
|
||||
echo ::set-output name=channel::none
|
||||
fi
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: ${{ env.GO_VERSION }}
|
||||
-
|
||||
name: Set up Node
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 13
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
-
|
||||
name: Set up Go modules cache
|
||||
uses: actions/cache@v2
|
||||
|
@ -142,98 +122,30 @@ jobs:
|
|||
run: |
|
||||
sudo apt-get -yq --no-install-suggests --no-install-recommends install snapcraft
|
||||
-
|
||||
name: Run node build-prod
|
||||
name: Set up GoReleaser
|
||||
run: |
|
||||
npm --prefix client ci
|
||||
npm --prefix client run build-prod
|
||||
curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | BINDIR="$(go env GOPATH)/bin" sh
|
||||
-
|
||||
name: Snapcraft Login
|
||||
if: success() && startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
SNAPCRAFT_LOGIN: ${{ secrets.SNAPCRAFT_LOGIN }}
|
||||
name: Run snapshot build
|
||||
run: |
|
||||
snapcraft login --with <(echo "$SNAPCRAFT_LOGIN")
|
||||
-
|
||||
name: GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: latest
|
||||
args: release --parallelism 2
|
||||
env:
|
||||
CHANNEL: ${{ steps.prepare.outputs.channel }}
|
||||
-
|
||||
name: GitHub Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: success() && startsWith(github.ref, 'refs/tags/v')
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
dist/checksums.txt
|
||||
dist/*.tar.gz
|
||||
dist/*.zip
|
||||
name: AdGuard Home ${{ steps.prepare.outputs.tag_name }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
make snapshot
|
||||
|
||||
docker:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
steps:
|
||||
-
|
||||
name: Prepare
|
||||
id: prepare
|
||||
run: |
|
||||
DOCKER_IMAGE=adguard/adguardhome
|
||||
DOCKER_PLATFORMS=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/ppc64le
|
||||
VERSION=edge
|
||||
CHANNEL=none
|
||||
|
||||
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
||||
VERSION=${GITHUB_REF#refs/tags/v}
|
||||
CHANNEL=release
|
||||
fi
|
||||
TAGS="--tag ${DOCKER_IMAGE}:${VERSION}"
|
||||
if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
|
||||
TAGS="$TAGS --tag ${DOCKER_IMAGE}:latest"
|
||||
fi
|
||||
|
||||
echo ::set-output name=docker_image::${DOCKER_IMAGE}
|
||||
echo ::set-output name=version::${VERSION}
|
||||
echo ::set-output name=channel::${CHANNEL}
|
||||
echo ::set-output name=buildx_args::--platform ${DOCKER_PLATFORMS} \
|
||||
--build-arg VERSION=${VERSION} \
|
||||
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
|
||||
--build-arg VCS_REF=${GITHUB_SHA::8} \
|
||||
${TAGS} \
|
||||
--file Dockerfile .
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: crazy-max/ghaction-docker-buildx@v1
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Docker Buildx (build)
|
||||
run: |
|
||||
docker buildx build --output "type=image,push=false" ${{ steps.prepare.outputs.buildx_args }}
|
||||
-
|
||||
name: Docker Login
|
||||
if: success() && startsWith(github.ref, 'refs/tags/v')
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
run: |
|
||||
echo "${DOCKER_PASSWORD}" | docker login --username "${DOCKER_USERNAME}" --password-stdin
|
||||
-
|
||||
name: Docker Buildx (push)
|
||||
if: success() && startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
docker buildx build --output "type=image,push=true" ${{ steps.prepare.outputs.buildx_args }}
|
||||
-
|
||||
name: Docker Check Manifest
|
||||
if: always() && startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
docker run --rm mplatform/mquery ${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.version }}
|
||||
make docker-multi-arch
|
||||
-
|
||||
name: Clear
|
||||
if: always() && startsWith(github.ref, 'refs/tags/v')
|
||||
|
@ -242,7 +154,8 @@ jobs:
|
|||
|
||||
notify:
|
||||
needs: [app, docker]
|
||||
if: always()
|
||||
# Secrets are not passed to workflows that are triggered by a pull request from a fork
|
||||
if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
name: gosum
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
paths:
|
||||
- '.github/workflows/gosum.yml'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
|
||||
jobs:
|
||||
fix:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
-
|
||||
name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.14
|
||||
-
|
||||
name: Tidy
|
||||
run: |
|
||||
rm -f go.sum
|
||||
go mod tidy
|
||||
-
|
||||
name: Set up Git
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
git config user.name GitHub
|
||||
git config user.email noreply@github.com
|
||||
git remote set-url origin https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git
|
||||
-
|
||||
name: Commit and push changes
|
||||
run: |
|
||||
git add .
|
||||
if output=$(git status --porcelain) && [ ! -z "$output" ]; then
|
||||
git commit --author "github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>" --message "Fix go modules"
|
||||
git push
|
||||
fi
|
|
@ -0,0 +1,47 @@
|
|||
name: golangci-lint
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- '*'
|
||||
pull_request:
|
||||
jobs:
|
||||
golangci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v1
|
||||
with:
|
||||
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
||||
version: v1.27
|
||||
|
||||
eslint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Install modules
|
||||
run: npm --prefix client ci
|
||||
- name: Run ESLint
|
||||
run: npm --prefix client run lint
|
||||
|
||||
|
||||
notify:
|
||||
needs: [golangci,eslint]
|
||||
# Secrets are not passed to workflows that are triggered by a pull request from a fork
|
||||
if: ${{ github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Conclusion
|
||||
uses: technote-space/workflow-conclusion-action@v1
|
||||
-
|
||||
name: Send Slack notif
|
||||
uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ env.WORKFLOW_CONCLUSION }}
|
||||
fields: repo,message,commit,author
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"Vendor": true,
|
||||
"Test": true,
|
||||
"Deadline": "2m",
|
||||
"Sort": ["linter", "severity", "path", "line"],
|
||||
"Exclude": [
|
||||
".*generated.*",
|
||||
"dnsfilter/rule_to_regexp.go"
|
||||
],
|
||||
"EnableGC": true,
|
||||
"Linters": {
|
||||
"nakedret": {
|
||||
"Command": "nakedret",
|
||||
"Pattern": "^(?P<path>.*?\\.go):(?P<line>\\d+)\\s*(?P<message>.*)$"
|
||||
}
|
||||
},
|
||||
"WarnUnmatchedDirective": true,
|
||||
|
||||
"EnableAll": true,
|
||||
"DisableAll": false,
|
||||
"Disable": [
|
||||
"maligned",
|
||||
"goconst",
|
||||
"vetshadow"
|
||||
],
|
||||
|
||||
"Cyclo": 20,
|
||||
"LineLength": 200
|
||||
}
|
|
@ -24,23 +24,37 @@ builds:
|
|||
- amd64
|
||||
- arm
|
||||
- arm64
|
||||
- mips
|
||||
- mipsle
|
||||
- mips64
|
||||
- mips64le
|
||||
goarm:
|
||||
- 5
|
||||
- 6
|
||||
- 7
|
||||
gomips:
|
||||
- softfloat
|
||||
ignore:
|
||||
- goos: freebsd
|
||||
goarch: arm
|
||||
goarch: mips
|
||||
- goos: freebsd
|
||||
goarch: arm64
|
||||
goarch: mipsle
|
||||
|
||||
archives:
|
||||
-
|
||||
replacements:
|
||||
386: i386
|
||||
amd64: x86_64
|
||||
# Archive name template.
|
||||
# Defaults:
|
||||
# - if format is `tar.gz`, `tar.xz`, `gz` or `zip`:
|
||||
# - `{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}`
|
||||
# - if format is `binary`:
|
||||
# - `{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}`
|
||||
name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}"
|
||||
wrap_in_directory: "AdGuardHome"
|
||||
format_overrides:
|
||||
- goos: windows
|
||||
format: zip
|
||||
- goos: darwin
|
||||
format: zip
|
||||
files:
|
||||
- LICENSE.txt
|
||||
- README.md
|
||||
|
@ -62,7 +76,7 @@ snapcrafts:
|
|||
of common code.
|
||||
grade: stable
|
||||
confinement: strict
|
||||
publish: true
|
||||
publish: false
|
||||
license: GPL-3.0
|
||||
apps:
|
||||
adguard-home:
|
||||
|
|
|
@ -4,7 +4,7 @@ FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.14-alpine as builder
|
|||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
ARG VERSION=dev
|
||||
ARG CHANNEL=none
|
||||
ARG CHANNEL=release
|
||||
|
||||
ENV CGO_ENABLED 0
|
||||
ENV GO111MODULE on
|
||||
|
@ -21,12 +21,17 @@ RUN apk --update --no-cache add \
|
|||
&& rm -rf /tmp/* /var/cache/apk/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . ./
|
||||
|
||||
# Prepare the client code
|
||||
RUN npm --prefix client ci && npm --prefix client run build-prod
|
||||
|
||||
# Download go dependencies
|
||||
RUN go mod download
|
||||
RUN go generate ./...
|
||||
|
||||
# It's important to place TARGET* arguments here to avoid running npm and go mod download for every platform
|
||||
ARG TARGETPLATFORM
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
|
|
|
@ -0,0 +1,222 @@
|
|||
#
|
||||
# Available targets
|
||||
#
|
||||
# * build -- builds AdGuardHome for the current platform
|
||||
# * client -- builds client-side code of AdGuard Home
|
||||
# * client-watch -- builds client-side code of AdGuard Home and watches for changes there
|
||||
# * docker -- builds a docker image for the current platform
|
||||
# * clean -- clean everything created by previous builds
|
||||
# * lint -- run all linters
|
||||
# * test -- run all unit-tests
|
||||
# * dependencies -- installs dependencies (go and npm modules)
|
||||
# * ci -- installs dependencies, runs linters and tests, intended to be used by CI/CD
|
||||
#
|
||||
# Building releases:
|
||||
#
|
||||
# * release -- builds release version of AdGuard Home. CHANNEL must be specified (release or beta).
|
||||
# * snapshot -- builds snapshot version of AdGuard Home. Use with CHANNEL=edge.
|
||||
# * docker-multi-arch -- builds a multi-arch image. If you want it to be pushed to docker hub,
|
||||
# you must specify:
|
||||
# * DOCKER_IMAGE_NAME - adguard/adguard-home
|
||||
# * DOCKER_OUTPUT - type=image,name=adguard/adguard-home,push=true
|
||||
|
||||
GOPATH := $(shell go env GOPATH)
|
||||
PWD := $(shell pwd)
|
||||
TARGET=AdGuardHome
|
||||
BASE_URL="https://static.adguard.com/adguardhome/$(CHANNEL)"
|
||||
|
||||
# See release and snapshot targets
|
||||
DIST_DIR=dist
|
||||
|
||||
# Update channel. Can be release, beta or edge. Uses edge by default.
|
||||
CHANNEL ?= edge
|
||||
|
||||
# Validate channel
|
||||
ifneq ($(CHANNEL),relese)
|
||||
ifneq ($(CHANNEL),beta)
|
||||
ifneq ($(CHANNEL),edge)
|
||||
$(error CHANNEL value is not valid. Valid values are release,beta or edge)
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
# Version properties
|
||||
COMMIT=$(shell git rev-parse --short HEAD)
|
||||
TAG_NAME=$(shell git describe --abbrev=0)
|
||||
|
||||
# Remove leading "v" from the tag name
|
||||
RELEASE_VERSION=$(TAG_NAME:v%=%)
|
||||
SNAPSHOT_VERSION=$(RELEASE_VERSION)-SNAPSHOT-$(COMMIT)
|
||||
|
||||
# Set proper version
|
||||
VERSION=
|
||||
ifeq ($(TAG_NAME),$(shell git describe --abbrev=4))
|
||||
VERSION=$(RELEASE_VERSION)
|
||||
else
|
||||
VERSION=$(SNAPSHOT_VERSION)
|
||||
endif
|
||||
|
||||
# Docker target parameters
|
||||
DOCKER_IMAGE_NAME ?= adguardhome-dev
|
||||
DOCKER_PLATFORMS=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/ppc64le
|
||||
DOCKER_OUTPUT ?= type=image,name=$(DOCKER_IMAGE_NAME),push=false
|
||||
BUILD_DATE=$(shell date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||
|
||||
# Docker tags (can be redefined)
|
||||
DOCKER_TAGS ?=
|
||||
ifndef DOCKER_TAGS
|
||||
ifeq ($(CHANNEL),release)
|
||||
DOCKER_TAGS := $(DOCKER_TAGS) --tag $(DOCKER_IMAGE_NAME):latest
|
||||
endif
|
||||
ifeq ($(CHANNEL),beta)
|
||||
DOCKER_TAGS := $(DOCKER_TAGS) --tag $(DOCKER_IMAGE_NAME):beta
|
||||
endif
|
||||
ifeq ($(CHANNEL),edge)
|
||||
DOCKER_TAGS := $(DOCKER_TAGS) --tag $(DOCKER_IMAGE_NAME):edge
|
||||
endif
|
||||
endif
|
||||
|
||||
# Validate docker build arguments
|
||||
ifndef DOCKER_IMAGE_NAME
|
||||
$(error DOCKER_IMAGE_NAME value is not set)
|
||||
endif
|
||||
|
||||
.PHONY: all build client client-watch docker lint test dependencies clean release snapshot docker-multi-arch
|
||||
all: build
|
||||
|
||||
build: dependencies client
|
||||
go generate ./...
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w -X main.version=$(VERSION) -X main.channel=$(CHANNEL) -X main.goarm=$(GOARM)"
|
||||
PATH=$(GOPATH)/bin:$(PATH) packr clean
|
||||
|
||||
client:
|
||||
npm --prefix client run build-prod
|
||||
|
||||
client-watch:
|
||||
npm --prefix client run watch
|
||||
|
||||
docker:
|
||||
DOCKER_CLI_EXPERIMENTAL=enabled \
|
||||
docker buildx build \
|
||||
--build-arg VERSION=$(VERSION) \
|
||||
--build-arg CHANNEL=$(CHANNEL) \
|
||||
--build-arg VCS_REF=$(COMMIT) \
|
||||
--build-arg BUILD_DATE=$(BUILD_DATE) \
|
||||
$(DOCKER_TAGS) \
|
||||
--load \
|
||||
-t "$(DOCKER_IMAGE_NAME)" -f ./Dockerfile .
|
||||
|
||||
@echo Now you can run the docker image:
|
||||
@echo docker run --name "adguard-home" -p 53:53/tcp -p 53:53/udp -p 80:80/tcp -p 443:443/tcp -p 853:853/tcp -p 3000:3000/tcp $(DOCKER_IMAGE_NAME)
|
||||
|
||||
lint:
|
||||
@echo Running linters
|
||||
golangci-lint run ./...
|
||||
npm --prefix client run lint
|
||||
|
||||
test:
|
||||
@echo Running unit-tests
|
||||
go test -race -v -bench=. -coverprofile=coverage.txt -covermode=atomic ./...
|
||||
|
||||
ci: dependencies client test
|
||||
|
||||
dependencies:
|
||||
npm --prefix client ci
|
||||
go mod download
|
||||
|
||||
clean:
|
||||
# make build output
|
||||
rm -f AdGuardHome
|
||||
rm -f AdGuardHome.exe
|
||||
# static build output
|
||||
rm -rf build
|
||||
# dist folder
|
||||
rm -rf $(DIST_DIR)
|
||||
# client deps
|
||||
rm -rf client/node_modules
|
||||
# packr-generated files
|
||||
PATH=$(GOPATH)/bin:$(PATH) packr clean
|
||||
|
||||
docker-multi-arch:
|
||||
DOCKER_CLI_EXPERIMENTAL=enabled \
|
||||
docker buildx build \
|
||||
--platform $(DOCKER_PLATFORMS) \
|
||||
--build-arg VERSION=$(VERSION) \
|
||||
--build-arg CHANNEL=$(CHANNEL) \
|
||||
--build-arg VCS_REF=$(COMMIT) \
|
||||
--build-arg BUILD_DATE=$(BUILD_DATE) \
|
||||
$(DOCKER_TAGS) \
|
||||
--output "$(DOCKER_OUTPUT)" \
|
||||
-t "$(DOCKER_IMAGE_NAME):$(VERSION)" -f ./Dockerfile .
|
||||
|
||||
@echo If the image was pushed to the registry, you can now run it:
|
||||
@echo docker run --name "adguard-home" -p 53:53/tcp -p 53:53/udp -p 80:80/tcp -p 443:443/tcp -p 853:853/tcp -p 3000:3000/tcp $(DOCKER_IMAGE_NAME)
|
||||
|
||||
snapshot: dependencies client
|
||||
@echo Starting snapshot build: version $(VERSION), channel $(CHANNEL)
|
||||
CHANNEL=$(CHANNEL) goreleaser release --rm-dist --skip-publish --snapshot
|
||||
$(call write_version_file,$(VERSION))
|
||||
PATH=$(GOPATH)/bin:$(PATH) packr clean
|
||||
|
||||
release: dependencies client
|
||||
@echo Starting release build: version $(VERSION), channel $(CHANNEL)
|
||||
CHANNEL=$(CHANNEL) goreleaser release --rm-dist --skip-publish
|
||||
$(call write_version_file,$(VERSION))
|
||||
PATH=$(GOPATH)/bin:$(PATH) packr clean
|
||||
|
||||
define write_version_file
|
||||
$(eval version := $(1))
|
||||
|
||||
@echo Writing version file: $(version)
|
||||
|
||||
# Variables for CI
|
||||
rm -f $(DIST_DIR)/version.txt
|
||||
echo "version=v$(version)" > $(DIST_DIR)/version.txt
|
||||
|
||||
# Prepare the version.json file
|
||||
rm -f $(DIST_DIR)/version.json
|
||||
echo "{" >> $(DIST_DIR)/version.json
|
||||
echo " \"version\": \"v$(version)\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"announcement\": \"AdGuard Home $(version) is now available!\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"announcement_url\": \"https://github.com/AdguardTeam/AdGuardHome/releases\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"selfupdate_min_version\": \"v0.0\"," >> $(DIST_DIR)/version.json
|
||||
|
||||
# Windows builds
|
||||
echo " \"download_windows_amd64\": \"$(BASE_URL)/AdGuardHome_windows_amd64.zip\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_windows_386\": \"$(BASE_URL)/AdGuardHome_windows_386.zip\"," >> $(DIST_DIR)/version.json
|
||||
|
||||
# MacOS builds
|
||||
echo " \"download_darwin_386\": \"$(BASE_URL)/AdGuardHome_darwin_386.zip\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_darwin_amd64\": \"$(BASE_URL)/AdGuardHome_darwin_amd64.zip\"," >> $(DIST_DIR)/version.json
|
||||
|
||||
# Linux
|
||||
echo " \"download_linux_amd64\": \"$(BASE_URL)/AdGuardHome_linux_amd64.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_386\": \"$(BASE_URL)/AdGuardHome_linux_386.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
|
||||
# Linux, all kinds of ARM
|
||||
echo " \"download_linux_arm\": \"$(BASE_URL)/AdGuardHome_linux_armv6.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_armv5\": \"$(BASE_URL)/AdGuardHome_linux_armv5.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_armv6\": \"$(BASE_URL)/AdGuardHome_linux_armv6.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_armv7\": \"$(BASE_URL)/AdGuardHome_linux_armv7.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_arm64\": \"$(BASE_URL)/AdGuardHome_linux_arm64.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
|
||||
# Linux, MIPS
|
||||
echo " \"download_linux_mips\": \"$(BASE_URL)/AdGuardHome_linux_mips_softfloat.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_mipsle\": \"$(BASE_URL)/AdGuardHome_linux_mipsle_softfloat.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_mips64\": \"$(BASE_URL)/AdGuardHome_linux_mips64_softfloat.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_linux_mips64le\": \"$(BASE_URL)/AdGuardHome_linux_mips64le_softfloat.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
|
||||
# FreeBSD
|
||||
echo " \"download_freebsd_386\": \"$(BASE_URL)/AdGuardHome_freebsd_386.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_freebsd_amd64\": \"$(BASE_URL)/AdGuardHome_freebsd_amd64.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
|
||||
# FreeBSD, all kinds of ARM
|
||||
echo " \"download_freebsd_arm\": \"$(BASE_URL)/AdGuardHome_freebsd_armv6.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_freebsd_armv5\": \"$(BASE_URL)/AdGuardHome_freebsd_armv5.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_freebsd_armv6\": \"$(BASE_URL)/AdGuardHome_freebsd_armv6.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_freebsd_armv7\": \"$(BASE_URL)/AdGuardHome_freebsd_armv7.tar.gz\"," >> $(DIST_DIR)/version.json
|
||||
echo " \"download_freebsd_arm64\": \"$(BASE_URL)/AdGuardHome_freebsd_arm64.tar.gz\"" >> $(DIST_DIR)/version.json
|
||||
|
||||
# Finish
|
||||
echo "}" >> $(DIST_DIR)/version.json
|
||||
endef
|
38
README.md
38
README.md
|
@ -14,9 +14,6 @@
|
|||
<a href="https://twitter.com/AdGuard">Twitter</a> |
|
||||
<a href="https://t.me/adguard_en">Telegram</a>
|
||||
<br /><br />
|
||||
<a href="https://travis-ci.com/AdguardTeam/AdGuardHome">
|
||||
<img src="https://travis-ci.com/AdguardTeam/AdGuardHome.svg" alt="Build status" />
|
||||
</a>
|
||||
<a href="https://codecov.io/github/AdguardTeam/AdGuardHome?branch=master">
|
||||
<img src="https://img.shields.io/codecov/c/github/AdguardTeam/AdGuardHome/master.svg" alt="Code Coverage" />
|
||||
</a>
|
||||
|
@ -153,17 +150,11 @@ Is there a chance to handle this in the future? DNS will never be enough to do t
|
|||
|
||||
### Prerequisites
|
||||
|
||||
You will need:
|
||||
You will need this to build AdGuard Home:
|
||||
|
||||
* [go](https://golang.org/dl/) v1.14 or later.
|
||||
* [node.js](https://nodejs.org/en/download/) v10 or later.
|
||||
|
||||
You can either install them via the provided links or use [brew.sh](https://brew.sh/) if you're on Mac:
|
||||
|
||||
```bash
|
||||
brew install go node
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
Open Terminal and execute these commands:
|
||||
|
@ -174,15 +165,26 @@ cd AdGuardHome
|
|||
make
|
||||
```
|
||||
|
||||
#### (For devs) Upload translations
|
||||
```
|
||||
node upload.js
|
||||
```
|
||||
Check the [`Makefile`](https://github.com/AdguardTeam/AdGuardHome/blob/master/Makefile) to learn about other commands.
|
||||
|
||||
#### (For devs) Download translations
|
||||
```
|
||||
node download.js
|
||||
```
|
||||
#### Preparing release
|
||||
|
||||
You'll need this to prepare a release build:
|
||||
|
||||
* [goreleaser](https://goreleaser.com/)
|
||||
* [snapcraft](https://snapcraft.io/)
|
||||
|
||||
Run `make snapshot` or `make release` to build all AdGuard distrs.
|
||||
|
||||
#### Docker image
|
||||
|
||||
* Run `make docker` to build the Docker image locally.
|
||||
* Run `make docker-multi-arch` to build the multi-arch Docker image (the one that we publish to Docker Hub).
|
||||
|
||||
### Resources that we update periodically
|
||||
|
||||
* `scripts/translations`
|
||||
* `scripts/whotracksme`
|
||||
|
||||
<a id="contributing"></a>
|
||||
## Contributing
|
||||
|
|
1
go.sum
1
go.sum
|
@ -146,6 +146,7 @@ golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
|||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190624180213-70d37148ca0c/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
@ -18,61 +18,20 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/AdguardTeam/AdGuardHome/util"
|
||||
|
||||
"github.com/AdguardTeam/golibs/log"
|
||||
)
|
||||
|
||||
// Convert version.json data to our JSON response
|
||||
func getVersionResp(data []byte) []byte {
|
||||
versionJSON := make(map[string]interface{})
|
||||
err := json.Unmarshal(data, &versionJSON)
|
||||
if err != nil {
|
||||
log.Error("version.json: %s", err)
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
ret := make(map[string]interface{})
|
||||
ret["can_autoupdate"] = false
|
||||
|
||||
var ok1, ok2, ok3 bool
|
||||
ret["new_version"], ok1 = versionJSON["version"].(string)
|
||||
ret["announcement"], ok2 = versionJSON["announcement"].(string)
|
||||
ret["announcement_url"], ok3 = versionJSON["announcement_url"].(string)
|
||||
selfUpdateMinVersion, ok4 := versionJSON["selfupdate_min_version"].(string)
|
||||
if !ok1 || !ok2 || !ok3 || !ok4 {
|
||||
log.Error("version.json: invalid data")
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
// the key is download_linux_arm or download_linux_arm64 for regular ARM versions
|
||||
dloadName := fmt.Sprintf("download_%s_%s", runtime.GOOS, runtime.GOARCH)
|
||||
if runtime.GOARCH == "arm" && ARMVersion == "5" {
|
||||
// the key is download_linux_armv5 for ARMv5
|
||||
dloadName = fmt.Sprintf("download_%s_%sv%s", runtime.GOOS, runtime.GOARCH, ARMVersion)
|
||||
}
|
||||
_, ok := versionJSON[dloadName]
|
||||
if ok && ret["new_version"] != versionString && versionString >= selfUpdateMinVersion {
|
||||
canUpdate := true
|
||||
|
||||
tlsConf := tlsConfigSettings{}
|
||||
Context.tls.WriteDiskConfig(&tlsConf)
|
||||
|
||||
if runtime.GOOS != "windows" &&
|
||||
((tlsConf.Enabled && (tlsConf.PortHTTPS < 1024 || tlsConf.PortDNSOverTLS < 1024)) ||
|
||||
config.BindPort < 1024 ||
|
||||
config.DNS.Port < 1024) {
|
||||
// On UNIX, if we're running under a regular user,
|
||||
// but with CAP_NET_BIND_SERVICE set on a binary file,
|
||||
// and we're listening on ports <1024,
|
||||
// we won't be able to restart after we replace the binary file,
|
||||
// because we'll lose CAP_NET_BIND_SERVICE capability.
|
||||
canUpdate, _ = util.HaveAdminRights()
|
||||
}
|
||||
ret["can_autoupdate"] = canUpdate
|
||||
}
|
||||
|
||||
d, _ := json.Marshal(ret)
|
||||
return d
|
||||
type updateInfo struct {
|
||||
pkgURL string // URL for the new package
|
||||
pkgName string // Full path to package file
|
||||
newVer string // New version string
|
||||
updateDir string // Full path to the directory containing unpacked files from the new package
|
||||
backupDir string // Full path to backup directory
|
||||
configName string // Full path to the current configuration file
|
||||
updateConfigName string // Full path to the configuration file to check by the new binary
|
||||
curBinName string // Full path to the current executable file
|
||||
bkpBinName string // Full path to the current executable file in backup directory
|
||||
newBinName string // Full path to the new executable file
|
||||
}
|
||||
|
||||
type getVersionJSONRequest struct {
|
||||
|
@ -81,7 +40,6 @@ type getVersionJSONRequest struct {
|
|||
|
||||
// Get the latest available version from the Internet
|
||||
func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if Context.disableUpdate {
|
||||
return
|
||||
}
|
||||
|
@ -103,7 +61,7 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
|
|||
if cached {
|
||||
log.Tracef("Returning cached data")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(getVersionResp(data))
|
||||
_, _ = w.Write(getVersionResp(data))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -146,6 +104,80 @@ func handleGetVersionJSON(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
// Perform an update procedure to the latest available version
|
||||
func handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
if len(config.versionCheckJSON) == 0 {
|
||||
httpError(w, http.StatusBadRequest, "/update request isn't allowed now")
|
||||
return
|
||||
}
|
||||
|
||||
u, err := getUpdateInfo(config.versionCheckJSON)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = doUpdate(u)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
returnOK(w)
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
|
||||
go finishUpdate(u)
|
||||
}
|
||||
|
||||
// Convert version.json data to our JSON response
|
||||
func getVersionResp(data []byte) []byte {
|
||||
versionJSON := make(map[string]interface{})
|
||||
err := json.Unmarshal(data, &versionJSON)
|
||||
if err != nil {
|
||||
log.Error("version.json: %s", err)
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
ret := make(map[string]interface{})
|
||||
ret["can_autoupdate"] = false
|
||||
|
||||
var ok1, ok2, ok3 bool
|
||||
ret["new_version"], ok1 = versionJSON["version"].(string)
|
||||
ret["announcement"], ok2 = versionJSON["announcement"].(string)
|
||||
ret["announcement_url"], ok3 = versionJSON["announcement_url"].(string)
|
||||
selfUpdateMinVersion, ok4 := versionJSON["selfupdate_min_version"].(string)
|
||||
if !ok1 || !ok2 || !ok3 || !ok4 {
|
||||
log.Error("version.json: invalid data")
|
||||
return []byte{}
|
||||
}
|
||||
|
||||
_, ok := getDownloadURL(versionJSON)
|
||||
if ok && ret["new_version"] != versionString && versionString >= selfUpdateMinVersion {
|
||||
canUpdate := true
|
||||
|
||||
tlsConf := tlsConfigSettings{}
|
||||
Context.tls.WriteDiskConfig(&tlsConf)
|
||||
|
||||
if runtime.GOOS != "windows" &&
|
||||
((tlsConf.Enabled && (tlsConf.PortHTTPS < 1024 || tlsConf.PortDNSOverTLS < 1024)) ||
|
||||
config.BindPort < 1024 ||
|
||||
config.DNS.Port < 1024) {
|
||||
// On UNIX, if we're running under a regular user,
|
||||
// but with CAP_NET_BIND_SERVICE set on a binary file,
|
||||
// and we're listening on ports <1024,
|
||||
// we won't be able to restart after we replace the binary file,
|
||||
// because we'll lose CAP_NET_BIND_SERVICE capability.
|
||||
canUpdate, _ = util.HaveAdminRights()
|
||||
}
|
||||
ret["can_autoupdate"] = canUpdate
|
||||
}
|
||||
|
||||
d, _ := json.Marshal(ret)
|
||||
return d
|
||||
}
|
||||
|
||||
// Copy file on disk
|
||||
func copyFile(src, dst string) error {
|
||||
d, e := ioutil.ReadFile(src)
|
||||
|
@ -159,19 +191,6 @@ func copyFile(src, dst string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
type updateInfo struct {
|
||||
pkgURL string // URL for the new package
|
||||
pkgName string // Full path to package file
|
||||
newVer string // New version string
|
||||
updateDir string // Full path to the directory containing unpacked files from the new package
|
||||
backupDir string // Full path to backup directory
|
||||
configName string // Full path to the current configuration file
|
||||
updateConfigName string // Full path to the configuration file to check by the new binary
|
||||
curBinName string // Full path to the current executable file
|
||||
bkpBinName string // Full path to the current executable file in backup directory
|
||||
newBinName string // Full path to the new executable file
|
||||
}
|
||||
|
||||
// Fill in updateInfo object
|
||||
func getUpdateInfo(jsonData []byte) (*updateInfo, error) {
|
||||
var u updateInfo
|
||||
|
@ -184,7 +203,12 @@ func getUpdateInfo(jsonData []byte) (*updateInfo, error) {
|
|||
return nil, fmt.Errorf("JSON parse: %s", err)
|
||||
}
|
||||
|
||||
u.pkgURL = versionJSON[fmt.Sprintf("download_%s_%s", runtime.GOOS, runtime.GOARCH)].(string)
|
||||
pkgURL, ok := getDownloadURL(versionJSON)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("failed to get download URL")
|
||||
}
|
||||
|
||||
u.pkgURL = pkgURL
|
||||
u.newVer = versionJSON["version"].(string)
|
||||
if len(u.pkgURL) == 0 || len(u.newVer) == 0 {
|
||||
return nil, fmt.Errorf("invalid JSON")
|
||||
|
@ -226,6 +250,33 @@ func getUpdateInfo(jsonData []byte) (*updateInfo, error) {
|
|||
return &u, nil
|
||||
}
|
||||
|
||||
// getDownloadURL - gets download URL for the current GOOS/GOARCH
|
||||
// returns
|
||||
func getDownloadURL(json map[string]interface{}) (string, bool) {
|
||||
var key string
|
||||
|
||||
if runtime.GOARCH == "arm" && ARMVersion != "" {
|
||||
// the key is:
|
||||
// download_linux_armv5 for ARMv5
|
||||
// download_linux_armv6 for ARMv6
|
||||
// download_linux_armv7 for ARMv7
|
||||
key = fmt.Sprintf("download_%s_%sv%s", runtime.GOOS, runtime.GOARCH, ARMVersion)
|
||||
}
|
||||
|
||||
u, ok := json[key]
|
||||
if !ok {
|
||||
// the key is download_linux_arm or download_linux_arm64 for regular ARM versions
|
||||
key = fmt.Sprintf("download_%s_%s", runtime.GOOS, runtime.GOARCH)
|
||||
u, ok = json[key]
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return u.(string), true
|
||||
}
|
||||
|
||||
// Unpack all files from .zip file to the specified directory
|
||||
// Existing files are overwritten
|
||||
// Return the list of files (not directories) written
|
||||
|
@ -526,31 +577,3 @@ func finishUpdate(u *updateInfo) {
|
|||
// Unreachable code
|
||||
}
|
||||
}
|
||||
|
||||
// Perform an update procedure to the latest available version
|
||||
func handleUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if len(config.versionCheckJSON) == 0 {
|
||||
httpError(w, http.StatusBadRequest, "/update request isn't allowed now")
|
||||
return
|
||||
}
|
||||
|
||||
u, err := getUpdateInfo(config.versionCheckJSON)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = doUpdate(u)
|
||||
if err != nil {
|
||||
httpError(w, http.StatusInternalServerError, "%s", err)
|
||||
return
|
||||
}
|
||||
|
||||
returnOK(w)
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
|
||||
go finishUpdate(u)
|
||||
}
|
||||
|
|
17
home/home.go
17
home/home.go
|
@ -134,6 +134,15 @@ func Main(version string, channel string, armVer string) {
|
|||
run(args)
|
||||
}
|
||||
|
||||
// version - returns the current version string
|
||||
func version() string {
|
||||
msg := "AdGuard Home, version %s, channel %s, arch %s %s"
|
||||
if ARMVersion != "" {
|
||||
msg = msg + " v" + ARMVersion
|
||||
}
|
||||
return fmt.Sprintf(msg, versionString, updateChannel, runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
// run initializes configuration and runs the AdGuard Home
|
||||
// run is a blocking method!
|
||||
// nolint
|
||||
|
@ -153,11 +162,7 @@ func run(args options) {
|
|||
configureLogger(args)
|
||||
|
||||
// print the first message after logger is configured
|
||||
msg := "AdGuard Home, version %s, channel %s, arch %s %s"
|
||||
if ARMVersion != "" {
|
||||
msg = msg + " v" + ARMVersion
|
||||
}
|
||||
log.Printf(msg, versionString, updateChannel, runtime.GOOS, runtime.GOARCH)
|
||||
log.Println(version())
|
||||
log.Debug("Current working directory is %s", Context.workDir)
|
||||
if args.runningAsService {
|
||||
log.Info("AdGuard Home is running as a service")
|
||||
|
@ -564,7 +569,7 @@ func loadOptions() options {
|
|||
{"verbose", "v", "Enable verbose output", nil, func() { o.verbose = true }},
|
||||
{"glinet", "", "Run in GL-Inet compatibility mode", nil, func() { o.glinetMode = true }},
|
||||
{"version", "", "Show the version and exit", nil, func() {
|
||||
fmt.Printf("AdGuardHome %s\n", versionString)
|
||||
fmt.Println(version())
|
||||
os.Exit(0)
|
||||
}},
|
||||
{"help", "", "Print this help", nil, func() {
|
||||
|
|
Loading…
Reference in New Issue