Compare commits

..
17 Commits
Author SHA1 Message Date
devydave a2647c3331 refactor: add option to run specific test files, file mode ssh key and ignore changes to path_to_file
continuous-integration/drone/push Build is passing
2026-08-31 10:59:02 +02:00
devydave d510e13461 docs: explains -t option
continuous-integration/drone/push Build is passing
2026-08-24 23:31:12 +02:00
devydave 470a34fdb1 test: adds automation script and ignore path_to_repo changes 2026-08-24 23:29:30 +02:00
devydave a28cc7afce docs: more formatting and typo
continuous-integration/drone/push Build is passing
2026-08-13 20:41:04 +02:00
devydave 21dccc895d docs: formatting
continuous-integration/drone/push Build is passing
2026-08-13 20:38:20 +02:00
devydave bb3c7576f8 docs: add setup documentation
continuous-integration/drone/push Build is passing
2026-08-13 20:34:14 +02:00
devydave 02cbf7c014 refactor: make path editable without nix and impure 2026-08-13 19:57:51 +02:00
devydave a8c8df8604 fix: remove personal terminal choice
continuous-integration/drone/push Build is passing
2026-08-12 23:01:12 +02:00
devydave 5766a25144 feat: adds host module
continuous-integration/drone/push Build is passing
2026-08-12 22:43:16 +02:00
devydave 1ccbb31b70 refactor: simplify config and remove hom-manager dependency
continuous-integration/drone/push Build is passing
2026-08-07 15:15:04 +02:00
devydave 5e0dac6508 refactor: adjusting hosts generation
continuous-integration/drone/push Build is passing
2026-08-07 14:11:00 +02:00
devydave 38d13eae73 refactor: better variable name 2026-08-07 13:42:22 +02:00
devydave b5e22790be refactor: better name for inspect function 2026-08-07 13:36:25 +02:00
devydave beee7bd3f2 refactor: only export ssh auth socket when not existing 2026-08-07 11:09:30 +02:00
devydave 0c0e16914a refactor: remove traefik deployment because it is not needed 2026-08-07 10:36:23 +02:00
devydave 9bd8f4962a feat: adds running integrations test suite
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2026-08-06 22:00:29 +02:00
devydave 4e42d4fb9e feat: adds local vm setup that can reach each other
continuous-integration/drone/push Build is passing
2026-07-30 16:53:34 +02:00
325 changed files with 32226 additions and 16702 deletions
+3 -3
View File
@@ -3,12 +3,12 @@ kind: pipeline
name: coopcloud.tech/abra
steps:
- name: make check
image: golang:1.27
image: golang:1.26
commands:
- make check
- name: xgettext-go
image: golang:1.27
image: golang:1.26
environment:
GOPRIVATE: coopcloud.tech
commands:
@@ -43,7 +43,7 @@ steps:
- tag
- name: make test
image: golang:1.27
image: golang:1.26
environment:
ABRA_DIR: /root/.abra_test
commands:
-1
View File
@@ -1 +0,0 @@
vendor/** linguist-generated=true
+1
View File
@@ -7,3 +7,4 @@
/bin
dist/
tests/integration/.bats
tests/resources/path_to_repo
+1 -1
View File
@@ -1,5 +1,5 @@
# Build image
FROM golang:1.27-alpine AS build
FROM golang:1.26-alpine AS build
ENV GOPRIVATE=coopcloud.tech
+50
View File
@@ -84,3 +84,53 @@ build-mo:
release:
@goreleaser release --clean
CLIENT_VM := abra-client
vm-client-status:
sudo systemctl status microvm@$(CLIENT_VM).service
vm-client-create:test-integration-hosts
sudo microvm -f git+file://$$(pwd) -c $(CLIENT_VM)
vm-client-start:
sudo systemctl start microvm@$(CLIENT_VM).service
vm-client-update:test-integration-hosts
sudo microvm -R -f git+file://$$(pwd) -u $(CLIENT_VM)
vm-client-run:
sudo microvm -f git+file://$$(pwd) -r $(CLIENT_VM)
vm-client-stop:
sudo systemctl stop microvm@$(CLIENT_VM)
vm-client-delete: vm-client-stop
sudo rm -rf /var/lib/microvms/$(CLIENT_VM)
vm-client-connect:
ssh abra@10.0.0.2 -i ./tests/resources/local_integration_ssh
SERVER_VM := abra-server
vm-server-status:
sudo systemctl status microvm@$(SERVER_VM).service
vm-server-create:
sudo microvm -f git+file://$$(pwd) -c $(SERVER_VM)
vm-server-start:
sudo systemctl start microvm@$(SERVER_VM).service
vm-server-update:
sudo microvm -R -f git+file://$$(pwd) -u $(SERVER_VM)
vm-server-run:
sudo microvm -f git+file://$$(pwd) -r $(SERVER_VM)
vm-server-stop:
sudo systemctl stop microvm@$(SERVER_VM)
vm-server-delete: vm-server-stop
sudo rm -rf /var/lib/microvms/$(SERVER_VM)
vm-server-connect:
ssh abra@10.0.0.3 -i ./tests/resources/local_integration_ssh
test-integration-hosts:
./scripts/tests/extra_hosts
# TEST_INTEGRATION_FILES overrides the default of running all test files
ifdef TEST_INTEGRATION_FILES
test_files := $(TEST_INTEGRATION_FILES);
else
test_files := *;
endif
test-integration: build
chmod 600 ./tests/resources/local_integration_ssh
ssh-add ./tests/resources/local_integration_ssh
@bats -Tp --verbose-run tests/integration/$(test_files)
Generated
+38
View File
@@ -18,6 +18,27 @@
"type": "github"
}
},
"microvm": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"spectrum": "spectrum"
},
"locked": {
"lastModified": 1784666190,
"narHash": "sha256-xgfS6slV7J3baMooNN1UuBi51RIgg9y0DbCxfSA0668=",
"owner": "astro",
"repo": "microvm.nix",
"rev": "fa5340ac684cdce8a22b6d4a0bcebb0cc999275e",
"type": "github"
},
"original": {
"owner": "astro",
"repo": "microvm.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1778443072,
@@ -37,9 +58,26 @@
"root": {
"inputs": {
"flake-utils": "flake-utils",
"microvm": "microvm",
"nixpkgs": "nixpkgs"
}
},
"spectrum": {
"flake": false,
"locked": {
"lastModified": 1783694892,
"narHash": "sha256-xO8f7Qng+18FK2UlB9vcrkxCaQMCt5WjCH24aW/11eg=",
"ref": "refs/heads/main",
"rev": "24c4346e30fdea8d8e80f34aec3554a15a667d24",
"revCount": 1410,
"type": "git",
"url": "https://spectrum-os.org/git/spectrum"
},
"original": {
"type": "git",
"url": "https://spectrum-os.org/git/spectrum"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
+87 -18
View File
@@ -4,34 +4,103 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
microvm = {
url = "github:astro/microvm.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
{
self,
nixpkgs,
microvm,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
packages = rec {
abra = pkgs.callPackage ./package.nix { };
default = abra;
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
publicKey = builtins.readFile ./tests/resources/local_integration_ssh.pub;
privateKey = builtins.readFile ./tests/resources/local_integration_ssh;
pathToRepo = builtins.readFile ./tests/resources/path_to_repo;
# local DNS aliases for the server host
extraHosts = builtins.readFile ./tests/resources/extra_hosts;
username = "abra";
password = "abra";
defaultDNS = [
# Quad9.net
"9.9.9.9"
"149.112.112.112"
"2620:fe::fe"
"2620:fe::9"
];
in
{
nixosConfigurations = {
abra-client = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
microvm.nixosModules.microvm
./nix/hosts/client/configuration.nix
];
specialArgs = {
inherit
publicKey
privateKey
username
password
defaultDNS
extraHosts
pathToRepo
;
};
};
apps = rec {
abra = flake-utils.lib.mkApp { drv = self.packages.${system}.abra; };
default = abra;
abra-server = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
microvm.nixosModules.microvm
./nix/hosts/server/configuration.nix
];
specialArgs = {
inherit
publicKey
privateKey
username
password
defaultDNS
;
};
};
devShells.default = pkgs.mkShell {
packages = with pkgs; [
go_1_26
gnumake
};
nixosModules = rec {
host = {
imports = [
microvm.nixosModules.host
./nix/modules/host.nix
];
};
}
);
default = host;
};
packages = rec {
abra = pkgs.callPackage ./nix/package.nix { };
default = abra;
};
apps = rec {
abra = flake-utils.lib.mkApp { drv = self.packages.${system}.abra; };
default = abra;
};
devShells.${system}.default = pkgs.mkShell {
# testing env variables
BATS_LIB_PATH = "~/.local/share/bats/";
TEST_SERVER = "abra.local";
ABRA_DIR = "$HOME/.abra_test";
packages = with pkgs; [
go_1_26
gnumake
gopls
];
};
};
}
+34 -28
View File
@@ -1,6 +1,6 @@
module coopcloud.tech/abra
go 1.27
go 1.26.0
require (
coopcloud.tech/tagcmp v0.0.0-20260515102403-c26951b55977
@@ -9,24 +9,18 @@ require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/charmbracelet/log v1.0.0
github.com/containers/image v3.0.2+incompatible
github.com/decentral1se/passgen v1.0.1
github.com/distribution/reference v0.6.0
github.com/docker/cli v28.4.0+incompatible
github.com/docker/docker v28.5.2+incompatible
github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.19.3
github.com/go-git/go-git/v5 v5.19.1
github.com/google/go-cmp v0.7.0
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/leonelquinteros/gotext v1.8.0
github.com/leonelquinteros/gotext v1.7.2
github.com/moby/sys/signal v0.7.1
github.com/moby/term v0.5.2
github.com/pkg/errors v0.9.1
github.com/schollz/progressbar/v3 v3.19.1
github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.12.1
golang.org/x/sys v0.49.0
golang.org/x/term v0.47.0
golang.org/x/term v0.44.0
gopkg.in/yaml.v3 v3.0.1
gotest.tools/v3 v3.5.2
)
@@ -54,29 +48,25 @@ require (
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/containers/storage v1.38.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.9.2 // indirect
github.com/go-git/go-billy/v5 v5.9.0 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -84,6 +74,7 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kevinburke/ssh_config v1.6.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
@@ -94,9 +85,7 @@ require (
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/go-archive v0.1.0 // indirect
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/sys/user v0.4.0 // indirect
github.com/moby/sys/userns v0.1.0 // indirect
github.com/morikuni/aec v1.1.0 // indirect
@@ -105,23 +94,19 @@ require (
github.com/muesli/termenv v0.16.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/opencontainers/runc v1.1.13 // indirect
github.com/opencontainers/runtime-spec v1.1.0 // indirect
github.com/pjbgf/sha1cd v0.7.0 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/pjbgf/sha1cd v0.6.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/skeema/knownhosts v1.3.2 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
@@ -138,11 +123,11 @@ require (
go.opentelemetry.io/otel/trace v1.42.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/crypto v0.56.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.57.0 // indirect
golang.org/x/text v0.41.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/text v0.36.0 // indirect
golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
@@ -152,6 +137,27 @@ require (
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
github.com/containers/image v3.0.2+incompatible
github.com/containers/storage v1.38.2 // indirect
github.com/decentral1se/passgen v1.0.1
github.com/docker/docker-credential-helpers v0.9.5 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8
github.com/moby/patternmatcher v0.6.1 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/sergi/go-diff v1.4.0 // indirect
github.com/spf13/cobra v1.10.1
github.com/stretchr/testify v1.11.1
github.com/theupdateframework/notary v0.7.0 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
golang.org/x/sys v0.46.0
)
replace github.com/docker/cli v28.4.0+incompatible => git.coopcloud.tech/toolshed/docker-cli v28.5.3-0.20260202112816-30df2d0b3a00+incompatible
replace github.com/spf13/cobra => github.com/decentral1se/cobra v1.10.2
+25 -22
View File
@@ -306,6 +306,7 @@ github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW
github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8=
github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decentral1se/cobra v1.10.2 h1:MZ8Ifi/jRels9sZrpSccDbUlK++3b2HlBODfv0Bh6x0=
github.com/decentral1se/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
@@ -388,12 +389,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
github.com/go-git/go-billy/v5 v5.9.2 h1:OXFSRyz4g20upsGDJgQG9Bak1l/ZEv8GHVYB52O71sE=
github.com/go-git/go-billy/v5 v5.9.2/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
github.com/go-git/go-billy/v5 v5.9.0 h1:jItGXszUDRtR/AlferWPTMN4j38BQ88XnXKbilmmBPA=
github.com/go-git/go-billy/v5 v5.9.0/go.mod h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
github.com/go-git/go-git/v5 v5.19.3 h1:qHttbZ+Am7wEjdTpR1XMQ4rl42FK9SIXzZ4o9x7s6M4=
github.com/go-git/go-git/v5 v5.19.3/go.mod h1:Ye4C8dVigkqrv9UsB4NMdSLV4yAIkLiIhW61gcOyhl4=
github.com/go-git/go-git/v5 v5.19.1 h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
github.com/go-git/go-git/v5 v5.19.1/go.mod h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
@@ -585,6 +586,8 @@ github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdY
github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -602,8 +605,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leonelquinteros/gotext v1.8.0 h1:wwpxINWmWq3JN3WFI9BoSupBFrvAqxZTtf3qppijYkU=
github.com/leonelquinteros/gotext v1.8.0/go.mod h1:Wqke380+8unZq2F4oTmmqmbUY9zWQMzwImdC34XOAo8=
github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
@@ -750,13 +753,14 @@ github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuh
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pelletier/go-toml v1.8.1/go.mod h1:T2/BmBdy8dvIRq1a/8aqjN41wvWlN4lrapLU/GW4pbc=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pjbgf/sha1cd v0.7.0 h1:ZRNPKHj+gfkLBf0KJv/p1Hmz+7bqCT5o311YX0nq+DA=
github.com/pjbgf/sha1cd v0.7.0/go.mod h1:pKR5Li+qTCo+ebqITZfwPlJGoCFCvSO00qxjbNtTUFQ=
github.com/pjbgf/sha1cd v0.6.0 h1:3WJ8Wz8gvDz29quX1OcEmkAlUg9diU4GxJHqs0/XiwU=
github.com/pjbgf/sha1cd v0.6.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
@@ -853,8 +857,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI=
@@ -942,9 +946,8 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@@ -963,8 +966,8 @@ golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y=
golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1040,8 +1043,8 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210825183410-e898025ed96a/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1136,13 +1139,13 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.47.0 h1:cpyOwQkr2Nzxe2p6KnrTG7Ei2wtzjsuSjhpXXepZ/BM=
golang.org/x/term v0.47.0/go.mod h1:efAfAuG6RKABvg1ArGebRkvX6LXul14SM67o2e4mUCY=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -1152,8 +1155,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+75
View File
@@ -0,0 +1,75 @@
# Hosts
These hosts are VMs used for running the integration tests on your local machine.
The hosts contain a client where abra with the tests will be run and a server
to simulate a remote machine.
## Prepare the host
For this setup to work you need a machine with NixOS and flakes enabled.
1. Import the abra flake
The install example is based on the [using nix flakes wiki page](https://nixos.wiki/wiki/flakes#Using_nix_flakes_with_NixOS).
```nix
inputs = {
abra = {
url = "git+https://git.coopcloud.tech/toolshed/abra.git";
};
};
```
2. Add the host module to your configuration
Now add the host module to your configuration. At the toplevel of a flake it could look like this:
```nix
desktop = inputs.nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
abra.nixosModules.host
./configuration.nix
];
specialArgs = inputs;
};
```
3. Adjust the required config
```nix
abra.testing = {
enable = true; # loads the required config, when set true
externalInterface = "eth1"; # adjust this to your network interface that has access to the internet
};
```
You can look up the interface with `ip link`. After that rebuild your system and you are ready to go.
## Get started
All commands are run on the repository root path.
1. Set path to repo
The VM will create a shared volume from the content of the file `tests/resources/path_to_repo`.
Set the content of the file to the path of your abra repository and run the following command to prevent tracking of the file:
```sh
git update-index --assume-unchanged ./tests/resources/path_to_repo
```
This will share the repository from your machine with the VM and you can edit files while testing without rebuiling
or restarting the VM.
2. Create the VMs
```sh
make vm-client-create
make vm-server-create
```
3. Start the VMs
```sh
make vm-client-start
make vm-server-start
```
After running that command you should be able to ping the machines.
The client runs on 10.0.0.2 and server 10.0.0.3
4. Connect to the client
Running the following command will connect to the client VM via SSH.
```sh
make vm-client-connect
```
5. Run the tests
```sh
make test-integration
```
+145
View File
@@ -0,0 +1,145 @@
{
lib,
pkgs,
publicKey,
username,
pathToRepo,
defaultDNS,
extraHosts,
...
}:
let
index = 2;
mac = "00:00:00:00:00:01";
serverIp = "10.0.0.3";
in
{
imports = [
../../modules/ssh.nix
../../modules/user.nix
../../modules/docker.nix
];
microvm = {
vcpu = 4;
mem = 2049; # see issue related in microvm repo with 2048, so add 1MB
interfaces = [
{
id = "vm${toString index}";
type = "tap";
inherit mac;
}
];
shares = [
{
proto = "virtiofs";
tag = "repo";
# Source path can be absolute or relative
# to /var/lib/microvms/$hostName
source = "${lib.trim pathToRepo}";
mountPoint = "/home/${username}/abra";
}
];
};
boot.tmp = {
useTmpfs = true;
tmpfsSize = "2G";
};
networking = {
hostName = "abra-client";
useNetworkd = true;
};
systemd.network.networks."10-eth" = {
matchConfig.MACAddress = mac;
# Static IP configuration
address = [
"10.0.0.${toString index}/32"
"fec0::${lib.toHexString index}/128"
];
routes = [
{
# A route to the host
Destination = "10.0.0.0/32";
GatewayOnLink = true;
}
{
# Route to server
Destination = "${serverIp}/32";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "0.0.0.0/0";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "::/0";
Gateway = "fec0::";
GatewayOnLink = true;
}
];
networkConfig = {
# DNS servers no longer come from DHCP nor Router
# Advertisements. Perhaps you want to change the defaults:
DNS = defaultDNS;
};
};
# open the ports to allow ssh access from the host
networking.firewall.allowedTCPPorts = [ 22 ];
networking.firewall.allowedUDPPorts = [ 22 ];
networking.extraHosts = extraHosts;
environment.variables = {
CGO_ENABLED = 0;
TEST_SERVER = "abra.local";
ABRA_DIR = "$HOME/.abra_test";
};
programs.ssh = {
startAgent = true;
knownHostsFiles = [
(pkgs.writeText "local.keys" ''
abra.local ${publicKey}
'')
];
extraConfig = ''
Host abra.local
HostName abra.local
Port 22
User ${username}
'';
};
programs.git = {
enable = true;
# many recipe commands need a preset git user
config = [
{
user = {
name = "abra dev";
email = "helo@coopcloud.tech";
};
}
];
};
environment.systemPackages = with pkgs; [
# build dependencies, copied from flake.nix
go_1_26
gnumake
# testing dependencies
(bats.withLibraries (p: [
p.bats-assert
p.bats-file
p.bats-support
]))
jq
wget
];
}
+42
View File
@@ -0,0 +1,42 @@
{
serverIp,
username,
...
}:
{
# Home Manager needs a bit of information about you and the
# paths it should manage.
home.username = username;
home.homeDirectory = "/home/${username}";
# This value determines the Home Manager release that your
# configuration is compatible with. This helps avoid breakage
# when a new Home Manager release introduces backwards
# incompatible changes.
#
# You can update Home Manager without changing this value. See
# the Home Manager release notes for a list of state version
# changes in each release.
home.stateVersion = "26.05";
# Let Home Manager install and manage itself.
programs.home-manager.enable = true;
programs.ssh = {
enable = true;
enableDefaultConfig = false;
settings = {
"abra.local" = {
HostName = serverIp;
User = username;
Port = 22;
IdentityFile = "/home/${username}/abra/tests/resources/local_integration_ssh";
};
"*.abra.local" = {
HostName = serverIp;
User = username;
Port = 22;
IdentityFile = "/home/${username}/abra/tests/resources/local_integration_sshn";
};
};
};
}
+91
View File
@@ -0,0 +1,91 @@
{
lib,
defaultDNS,
...
}:
let
index = 3;
mac = "00:00:00:00:00:02";
clientIp = "10.0.0.2";
in
{
imports = [
../../modules/ssh.nix
../../modules/user.nix
../../modules/docker.nix
];
microvm = {
vcpu = 4;
mem = 8097; # see issue related in microvm repo with 2048, so add 1MB
interfaces = [
{
id = "vm${toString index}";
type = "tap";
inherit mac;
}
];
};
boot.tmp = {
useTmpfs = true;
tmpfsSize = "8G";
};
networking = {
hostName = "abra-server";
useNetworkd = true;
};
systemd.network.networks."11-eth" = {
matchConfig.MACAddress = mac;
# Static IP configuration
address = [
"10.0.0.${toString index}/32"
"fec0::${lib.toHexString index}/128"
];
routes = [
{
# A route to the host
Destination = "10.0.0.0/32";
GatewayOnLink = true;
}
{
# Route to server
Destination = "${clientIp}/32";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "0.0.0.0/0";
Gateway = "10.0.0.0";
GatewayOnLink = true;
}
{
# Default route
Destination = "::/0";
Gateway = "fec0::";
GatewayOnLink = true;
}
];
networkConfig = {
# DNS servers no longer come from DHCP nor Router
# Advertisements. Perhaps you want to change the defaults:
DNS = defaultDNS;
};
};
# open the ports to allow ssh access from the host
networking.firewall.allowedTCPPorts = [
22
80
443
1312 # deploy with udp and tcp on same port test
];
networking.firewall.allowedUDPPorts = [
22
80
443
1312 # deploy with udp and tcp on same port test
];
}
+7
View File
@@ -0,0 +1,7 @@
{ ... }:
{
virtualisation.docker = {
enable = true;
liveRestore = false;
};
}
+59
View File
@@ -0,0 +1,59 @@
{
lib,
config,
...
}:
let
maxVMs = 3;
in
{
options = {
abra.testing.enable = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Enable the abra integration testing framework";
};
abra.testing.externalInterface = lib.mkOption {
type = lib.types.str;
default = null;
description = "Change this to the interface with upstream Internet access";
};
};
config = lib.mkIf config.abra.testing.enable {
networking.useNetworkd = true;
systemd.network.wait-online.enable = false;
systemd.network.networks = builtins.listToAttrs (
map (index: {
name = "30-vm${toString index}";
value = {
matchConfig.Name = "vm${toString index}";
# Host's addresses
address = [
"10.0.0.0/32"
"fec0::/128"
];
# Setup routes to the VM
routes = [
{
Destination = "10.0.0.${toString index}/32";
}
{
Destination = "fec0::${lib.toHexString index}/128";
}
];
# Enable routing
networkConfig = {
IPv4Forwarding = true;
IPv6Forwarding = true;
};
};
}) (lib.genList (i: i + 1) maxVMs)
);
networking.nat = {
enable = true;
internalIPs = [ "10.0.0.0/24" ];
externalInterface = config.abra.testing.externalInterface;
};
};
}
+33
View File
@@ -0,0 +1,33 @@
{
privateKey,
publicKey,
username,
...
}:
{
# default host key location
environment.etc."ssh/ssh_host_ed25519_key" = {
text = privateKey;
mode = "0600";
};
environment.etc."ssh/ssh_host_ed25519_key.pub" = {
text = publicKey;
mode = "0644";
};
services.openssh = {
enable = true;
settings = {
PasswordAuthentication = false;
PermitRootLogin = "no";
AllowUsers = [ "${username}" ];
};
generateHostKeys = false;
hostKeys = [
{
path = "/etc/ssh/ssh_host_ed25519_key";
type = "ed25519";
}
];
};
}
+22
View File
@@ -0,0 +1,22 @@
{
username,
password,
publicKey,
...
}:
{
users.users."${username}" = {
isNormalUser = true;
password = password;
home = "/home/${username}";
description = "Abra user";
extraGroups = [
"wheel"
"networkmanager"
"docker"
];
openssh.authorizedKeys.keys = [
publicKey
];
};
}
View File
+2
View File
@@ -52,6 +52,7 @@ func Clone(dir, url string) error {
URL: url,
Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/main"),
SingleBranch: true,
})
if err != nil && gitCloneIgnoreErr(err) {
@@ -70,6 +71,7 @@ func Clone(dir, url string) error {
URL: url,
Tags: git.AllTags,
ReferenceName: plumbing.ReferenceName("refs/heads/master"),
SingleBranch: true,
})
if err != nil && gitCloneIgnoreErr(err) {
+37 -37
View File
@@ -7,7 +7,7 @@
msgid ""
msgstr "Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-10-14 10:32-0400\n"
"POT-Creation-Date: 2026-06-14 17:56+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -189,7 +189,7 @@ msgstr ""
msgid "%d volumes removed successfully"
msgstr ""
#: ./pkg/recipe/git.go:198
#: ./pkg/recipe/git.go:197
#, c-format
msgid "%s (%s) has locally unstaged changes?"
msgstr ""
@@ -419,7 +419,7 @@ msgstr ""
msgid "%s sanitised as %s for new app"
msgstr ""
#: ./pkg/recipe/git.go:451
#: ./pkg/recipe/git.go:445
#, c-format
msgid "%s service is missing image tag?"
msgstr ""
@@ -902,7 +902,7 @@ msgstr ""
msgid "Generate the recipe catalogue"
msgstr ""
#: ./pkg/recipe/git.go:465
#: ./pkg/recipe/git.go:459
#, c-format
msgid "GetRecipeVersions encountered error for %s: %s (collected %d versions)"
msgstr ""
@@ -1815,7 +1815,7 @@ msgstr ""
msgid "are you sure?"
msgstr ""
#: ./pkg/recipe/git.go:163
#: ./pkg/recipe/git.go:162
#, c-format
msgid "attempting to checkout '%s' as chaos commit"
msgstr ""
@@ -1951,7 +1951,7 @@ msgstr ""
msgid "cancelled"
msgstr ""
#: ./pkg/catalogue/catalogue.go:59 ./pkg/recipe/git.go:252
#: ./pkg/catalogue/catalogue.go:59 ./pkg/recipe/git.go:251
#, c-format
msgid "cannot ensure %s is up-to-date, no git remotes configured"
msgstr ""
@@ -1966,7 +1966,7 @@ msgstr ""
msgid "cannot get label %s for %s"
msgstr ""
#: ./pkg/recipe/git.go:59
#: ./pkg/recipe/git.go:58
#, c-format
msgid "cannot redeploy previous chaos version (%s), did you mean to use \"--chaos\"?"
msgstr ""
@@ -2118,7 +2118,7 @@ msgstr ""
msgid "cmd"
msgstr ""
#: ./pkg/recipe/git.go:476
#: ./pkg/recipe/git.go:470
#, c-format
msgid "collected %s for %s"
msgstr ""
@@ -2456,7 +2456,7 @@ msgstr ""
msgid "destination directory does not exist"
msgstr ""
#: ./pkg/recipe/git.go:379
#: ./pkg/recipe/git.go:373
#, c-format
msgid "detected %s as tags for recipe %s"
msgstr ""
@@ -2653,7 +2653,7 @@ msgstr ""
msgid "ensure recipe: %s"
msgstr ""
#: ./pkg/recipe/git.go:57
#: ./pkg/recipe/git.go:56
#, c-format
msgid "ensuring env version %s"
msgstr ""
@@ -2751,12 +2751,12 @@ msgstr ""
msgid "failed to check git status of %s: %s"
msgstr ""
#: ./pkg/git/branch.go:95 ./pkg/recipe/git.go:232
#: ./pkg/git/branch.go:95 ./pkg/recipe/git.go:231
#, c-format
msgid "failed to check out %s in %s"
msgstr ""
#: ./pkg/recipe/git.go:418
#: ./pkg/recipe/git.go:412
#, c-format
msgid "failed to check out %s in %s: %s"
msgstr ""
@@ -2806,7 +2806,7 @@ msgstr ""
msgid "failed to generate random bytes: %w"
msgstr ""
#: ./pkg/recipe/git.go:427
#: ./pkg/recipe/git.go:421
#, c-format
msgid "failed to get compose config for %s: %s"
msgstr ""
@@ -2844,7 +2844,7 @@ msgstr ""
msgid "failed to parse image %s, saw: %s"
msgstr ""
#: ./pkg/recipe/git.go:437
#: ./pkg/recipe/git.go:431
#, c-format
msgid "failed to parse image for %s in %s: %s"
msgstr ""
@@ -2952,7 +2952,7 @@ msgstr ""
msgid "fetch all recipes"
msgstr ""
#: ./pkg/catalogue/catalogue.go:84 ./pkg/recipe/git.go:290
#: ./pkg/catalogue/catalogue.go:84 ./pkg/recipe/git.go:284
#, c-format
msgid "fetched latest git changes for %s"
msgstr ""
@@ -3070,12 +3070,12 @@ msgstr ""
msgid "git changes pushed"
msgstr ""
#: ./pkg/recipe/git.go:423
#: ./pkg/recipe/git.go:417
#, c-format
msgid "git checkout: %s in %s"
msgstr ""
#: ./pkg/git/clone.go:63 ./pkg/git/clone.go:100
#: ./pkg/git/clone.go:64 ./pkg/git/clone.go:102
#, c-format
msgid "git clone %s: cancelled due to interrupt"
msgstr ""
@@ -3085,17 +3085,17 @@ msgstr ""
msgid "git clone: %s"
msgstr ""
#: ./pkg/git/clone.go:87
#: ./pkg/git/clone.go:89
#, c-format
msgid "git clone: %s already exists"
msgstr ""
#: ./pkg/git/clone.go:58 ./pkg/git/clone.go:76 ./pkg/git/clone.go:85
#: ./pkg/git/clone.go:59 ./pkg/git/clone.go:78 ./pkg/git/clone.go:87
#, c-format
msgid "git clone: %s cloned successfully"
msgstr ""
#: ./pkg/git/clone.go:67
#: ./pkg/git/clone.go:68
msgid "git clone: main branch failed, attempting master branch"
msgstr ""
@@ -3150,7 +3150,7 @@ msgstr ""
msgid "git.coopcloud.tech repo exists"
msgstr ""
#: ./pkg/recipe/git.go:390
#: ./pkg/recipe/git.go:384
#, c-format
msgid "git: opening repository in %s"
msgstr ""
@@ -4061,7 +4061,7 @@ msgstr ""
msgid "proceed?"
msgstr ""
#: ./pkg/recipe/git.go:410
#: ./pkg/recipe/git.go:404
#, c-format
msgid "processing %s for %s"
msgstr ""
@@ -4129,7 +4129,7 @@ msgstr ""
msgid "re"
msgstr ""
#: ./pkg/recipe/git.go:158
#: ./pkg/recipe/git.go:157
#, c-format
msgid "read %s as tags for recipe %s"
msgstr ""
@@ -4475,7 +4475,7 @@ msgstr ""
msgid "retrieving docker auth token: failed create docker cli: %s"
msgstr ""
#: ./pkg/recipe/recipe.go:656
#: ./pkg/recipe/recipe.go:659
msgid "retrieving recipes"
msgstr ""
@@ -4865,17 +4865,17 @@ msgstr ""
msgid "skipping secret (because it already exists) on %s: %s"
msgstr ""
#: ./pkg/recipe/git.go:419
#: ./pkg/recipe/git.go:413
#, c-format
msgid "skipping tag %s: checkout failed: %s"
msgstr ""
#: ./pkg/recipe/git.go:428
#: ./pkg/recipe/git.go:422
#, c-format
msgid "skipping tag %s: invalid compose config: %s"
msgstr ""
#: ./pkg/recipe/git.go:438
#: ./pkg/recipe/git.go:432
#, c-format
msgid "skipping tag %s: invalid image reference in service %s: %s"
msgstr ""
@@ -4978,7 +4978,7 @@ msgstr ""
msgid "succeeded"
msgstr ""
#: ./pkg/recipe/git.go:185
#: ./pkg/recipe/git.go:184
#, c-format
msgid "successfully checked %s out to %s in %s"
msgstr ""
@@ -5138,17 +5138,17 @@ msgstr ""
msgid "un"
msgstr ""
#: ./pkg/recipe/git.go:194
#: ./pkg/recipe/git.go:193
#, c-format
msgid "unable to check git clean status in %s: %s"
msgstr ""
#: ./pkg/recipe/git.go:263
#: ./pkg/recipe/git.go:262
#, c-format
msgid "unable to check out default branch in %s: %s"
msgstr ""
#: ./pkg/git/clone.go:98
#: ./pkg/git/clone.go:100
#, c-format
msgid "unable to clean up git clone of %s: %s"
msgstr ""
@@ -5222,7 +5222,7 @@ msgstr ""
msgid "unable to discover SSH remote for %s"
msgstr ""
#: ./pkg/recipe/git.go:274
#: ./pkg/recipe/git.go:268
#, c-format
msgid "unable to fetch tags in %s: %s"
msgstr ""
@@ -5232,7 +5232,7 @@ msgstr ""
msgid "unable to get container matching %s: %s"
msgstr ""
#: ./pkg/recipe/git.go:286
#: ./pkg/recipe/git.go:280
#, c-format
msgid "unable to git pull in %s: %s"
msgstr ""
@@ -5261,12 +5261,12 @@ msgstr ""
msgid "unable to look up server context for %s: %s"
msgstr ""
#: ./cli/recipe/fetch.go:77 ./pkg/git/read.go:26 ./pkg/lint/recipe.go:491 ./pkg/recipe/git.go:243
#: ./cli/recipe/fetch.go:77 ./pkg/git/read.go:26 ./pkg/lint/recipe.go:491 ./pkg/recipe/git.go:242
#, c-format
msgid "unable to open %s: %s"
msgstr ""
#: ./pkg/recipe/git.go:258
#: ./pkg/recipe/git.go:257
#, c-format
msgid "unable to open git work tree in %s: %s"
msgstr ""
@@ -5326,7 +5326,7 @@ msgstr ""
msgid "unable to read new env %s: %s"
msgstr ""
#: ./pkg/recipe/git.go:248
#: ./pkg/recipe/git.go:247
#, c-format
msgid "unable to read remotes in %s: %s"
msgstr ""
@@ -5361,7 +5361,7 @@ msgstr ""
msgid "unable to reset commit after failed release attempt: %s"
msgstr ""
#: ./pkg/recipe/git.go:167
#: ./pkg/recipe/git.go:166
#, c-format
msgid "unable to resolve '%s': %s"
msgstr ""
Binary file not shown.
+82 -144
View File
@@ -2,8 +2,8 @@ msgid ""
msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: EMAIL\n"
"POT-Creation-Date: 2026-10-14 10:32-0400\n"
"PO-Revision-Date: 2026-09-01 22:35+0000\n"
"POT-Creation-Date: 2026-06-14 17:56+0200\n"
"PO-Revision-Date: 2026-02-28 13:52+0000\n"
"Last-Translator: chasqui <chasqui@cryptolab.net>\n"
"Language-Team: Spanish <https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/>\n"
"Language: es\n"
@@ -113,25 +113,17 @@ msgid ""
" # list apps of all servers which match a specific recipe\n"
" abra app ls -r gitea"
msgstr ""
" OPCIONES Y FLAGS\n"
" ────────────────\n"
" ejemplos de uso:\n"
" # Listar las aplicaciones instaladas\n"
" abra aplicacion listar\n"
"\n"
"┌─ Información detallada ─────────────────────────────────────┐\n"
"│ $ abra aplicacion listar --estado │\n"
"└─────────────────────────────────────────────────────────────┘\n"
" # Listar las aplicaciones instaladas en un servidor específico\n"
" abra aplicacion listar -s 1312.net\n"
"\n"
"┌─ Aplicaciones de una receta ────────────────────────────────┐\n"
"│ $ abra aplicacion listar --receta wordpress │\n"
"└─────────────────────────────────────────────────────────────┘\n"
" # Listar aplicaciones instaladas en servidor específico con detalles\n"
" abra aplicacion listar -s 1312.net -S\n"
"\n"
"┌─ Receta + información detallada ────────────────────────────┐\n"
"│ $ abra aplicacion listar --receta wordpress --estado │\n"
"└─────────────────────────────────────────────────────────────┘\n"
"\n"
"┌─ Forma abreviada ───────────────────────────────────────────┐\n"
"│ $ abra app ls -S │\n"
"└─────────────────────────────────────────────────────────────┘"
" # Listar en qué servidores está desplegada la aplicación \"gitea\"\n"
" abra aplicacion listar -r gitea"
#: cli/app/move.go:59
msgid ""
@@ -244,21 +236,7 @@ msgstr " abra aplicacion entorno listar \"nombre_app.dominio\""
#: cli/app/remove.go:45
msgid " abra app remove 1312.net"
msgstr ""
" EJEMPLOS DE USO\n"
" ────────────────\n"
"\n"
"┌─ Borrar una aplicación ─────────────────────────────────────┐\n"
"│ $ abra aplicacion borrar \"nombre_app.dominio\" │\n"
"└─────────────────────────────────────────────────────────────┘\n"
"\n"
"┌─ Borrar sin confirmación ───────────────────────────────────┐\n"
"│ $ abra aplicacion borrar \"nombre_app.dominio\" --forzar │\n"
"└─────────────────────────────────────────────────────────────┘\n"
"\n"
"┌─ Borrar sin confirmación usando comando abreviado ──────────┐\n"
"│ $ abra app rm \"nombre_app.dominio\" -f │\n"
"└─────────────────────────────────────────────────────────────┘"
msgstr " abra aplicacion borrar \"nombre_app.dominio\""
#: cli/app/secret.go:346
msgid " abra app secret rm 1312.net oauth_key"
@@ -314,7 +292,7 @@ msgstr "%d volumen(es) eliminados con éxito"
msgid "%d volumes removed successfully"
msgstr "%d volúmenes eliminados correctamente"
#: pkg/recipe/git.go:198
#: pkg/recipe/git.go:197
#, c-format
msgid "%s (%s) has locally unstaged changes?"
msgstr "%s (%s) ¿tiene cambios locales?"
@@ -535,7 +513,7 @@ msgstr "%s eliminado del almacén de contraseñas"
#: cli/app/new.go:224
#, c-format
msgid "%s requires secret generation before deploy, run \"abra app secret generate %s --all\""
msgstr "%s requiere generación de secretos antes del despliegue, ejecuta \"abra aplicacion secreto generar %s --todos\""
msgstr "%s requiere generación de secretos antes del despliegue, ejecuta \"abra aplicacion secreto generar %s --all\""
#: cli/app/new.go:228
#, c-format
@@ -547,7 +525,7 @@ msgstr "%s requiere inserción de secretos antes del despliegue (#generate=false
msgid "%s sanitised as %s for new app"
msgstr "%s sanitisado como %s para nueva aplicación"
#: pkg/recipe/git.go:451
#: pkg/recipe/git.go:445
#, c-format
msgid "%s service is missing image tag?"
msgstr "¿falta la etiqueta de imagen del servicio %s?"
@@ -1130,25 +1108,9 @@ msgid ""
"\n"
"Use \"--status/-S\" flag to query all servers for the live deployment status."
msgstr ""
"┌──────────────────────────────────────────────────────────────┐\n"
"│ MANUAL DE USO · COMANDO «LISTAR» │\n"
"└──────────────────────────────────────────────────────────────┘\n"
" DESCRIPCIÓN\n"
" ───────────\n"
" El comando:\n"
"Genera un informe de todas las aplicaciones gestionadas.\n"
"\n"
" abra aplicacion listar\n"
"\n"
"Permite consultar rápidamente qué aplicaciones están siendo\n"
"gestionadas por Abra.\n"
"\n"
"Al ejecutarlo, Abra genera un informe con la lista de\n"
"aplicaciones disponibles y la información básica de cada una.\n"
"\n"
"En pocas palabras, puedes imaginar `listar` como una forma\n"
"de decirle a Abra:\n"
"\n"
" «muéstrame qué aplicaciones tengo»"
"Usa la opción \"--estado/-S\" para consultar en todos los servidores sobre el estado de despliegue."
#: cli/app/new.go:321
msgid "Generate app secrets?"
@@ -1174,7 +1136,7 @@ msgstr "Genera 🥷 secretos (contraseñas) automáticamente"
msgid "Generate the recipe catalogue"
msgstr "📋 Muestra el catálogo de 📜 recetas"
#: pkg/recipe/git.go:465
#: pkg/recipe/git.go:459
#, c-format
msgid "GetRecipeVersions encountered error for %s: %s (collected %d versions)"
msgstr "GetRecipeVersions encontró un error para %s: %s (recopiladas %d versiones)"
@@ -1516,48 +1478,24 @@ msgid ""
"To delete everything without prompt, use the \"--force/-f\" or the \"--no-input/n\"\n"
"flag."
msgstr ""
"┌──────────────────────────────────────────────────────────────┐\n"
"│ MANUAL DE USO · COMANDO «BORRAR» │\n"
"└──────────────────────────────────────────────────────────────┘\n"
" DESCRIPCIÓN\n"
" ───────────\n"
" El comando:\n"
"Elimina todo lo relacionado con una aplicación que ya está plegada.\n"
"\n"
" abra aplicacion borrar \"nombre_app.dominio\"\n"
"Por defecto, pedirá confirmación antes de continuar. Se borrarán \n"
"todos los secretos, volúmenes y el archivo .env local de la aplicación.\n"
"\n"
"elimina todo lo relacionado con una aplicación que ya está\n"
"desplegada.\n"
"Ejecuta este comando solo si estás seguro de querer quitar por \n"
"completo la aplicación y todos sus datos asociados. Esta acción \n"
"es destructiva. ¡Ten cuidado!\n"
"\n"
"Por defecto, Abra pedirá confirmación antes de continuar.\n"
"Si quieres borrar volúmenes o secretos específicos, \n"
"usa los subcomandos de eliminación bajo \"aplicación volumen\" \n"
"y \"aplicación secreto\" en su lugar.\n"
"\n"
"La eliminación incluye:\n"
"Ten en cuenta que si borras el archivo .env local sin eliminar primero \n"
"volúmenes y secretos, Abra *no* podrá ayudarte a eliminarlos después.\n"
"\n"
" ├── secretos\n"
" ├── volúmenes\n"
" └── archivo .env local\n"
"\n"
"\n"
" ¡ATENCIÓN!\n"
" ──────────\n"
"Esta es una acción destructiva.\n"
"\n"
"La aplicación y todos los datos asociados serán eliminados.\n"
"Ejecuta este comando únicamente si estás seguro de querer\n"
"quitar la aplicación por completo.\n"
"\n"
"\n"
"Si quieres eliminar volúmenes o secretos específicos, utiliza\n"
"los subcomandos correspondientes:\n"
"\n"
" abra aplicacion volumen\n"
" abra aplicacion secreto\n"
"\n"
"\n"
" IMPORTANTE\n"
" ──────────\n"
"Si eliminas el archivo .env local sin eliminar primero los\n"
"volúmenes y secretos, Abra no podrá ayudarte a eliminarlos\n"
"después."
"Para borrar todo sin confirmación, usa la opción \"--forzar/-f\" \n"
"o la opción \"--no-input/-n\"."
#. translators: Short description for `app volume remove` command
#: cli/app/volume.go:90
@@ -2087,32 +2025,32 @@ msgid ""
"\n"
"Use \"{{.CommandPath}} [command] --help\" for more information about a command.{{end}}\n"
msgstr ""
"COMANDO ABREVIADO\n"
"─────────────────\n"
" {{range .Aliases}}{{.}}{{end}}\n"
"Uso:{{if .Runnable}}\n"
" {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}\n"
" {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}\n"
"\n"
"Comando:\n"
" {{.NameAndAliases}}{{end}}{{if .HasExample}}\n"
"\n"
"Ejemplos:\n"
" # Nota: \"1312.net\" es solo un ejemplo de nombre de aplicación.\n"
"# Reemplázalo por el dominio o nombre real de tu aplicación.\n"
"\n"
"{{if .HasExample}}\n"
"{{.Example}}{{end}}{{if .HasAvailableSubCommands}}\n"
"\n"
"Comandos disponibles:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name \"help\"))}}\n"
" {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasHelpSubCommands}}\n"
" {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}\n"
"\n"
"Opciones:\n"
"{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}\n"
"\n"
"Opciones globales:\n"
"{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}\n"
"\n"
"Temas de ayuda adicionales:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}\n"
" {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}\n"
" {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}\n"
"\n"
" SINTAXIS\n"
" ────────{{if .Runnable}}\n"
" {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}\n"
" {{.CommandPath}} [command]{{end}}\n"
"\n"
"Opciones:{{if .HasAvailableLocalFlags}}\n"
"{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\n"
"\n"
"Opciones globales:{{if .HasAvailableInheritedFlags}}\n"
"{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}\n"
"\n"
"{{if .HasAvailableSubCommands}}\n"
"Use \"{{.CommandPath}} [comando] --help\" para más información sobre un comando.{{end}}\n"
"Use \"{{.CommandPath}} [command] --help\" para más información sobre un comando.{{end}}\n"
#: cli/recipe/fetch.go:28
msgid "Using \"--force/-f\" Git syncs an existing recipe. It does not erase unstaged changes."
@@ -2410,7 +2348,7 @@ msgstr "¿A la aplicación le falta la etiqueta de imagen?"
msgid "are you sure?"
msgstr "¿Estás segurx?"
#: pkg/recipe/git.go:163
#: pkg/recipe/git.go:162
#, c-format
msgid "attempting to checkout '%s' as chaos commit"
msgstr "Intentando extraer '%s' como el commit de caos"
@@ -2553,7 +2491,7 @@ msgstr "no se pueden leer las recetas locales: %s"
msgid "cancelled"
msgstr "cancelado"
#: pkg/catalogue/catalogue.go:59 pkg/recipe/git.go:252
#: pkg/catalogue/catalogue.go:59 pkg/recipe/git.go:251
#, c-format
msgid "cannot ensure %s is up-to-date, no git remotes configured"
msgstr "no se puede asegurar que %s esté actualizado, no hay remotos de git configurados"
@@ -2568,7 +2506,7 @@ msgstr "no se puede encontrar la aplicación con el nombre %s"
msgid "cannot get label %s for %s"
msgstr "no se puede obtener la etiqueta %s para %s"
#: pkg/recipe/git.go:59
#: pkg/recipe/git.go:58
#, c-format
msgid "cannot redeploy previous chaos version (%s), did you mean to use \"--chaos\"?"
msgstr "no se puede redeplegar la versión anterior de caos (%s), ¿Era tu intención usar \"--caos\"?"
@@ -2728,7 +2666,7 @@ msgstr "tarea: Actualizar tags de una imagen"
msgid "cmd"
msgstr ""
#: pkg/recipe/git.go:476
#: pkg/recipe/git.go:470
#, c-format
msgid "collected %s for %s"
msgstr "colectado %s para %s"
@@ -3067,7 +3005,7 @@ msgstr "tiempo de espera agotado 🟠"
msgid "destination directory does not exist"
msgstr "el directorio de destino no existe"
#: pkg/recipe/git.go:379
#: pkg/recipe/git.go:373
#, c-format
msgid "detected %s as tags for recipe %s"
msgstr "detectado(s) %s como etiquetas para la receta %s"
@@ -3264,7 +3202,7 @@ msgstr "asegúrate que \"image: ...\" esté establecido en todos los servicios"
msgid "ensure recipe: %s"
msgstr "asegura receta: %s"
#: pkg/recipe/git.go:57
#: pkg/recipe/git.go:56
#, c-format
msgid "ensuring env version %s"
msgstr "asegurando la versión del entorno (env) %s"
@@ -3366,12 +3304,12 @@ msgstr "🛑 error al agregar notas de la versión: %s"
msgid "failed to check git status of %s: %s"
msgstr "🛑 error al comprobar el estado de git de %s: %s"
#: pkg/git/branch.go:95 pkg/recipe/git.go:232
#: pkg/git/branch.go:95 pkg/recipe/git.go:231
#, c-format
msgid "failed to check out %s in %s"
msgstr "🛑 error al cambiar a %s en %s"
#: pkg/recipe/git.go:418
#: pkg/recipe/git.go:412
#, c-format
msgid "failed to check out %s in %s: %s"
msgstr "🛑 error al cambiar a %s en %s: %s"
@@ -3421,7 +3359,7 @@ msgstr "🛑 error al crear el volumen %s en %s: %s"
msgid "failed to generate random bytes: %w"
msgstr "🛑 error al generar bytes aleatorios: %w"
#: pkg/recipe/git.go:427
#: pkg/recipe/git.go:421
#, c-format
msgid "failed to get compose config for %s: %s"
msgstr "🛑 error al obtener la configuración para %s: %s"
@@ -3459,7 +3397,7 @@ msgstr "🛑 error al migrar la configuración de la aplicación: %s"
msgid "failed to parse image %s, saw: %s"
msgstr "🛑 error al analizar la imagen %s; detectado: %s"
#: pkg/recipe/git.go:437
#: pkg/recipe/git.go:431
#, c-format
msgid "failed to parse image for %s in %s: %s"
msgstr "🛑 error al analizar la imagen %s; en: %s: %s"
@@ -3567,7 +3505,7 @@ msgstr "obtener [receta | --todos] [opciones]"
msgid "fetch all recipes"
msgstr "Obtener todas las recetas"
#: pkg/catalogue/catalogue.go:84 pkg/recipe/git.go:290
#: pkg/catalogue/catalogue.go:84 pkg/recipe/git.go:284
#, c-format
msgid "fetched latest git changes for %s"
msgstr "Se obtuvieron los últimos cambios de Git para %s"
@@ -3687,12 +3625,12 @@ msgstr "cambios de git confirmados"
msgid "git changes pushed"
msgstr "cambios de git enviados (push)"
#: pkg/recipe/git.go:423
#: pkg/recipe/git.go:417
#, c-format
msgid "git checkout: %s in %s"
msgstr ""
#: pkg/git/clone.go:63 pkg/git/clone.go:100
#: pkg/git/clone.go:64 pkg/git/clone.go:102
#, c-format
msgid "git clone %s: cancelled due to interrupt"
msgstr "git clone %s: cancelado por interrupción"
@@ -3702,17 +3640,17 @@ msgstr "git clone %s: cancelado por interrupción"
msgid "git clone: %s"
msgstr ""
#: pkg/git/clone.go:87
#: pkg/git/clone.go:89
#, c-format
msgid "git clone: %s already exists"
msgstr "git clone: %s ya existe"
#: pkg/git/clone.go:58 pkg/git/clone.go:76 pkg/git/clone.go:85
#: pkg/git/clone.go:59 pkg/git/clone.go:78 pkg/git/clone.go:87
#, c-format
msgid "git clone: %s cloned successfully"
msgstr "git clone: %s clonado correctamente"
#: pkg/git/clone.go:67
#: pkg/git/clone.go:68
msgid "git clone: main branch failed, attempting master branch"
msgstr "git clone: error en la rama principal; intentando la rama master"
@@ -3767,7 +3705,7 @@ msgstr "git-nombre"
msgid "git.coopcloud.tech repo exists"
msgstr "el repositorio git.coopcloud.tech existe"
#: pkg/recipe/git.go:390
#: pkg/recipe/git.go:384
#, c-format
msgid "git: opening repository in %s"
msgstr "git: abriendo repositorio en %s"
@@ -4705,7 +4643,7 @@ msgstr "mostrar salida legible por máquina"
msgid "proceed?"
msgstr "¿procedemos?"
#: pkg/recipe/git.go:410
#: pkg/recipe/git.go:404
#, c-format
msgid "processing %s for %s"
msgstr "procesando %s para %s"
@@ -4775,7 +4713,7 @@ msgstr ""
msgid "re"
msgstr ""
#: pkg/recipe/git.go:158
#: pkg/recipe/git.go:157
#, c-format
msgid "read %s as tags for recipe %s"
msgstr "leer %s como etiquetas para la receta %s"
@@ -5125,7 +5063,7 @@ msgstr "versiones recuperadas del repositorio local de recetas"
msgid "retrieving docker auth token: failed create docker cli: %s"
msgstr "recuperando token de autenticación de Docker: error al crear docker cli: %s"
#: pkg/recipe/recipe.go:656
#: pkg/recipe/recipe.go:659
msgid "retrieving recipes"
msgstr "obteniendo 📜 recetas"
@@ -5521,17 +5459,17 @@ msgstr "omitiendo generación de %s (generate=false)"
msgid "skipping secret (because it already exists) on %s: %s"
msgstr "omitiendo secreto (porque ya existe) en %s: %s"
#: pkg/recipe/git.go:419
#: pkg/recipe/git.go:413
#, c-format
msgid "skipping tag %s: checkout failed: %s"
msgstr "omitiendo la etiqueta %s: error en el checkout: %s"
#: pkg/recipe/git.go:428
#: pkg/recipe/git.go:422
#, c-format
msgid "skipping tag %s: invalid compose config: %s"
msgstr "omitiendo la etiqueta %s: configuración de compose inválida: %s"
#: pkg/recipe/git.go:438
#: pkg/recipe/git.go:432
#, c-format
msgid "skipping tag %s: invalid image reference in service %s: %s"
msgstr "omitiendo la etiqueta %s: referencia de imagen inválida en el servicio %s: %s"
@@ -5634,7 +5572,7 @@ msgstr "recortado %s a %s para el análisis"
msgid "succeeded"
msgstr "exitoso"
#: pkg/recipe/git.go:185
#: pkg/recipe/git.go:184
#, c-format
msgid "successfully checked %s out to %s in %s"
msgstr "se comprobó correctamente %s de %s en %s"
@@ -5796,17 +5734,17 @@ msgstr ""
msgid "un"
msgstr ""
#: pkg/recipe/git.go:194
#: pkg/recipe/git.go:193
#, c-format
msgid "unable to check git clean status in %s: %s"
msgstr "no se pudo comprobar el estado de git clean en %s: %s"
#: pkg/recipe/git.go:263
#: pkg/recipe/git.go:262
#, c-format
msgid "unable to check out default branch in %s: %s"
msgstr "no se pudo cambiar a la rama predeterminada en %s: %s"
#: pkg/git/clone.go:98
#: pkg/git/clone.go:100
#, c-format
msgid "unable to clean up git clone of %s: %s"
msgstr "no se pudo limpiar la clonación de Git de %s: %s"
@@ -5880,7 +5818,7 @@ msgstr "no se pudieron determinar las reglas de versionado de %s; listando todas
msgid "unable to discover SSH remote for %s"
msgstr "no se pudo detectar el remoto SSH para %s"
#: pkg/recipe/git.go:274
#: pkg/recipe/git.go:268
#, c-format
msgid "unable to fetch tags in %s: %s"
msgstr "no se pudieron obtener las etiquetas en %s: %s"
@@ -5890,7 +5828,7 @@ msgstr "no se pudieron obtener las etiquetas en %s: %s"
msgid "unable to get container matching %s: %s"
msgstr "no se pudo obtener el contenedor que coincide con %s: %s"
#: pkg/recipe/git.go:286
#: pkg/recipe/git.go:280
#, c-format
msgid "unable to git pull in %s: %s"
msgstr "no se pudo hacer git pull en %s: %s"
@@ -5920,12 +5858,12 @@ msgid "unable to look up server context for %s: %s"
msgstr "no se pudo obtener el contexto del servidor para %s: %s"
#: cli/recipe/fetch.go:77 pkg/git/read.go:26 pkg/lint/recipe.go:491
#: pkg/recipe/git.go:243
#: pkg/recipe/git.go:242
#, c-format
msgid "unable to open %s: %s"
msgstr "no se pudo abrir %s: %s"
#: pkg/recipe/git.go:258
#: pkg/recipe/git.go:257
#, c-format
msgid "unable to open git work tree in %s: %s"
msgstr "no se pudo abrir el git work tree en %s: %s"
@@ -5985,7 +5923,7 @@ msgstr "no se pudo consultar el estado de %s: %s"
msgid "unable to read new env %s: %s"
msgstr "no se pudo leer el nuevo env %s: %s"
#: pkg/recipe/git.go:248
#: pkg/recipe/git.go:247
#, c-format
msgid "unable to read remotes in %s: %s"
msgstr "no se pudieron leer los remotos en %s: %s"
@@ -6021,7 +5959,7 @@ msgstr "no se pudo renderizar a JSON: %s"
msgid "unable to reset commit after failed release attempt: %s"
msgstr "no se pudo establecer los streams de IO como terminal raw: %s"
#: pkg/recipe/git.go:167
#: pkg/recipe/git.go:166
#, c-format
msgid "unable to resolve '%s': %s"
msgstr "no se pudo resolver '%s': %s"
+1 -7
View File
@@ -16,7 +16,6 @@ import (
"coopcloud.tech/tagcmp"
"github.com/distribution/reference"
"github.com/go-git/go-git/v5"
gitCfg "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
)
@@ -263,12 +262,7 @@ func (r Recipe) EnsureUpToDate() error {
return errors.New(i18n.G("unable to check out default branch in %s: %s", r.Dir, err))
}
// the refspec is passed explicitly, because a repository cloned by an older abra has a
// single-branch refspec stored in its config and would otherwise never see other branches
fetchOpts := &git.FetchOptions{
Tags: git.AllTags,
RefSpecs: []gitCfg.RefSpec{"+refs/heads/*:refs/remotes/origin/*"},
}
fetchOpts := &git.FetchOptions{Tags: git.AllTags}
if err := repo.Fetch(fetchOpts); err != nil {
if !strings.Contains(err.Error(), "already up-to-date") {
return errors.New(i18n.G("unable to fetch tags in %s: %s", r.Dir, err))
+18 -15
View File
@@ -592,7 +592,15 @@ func ReadReposMetadata(debug bool) (RepoCatalogue, error) {
// sortRecipeVersions sorts the recipe semver versions
func sortRecipeVersions(versions RecipeVersions) {
sort.Slice(versions, func(i, j int) bool {
return safeCompareVersions(getVersionString(versions[i]), getVersionString(versions[j]))
version1, err := tagcmp.Parse(getVersionString(versions[i]))
if err != nil {
panic(err)
}
version2, err := tagcmp.Parse(getVersionString(versions[j]))
if err != nil {
panic(err)
}
return version1.IsLessThan(version2)
})
}
@@ -608,23 +616,18 @@ func getVersionString(versionMap map[string]map[string]ServiceMeta) string {
// sortVersionStrings sorts a list of semver version strings
func sortVersionStrings(versions []string) {
sort.Slice(versions, func(i, j int) bool {
return safeCompareVersions(versions[i], versions[j])
version1, err := tagcmp.Parse(versions[i])
if err != nil {
panic(err)
}
version2, err := tagcmp.Parse(versions[j])
if err != nil {
panic(err)
}
return version1.IsLessThan(version2)
})
}
// Invalid versions are sorted as less than valid ones.
func safeCompareVersions(v1, v2 string) bool {
version1, err1 := tagcmp.Parse(v1)
if err1 != nil {
return true
}
version2, err2 := tagcmp.Parse(v2)
if err2 != nil {
return false
}
return version1.IsLessThan(version2)
}
// GetRecipeCatalogueVersions list the recipe versions listed in the recipe catalogue.
func GetRecipeCatalogueVersions(recipeName string, catl RecipeCatalogue) ([]string, error) {
var versions []string
-38
View File
@@ -123,41 +123,3 @@ func TestGetVersionLabelLocalDoesNotUseTimeoutLabel(t *testing.T) {
assert.NotEqual(t, label, timeout)
}
}
// This is not a full test of comparison, as that occurs in the external
// tagcmp package. This is a sunny-day integration test
func TestSortRecipeVersionsSortsTwoValidStrings(t *testing.T) {
versions := RecipeVersions{
{"2.0.0": {}}, // Index 0
{"1.0.0": {}}, // Index 1
}
sortRecipeVersions(versions)
assert.Equal(t, "1.0.0", getVersionString(versions[0]), "Version 1.0.0 should be first")
assert.Equal(t, "2.0.0", getVersionString(versions[1]), "Version 2.0.0 should be second")
}
func TestSortRecipeVersionsSortsInvalidVersionStringToTheBottom(t *testing.T) {
versions := RecipeVersions{
{"2.0.0": {}}, // Index 0
{"invalid": {}}, // Index 1
}
sortRecipeVersions(versions)
assert.Equal(t, "invalid", getVersionString(versions[0]), "The invalid version should be first")
assert.Equal(t, "2.0.0", getVersionString(versions[1]), "Version 2.0.0 should be second")
}
func TestSortRecipeVersionsSortsInvalidVersionStringToTheBottomInReverse(t *testing.T) {
versions := RecipeVersions{
{"invalid": {}}, // Index 0
{"2.0.0": {}}, // Index 1
}
sortRecipeVersions(versions)
assert.Equal(t, "invalid", getVersionString(versions[0]), "The invalid version should be first")
assert.Equal(t, "2.0.0", getVersionString(versions[1]), "Version 2.0.0 should be second")
}
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
app:
image: nginx:1.31.8
image: nginx:1.31.2
secrets:
- test_pass_one
- test_pass_two
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# This file generates local DNS aliases for the client VM for integration testing
echo "generating hosts file..."
hosts_file_path="./tests/resources/extra_hosts"
echo "removing old file..."
rm -f $hosts_file_path
# the server domain
server_ip="10.0.0.3"
domain="abra.local"
echo "$server_ip $domain" >> $hosts_file_path
# static subdomains used in integration tests
subdomains=("gitea" "zammad" "custom-html" "foo" "foobar")
for subdomain in "${subdomains[@]}"
do
echo "$server_ip $subdomain.$domain" >> $hosts_file_path
done
search_dir=./tests/integration
for entry in "$search_dir"/*.bats
do
file_name=$(basename "${entry}")
# converting file names to testing subdomains
subdomain=$(echo "$file_name" | tr . _)
echo "$server_ip $subdomain.$domain" >> $hosts_file_path
done
echo "writing extra hosts success"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# ignore changes to path_to_repo
git update-index --assume-unchanged ./tests/resources/path_to_repo
# write path to repo
pwd > ./tests/resources/path_to_repo
# create or update a VM, start it and wait for successful ping
start_vm () {
if [ -d "/var/lib/microvms/abra-$1" ]; then
make "vm-$1-update"
else
make "vm-$1-create"
fi
make "vm-$1-start"
host_available=0
while [ $host_available == 0 ]; do
if ping -c 1 "$2" &> /dev/null
then
host_available=1
else
echo "could not ping $1, waiting for 5 seconds..."
sleep 5
fi
done
}
start_vm "client" "10.0.0.2"
start_vm "server" "10.0.0.3"
read -rp "Press enter to run all tests or specify test files: " TEST_INTEGRATION_FILES
# -t option is needed for stdin output that is checked in integration tests
ssh -t abra@10.0.0.2 -i ./tests/resources/local_integration_ssh "cd abra; make test-integration TEST_INTEGRATION_FILES=$TEST_INTEGRATION_FILES"
+2 -1
View File
@@ -7,4 +7,5 @@
* Integration tests are in `./tests/integration`. Please see [these
docs](https://docs.coopcloud.tech/abra/hack/#integration-tests) for
instructions and tips on how to run them.
instructions and tips on how to run them. If you want to run it locally via nix,
read the [nix testing docs](../nix/hosts/README.md).
+9 -5
View File
@@ -598,8 +598,7 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
assert_success
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
_inspect_env_test_app
assert_success
assert_output --partial "WITH_COMMENT=foo"
@@ -610,8 +609,7 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input --force
assert_success
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
_inspect_env_test_app
assert_success
refute_output --partial "WITH_COMMENT=foo"
assert_output --partial "WITH_COMMENT=bar"
@@ -629,8 +627,14 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" --no-input
assert_success
run docker service inspect --format '{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}' \
if _is_local;
then
run docker service inspect --format "{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}" \
"${TEST_APP_DOMAIN//./_}_app"
else
run ssh "$TEST_SERVER" "docker service inspect --format '{{ range .Endpoint.Ports }}{{ .Protocol }}={{ .PublishedPort }}{{ end }}' ${TEST_APP_DOMAIN//./_}_app"
fi
assert_success
assert_output --partial "tcp=1312"
assert_output --partial "udp=1312"
@@ -133,8 +133,7 @@ teardown(){
run $ABRA app deploy "$TEST_APP_DOMAIN" "0.1.0+1.20.0" --no-input
assert_success
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
_inspect_env_test_app
assert_success
assert_output --partial "$TEST_RECIIPE:0.1.0+1.20.0"
}
-33
View File
@@ -122,39 +122,6 @@ teardown(){
assert_failure
}
@test "create new app with commit from another branch" {
branchHash=$(_get_other_branch_hash)
if [[ -z "$branchHash" ]]; then
skip "$TEST_RECIPE has no branch besides main"
fi
# re-clone the recipe the way an older abra did, with a single-branch refspec. --no-local
# forces a real transfer, a local clone would hardlink the whole object database and leave
# the commit reachable
run rm -rf "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
run git clone -q --no-local --single-branch --branch main \
"$ABRA_DIR/origin-recipes/$TEST_RECIPE.git" "$ABRA_DIR/recipes/$TEST_RECIPE"
assert_success
# the commit has to be genuinely missing, otherwise this test passes for the wrong reason
run git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-parse --verify "$branchHash^{commit}"
assert_failure
run $ABRA app new "$TEST_RECIPE" "$branchHash" \
--no-input \
--server "$TEST_SERVER" \
--domain "$TEST_APP_DOMAIN"
assert_success
assert_exists "$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
# the recipe names itself in TYPE, which differs per branch, so only the version is checked
run grep -q "TYPE=.*:${branchHash}$" \
"$ABRA_DIR/servers/$TEST_SERVER/$TEST_APP_DOMAIN.env"
assert_success
}
@test "does not overwrite existing env files" {
run $ABRA app new "$TEST_RECIPE" \
--no-input \
+11 -4
View File
@@ -140,15 +140,22 @@ teardown(){
_undeploy_app
sanitisedDomainName="${TEST_APP_DOMAIN//./_}"
run docker config create "${sanitisedDomainName}_test_conf_v99" "$ABRA_DIR/recipes/abra-test-recipe/abra.sh"
assert_success
remote_ssh_command=""
if _is_local;
then
run docker config create "${sanitisedDomainName}_test_conf_v99" "$ABRA_DIR/recipes/abra-test-recipe/abra.sh"
else
remote_ssh_command="ssh '$TEST_SERVER'"
run cat "$ABRA_DIR/recipes/abra-test-recipe/abra.sh" | ssh "$TEST_SERVER" docker config create "${sanitisedDomainName}_test_conf_v99 -"
assert_success
fi
assert bash -c "docker config ls | grep -q test_conf_v99"
assert bash -c "$remote_ssh_command docker config ls | grep -q test_conf_v99"
run $ABRA app rm "$TEST_APP_DOMAIN" --no-input
assert_success
refute bash -c "docker config ls | grep -q test_conf_v99"
refute bash -c "$remote_ssh_command docker config ls | grep -q test_conf_v99"
}
@test "remove .env file" {
@@ -54,8 +54,7 @@ teardown(){
--no-input
assert_success
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
_inspect_env_test_app
assert_success
assert_output --partial "$TEST_RECIIPE:0.1.0+1.20.0"
}
@@ -53,8 +53,7 @@ teardown(){
run $ABRA app upgrade "$TEST_APP_DOMAIN" "0.2.0+1.21.0" --no-input
assert_success
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' \
$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)
_inspect_env_test_app
assert_success
assert_output --partial "$TEST_RECIIPE:0.2.0+1.21.0"
assert_output --partial "$TEST_RECIPE:0.2.0+1.21.0"
}
+11 -2
View File
@@ -31,9 +31,18 @@ _ensure_ssh_agent() {
exit 1
fi
export SSH_AUTH_SOCK="$HOME/.ssh/ssh_auth_sock"
if [ ! -S ~/.ssh/ssh_auth_sock ]; then
if [[ ! -v SSH_AUTH_SOCK ]]; then
export SSH_AUTH_SOCK="$HOME/.ssh/ssh_auth_sock"
eval `ssh-agent`
ln -sf "$SSH_AUTH_SOCK" ~/.ssh/ssh_auth_sock
fi
}
_is_local() {
if [[ "$TEST_SERVER" == "default" ]];
then
return 0
else
return 1
fi
}
+30 -6
View File
@@ -1,13 +1,37 @@
#!/usr/bin/env bash
_ensure_swarm() {
if [ "$(docker info | grep Swarm | sed 's/Swarm: //g' | tr -d ' ')" == "inactive" ]; then
run docker swarm init --advertise-addr 127.0.0.1:2377
assert_success
if [ "$(docker info | grep Swarm | sed 's/Swarm: //g' | tr -d ' ')" == "inactive" ]; then
run docker swarm init --advertise-addr 127.0.0.1:2377
assert_success
fi
if ! $(docker network ls | grep -q 'proxy'); then
run docker network create -d overlay proxy
assert_success
if ! docker network ls | grep -q 'proxy'; then
run docker network create -d overlay proxy
assert_success
fi
if ! _is_local;
then
if [ "$(ssh "$TEST_SERVER" docker info | grep Swarm | sed 's/Swarm: //g' | tr -d ' ')" == "inactive" ]; then
run ssh "$TEST_SERVER" docker swarm init --advertise-addr 127.0.0.1:2377
assert_success
fi
if ! ssh "$TEST_SERVER" docker network ls | grep -q 'proxy'; then
run ssh "$TEST_SERVER" docker network create -d overlay proxy
assert_success
fi
fi
}
_inspect_env_test_app() {
if _is_local;
then
containerId="$(docker ps -f name="$TEST_APP_DOMAIN_$TEST_SERVER" -q)"
run docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' "$containerId"
else
containerId="$(ssh "$TEST_SERVER" docker ps -f name="$TEST_APP_DOMAIN" -q)"
run ssh "$TEST_SERVER" "docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' '$containerId'"
fi
}
-7
View File
@@ -56,13 +56,6 @@ _get_tag_hash() {
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" rev-list -n 1 "$1")
}
_get_other_branch_hash() {
# asked from the origin mirror, not from the recipe checkout, so that the result does not
# depend on what has been fetched. empty when the recipe only has a default branch
echo $(git ls-remote "$ABRA_DIR/origin-recipes/$TEST_RECIPE.git" \
| grep 'refs/heads/' | grep -v 'refs/heads/main$' | head -1 | cut -f1)
}
_get_head_hash() {
echo $(git -C "$ABRA_DIR/recipes/$TEST_RECIPE" show -s --format="%H" HEAD)
}
-2
View File
@@ -38,8 +38,6 @@ teardown(){
run $ABRA recipe upgrade "custom-html" --no-input
assert_success
assert_output --partial 'can upgrade service: app'
assert_exists "$ABRA_DIR/recipes/custom-html"
}
+3 -3
View File
@@ -27,7 +27,7 @@ teardown(){
assert bash -c "docker context ls | grep -q $TEST_SERVER"
server_dir_perms=$(stat -c "%a" "$ABRA_DIR/servers/$TEST_SERVER")
assert_equal $server_dir_perms "700"
assert_equal "$server_dir_perms" "700"
}
@test "error if using name and --local together" {
@@ -43,8 +43,8 @@ teardown(){
assert bash -c "docker context ls | grep -q default"
assert_output --partial 'local server successfully added'
server_dir_perms=$(stat -c "%a" "$ABRA_DIR/servers/$TEST_SERVER")
assert_equal $server_dir_perms "700"
server_dir_perms=$(stat -c "%a" "$ABRA_DIR/servers/default")
assert_equal "$server_dir_perms" "700"
}
@test "create local server fails when no docker swarm" {
+57
View File
@@ -0,0 +1,57 @@
10.0.0.3 abra.local
10.0.0.3 gitea.abra.local
10.0.0.3 zammad.abra.local
10.0.0.3 custom-html.abra.local
10.0.0.3 foo.abra.local
10.0.0.3 foobar.abra.local
10.0.0.3 app_check_bats.abra.local
10.0.0.3 app_cmd_bats.abra.local
10.0.0.3 app_config_bats.abra.local
10.0.0.3 app_cp_bats.abra.local
10.0.0.3 app_deploy_bats.abra.local
10.0.0.3 app_deploy_env_version_bats.abra.local
10.0.0.3 app_deploy_overview_bats.abra.local
10.0.0.3 app_deploy_remote_recipes_bats.abra.local
10.0.0.3 app_env_bats.abra.local
10.0.0.3 app_env_version_bats.abra.local
10.0.0.3 app_labels_bats.abra.local
10.0.0.3 app_list_bats.abra.local
10.0.0.3 app_logs_bats.abra.local
10.0.0.3 app_move_bats.abra.local
10.0.0.3 app_new_bats.abra.local
10.0.0.3 app_ps_bats.abra.local
10.0.0.3 app_remove_bats.abra.local
10.0.0.3 app_restart_bats.abra.local
10.0.0.3 app_rollback_bats.abra.local
10.0.0.3 app_rollback_env_version_bats.abra.local
10.0.0.3 app_rollback_overview_bats.abra.local
10.0.0.3 app_run_bats.abra.local
10.0.0.3 app_secret_bats.abra.local
10.0.0.3 app_secret_env_version_bats.abra.local
10.0.0.3 app_services_bats.abra.local
10.0.0.3 app_undeploy_bats.abra.local
10.0.0.3 app_undeploy_env_version_bats.abra.local
10.0.0.3 app_undeploy_overview_bats.abra.local
10.0.0.3 app_upgrade_bats.abra.local
10.0.0.3 app_upgrade_env_version_bats.abra.local
10.0.0.3 app_upgrade_overview_bats.abra.local
10.0.0.3 app_volume_bats.abra.local
10.0.0.3 autocomplete_bats.abra.local
10.0.0.3 catalogue_bats.abra.local
10.0.0.3 dirs_bats.abra.local
10.0.0.3 install_bats.abra.local
10.0.0.3 recipe_diff_bats.abra.local
10.0.0.3 recipe_fetch_bats.abra.local
10.0.0.3 recipe_lint_bats.abra.local
10.0.0.3 recipe_list_bats.abra.local
10.0.0.3 recipe_new_bats.abra.local
10.0.0.3 recipe_release_bats.abra.local
10.0.0.3 recipe_reset_bats.abra.local
10.0.0.3 recipe_upgrade_bats.abra.local
10.0.0.3 recipe_version_bats.abra.local
10.0.0.3 server_add_bats.abra.local
10.0.0.3 server_list_bats.abra.local
10.0.0.3 server_prune_bats.abra.local
10.0.0.3 server_remove_bats.abra.local
10.0.0.3 upgrade_bats.abra.local
10.0.0.3 version_bats.abra.local
+7
View File
@@ -0,0 +1,7 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
QyNTUxOQAAACBQ967UhxmFkq71WXvVRkEmtehCGsEnBVppAXxgtHHm7wAAAJhFGwTnRRsE
5wAAAAtzc2gtZWQyNTUxOQAAACBQ967UhxmFkq71WXvVRkEmtehCGsEnBVppAXxgtHHm7w
AAAEAnHEZcf2NeRQEcJC/aVgUWsdOz+vQgEG9ZY+3ErCeaKFD3rtSHGYWSrvVZe9VGQSa1
6EIawScFWmkBfGC0cebvAAAAFWFicmEgaW50ZWdyYXRpb24gdGVzdA==
-----END OPENSSH PRIVATE KEY-----
@@ -0,0 +1 @@
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFD3rtSHGYWSrvVZe9VGQSa16EIawScFWmkBfGC0cebv abra integration test
+1
View File
@@ -0,0 +1 @@
/path/to/this/repo
+1 -1
View File
@@ -3,7 +3,7 @@ version: "3.8"
services:
app:
image: nginx:1.31.8
image: nginx:1.31.2
networks:
- proxy
deploy:
+3 -3
View File
@@ -3,16 +3,16 @@ kind: pipeline
name: coopcloud.tech/tagcmp
steps:
- name: gofmt
image: golang:1.27
image: golang:1.26
commands:
- test -z "$(gofmt -l .)"
- name: go build
image: golang:1.27
image: golang:1.26
commands:
- go build -v .
- name: go test
image: golang:1.27
image: golang:1.26
commands:
- go test . -cover
@@ -18,7 +18,6 @@
// tag is deprecated and thus should not be used.
// Go versions prior to 1.4 are disabled because they use a different layout
// for interfaces which make the implementation of unsafeReflectValue more complex.
//go:build !js && !appengine && !safe && !disableunsafe && go1.4
// +build !js,!appengine,!safe,!disableunsafe,go1.4
package spew
@@ -16,7 +16,6 @@
// when the code is running on Google App Engine, compiled by GopherJS, or
// "-tags safe" is added to the go build command line. The "disableunsafe"
// tag is deprecated and thus should not be used.
//go:build js || appengine || safe || disableunsafe || !go1.4
// +build js appengine safe disableunsafe !go1.4
package spew
@@ -254,15 +254,15 @@ pointer addresses used to indirect to the final value. It provides the
following features over the built-in printing facilities provided by the fmt
package:
- Pointers are dereferenced and followed
- Circular data structures are detected and handled properly
- Custom Stringer/error interfaces are optionally invoked, including
on unexported types
- Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
- Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by modifying the public members
of c. See ConfigState for options documentation.
@@ -295,12 +295,12 @@ func (c *ConfigState) convertArgs(args []interface{}) (formatters []interface{})
// NewDefaultConfig returns a ConfigState with the following default settings.
//
// Indent: " "
// MaxDepth: 0
// DisableMethods: false
// DisablePointerMethods: false
// ContinueOnMethod: false
// SortKeys: false
// Indent: " "
// MaxDepth: 0
// DisableMethods: false
// DisablePointerMethods: false
// ContinueOnMethod: false
// SortKeys: false
func NewDefaultConfig() *ConfigState {
return &ConfigState{Indent: " "}
}
@@ -21,36 +21,35 @@ debugging.
A quick overview of the additional features spew provides over the built-in
printing facilities for Go data types are as follows:
- Pointers are dereferenced and followed
- Circular data structures are detected and handled properly
- Custom Stringer/error interfaces are optionally invoked, including
on unexported types
- Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
- Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output (only when using
Dump style)
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output (only when using
Dump style)
There are two different approaches spew allows for dumping Go data structures:
- Dump style which prints with newlines, customizable indentation,
and additional debug information such as types and all pointer addresses
used to indirect to the final value
- A custom Formatter interface that integrates cleanly with the standard fmt
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
similar to the default %v while providing the additional functionality
outlined above and passing unsupported format verbs such as %x and %q
along to fmt
* Dump style which prints with newlines, customizable indentation,
and additional debug information such as types and all pointer addresses
used to indirect to the final value
* A custom Formatter interface that integrates cleanly with the standard fmt
package and replaces %v, %+v, %#v, and %#+v to provide inline printing
similar to the default %v while providing the additional functionality
outlined above and passing unsupported format verbs such as %x and %q
along to fmt
# Quick Start
Quick Start
This section demonstrates how to quickly get started with spew. See the
sections below for further details on formatting and configuration options.
To dump a variable with full newlines, indentation, type, and pointer
information use Dump, Fdump, or Sdump:
spew.Dump(myVar1, myVar2, ...)
spew.Fdump(someWriter, myVar1, myVar2, ...)
str := spew.Sdump(myVar1, myVar2, ...)
@@ -59,13 +58,12 @@ Alternatively, if you would prefer to use format strings with a compacted inline
printing style, use the convenience wrappers Printf, Fprintf, etc with
%v (most compact), %+v (adds pointer addresses), %#v (adds types), or
%#+v (adds types and pointer addresses):
spew.Printf("myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Printf("myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
spew.Fprintf(someWriter, "myVar1: %v -- myVar2: %+v", myVar1, myVar2)
spew.Fprintf(someWriter, "myVar3: %#v -- myVar4: %#+v", myVar3, myVar4)
# Configuration Options
Configuration Options
Configuration of spew is handled by fields in the ConfigState type. For
convenience, all of the top-level functions use a global state available
@@ -76,52 +74,51 @@ equivalent to the top-level functions. This allows concurrent configuration
options. See the ConfigState documentation for more details.
The following configuration options are available:
* Indent
String to use for each indentation level for Dump functions.
It is a single space by default. A popular alternative is "\t".
- Indent
String to use for each indentation level for Dump functions.
It is a single space by default. A popular alternative is "\t".
* MaxDepth
Maximum number of levels to descend into nested data structures.
There is no limit by default.
- MaxDepth
Maximum number of levels to descend into nested data structures.
There is no limit by default.
* DisableMethods
Disables invocation of error and Stringer interface methods.
Method invocation is enabled by default.
- DisableMethods
Disables invocation of error and Stringer interface methods.
Method invocation is enabled by default.
* DisablePointerMethods
Disables invocation of error and Stringer interface methods on types
which only accept pointer receivers from non-pointer variables.
Pointer method invocation is enabled by default.
- DisablePointerMethods
Disables invocation of error and Stringer interface methods on types
which only accept pointer receivers from non-pointer variables.
Pointer method invocation is enabled by default.
* DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
- DisablePointerAddresses
DisablePointerAddresses specifies whether to disable the printing of
pointer addresses. This is useful when diffing data structures in tests.
* DisableCapacities
DisableCapacities specifies whether to disable the printing of
capacities for arrays, slices, maps and channels. This is useful when
diffing data structures in tests.
- DisableCapacities
DisableCapacities specifies whether to disable the printing of
capacities for arrays, slices, maps and channels. This is useful when
diffing data structures in tests.
* ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default.
- ContinueOnMethod
Enables recursion into types after invoking error and Stringer interface
methods. Recursion after method invocation is disabled by default.
* SortKeys
Specifies map keys should be sorted before being printed. Use
this to have a more deterministic, diffable output. Note that
only native types (bool, int, uint, floats, uintptr and string)
and types which implement error or Stringer interfaces are
supported with other types sorted according to the
reflect.Value.String() output which guarantees display
stability. Natural map order is used by default.
- SortKeys
Specifies map keys should be sorted before being printed. Use
this to have a more deterministic, diffable output. Note that
only native types (bool, int, uint, floats, uintptr and string)
and types which implement error or Stringer interfaces are
supported with other types sorted according to the
reflect.Value.String() output which guarantees display
stability. Natural map order is used by default.
* SpewKeys
Specifies that, as a last resort attempt, map keys should be
spewed to strings and sorted by those strings. This is only
considered if SortKeys is true.
- SpewKeys
Specifies that, as a last resort attempt, map keys should be
spewed to strings and sorted by those strings. This is only
considered if SortKeys is true.
# Dump Usage
Dump Usage
Simply call spew.Dump with a list of variables you want to dump:
@@ -136,7 +133,7 @@ A third option is to call spew.Sdump to get the formatted output as a string:
str := spew.Sdump(myVar1, myVar2, ...)
# Sample Dump Output
Sample Dump Output
See the Dump example for details on the setup of the types and variables being
shown here.
@@ -153,14 +150,13 @@ shown here.
Byte (and uint8) arrays and slices are displayed uniquely like the hexdump -C
command as shown.
([]uint8) (len=32 cap=32) {
00000000 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 |............... |
00000010 21 22 23 24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 |!"#$%&'()*+,-./0|
00000020 31 32 |12|
}
# Custom Formatter
Custom Formatter
Spew provides a custom formatter that implements the fmt.Formatter interface
so that it integrates cleanly with standard fmt package printing functions. The
@@ -174,7 +170,7 @@ standard fmt package for formatting. In addition, the custom formatter ignores
the width and precision arguments (however they will still work on the format
specifiers not handled by the custom formatter).
# Custom Formatter Usage
Custom Formatter Usage
The simplest way to make use of the spew custom formatter is to call one of the
convenience functions such as spew.Printf, spew.Println, or spew.Printf. The
@@ -188,17 +184,15 @@ functions have syntax you are most likely already familiar with:
See the Index for the full list convenience functions.
# Sample Formatter Output
Sample Formatter Output
Double pointer to a uint8:
%v: <**>5
%+v: <**>(0xf8400420d0->0xf8400420c8)5
%#v: (**uint8)5
%#+v: (**uint8)(0xf8400420d0->0xf8400420c8)5
Pointer to circular struct with a uint8 field and a pointer to itself:
%v: <*>{1 <*><shown>}
%+v: <*>(0xf84003e260){ui8:1 c:<*>(0xf84003e260)<shown>}
%#v: (*main.circular){ui8:(uint8)1 c:(*main.circular)<shown>}
@@ -207,7 +201,7 @@ Pointer to circular struct with a uint8 field and a pointer to itself:
See the Printf example for details on the setup of variables being shown
here.
# Errors
Errors
Since it is possible for custom Stringer/error interfaces to panic, spew
detects them and handles them internally by printing the panic information
@@ -488,15 +488,15 @@ pointer addresses used to indirect to the final value. It provides the
following features over the built-in printing facilities provided by the fmt
package:
- Pointers are dereferenced and followed
- Circular data structures are detected and handled properly
- Custom Stringer/error interfaces are optionally invoked, including
on unexported types
- Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
- Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
* Pointers are dereferenced and followed
* Circular data structures are detected and handled properly
* Custom Stringer/error interfaces are optionally invoked, including
on unexported types
* Custom types which only implement the Stringer/error interfaces via
a pointer receiver are optionally invoked when passing non-pointer
variables
* Byte arrays and slices are dumped like the hexdump -C command which
includes offsets, byte values in hex, and ASCII output
The configuration options are controlled by an exported package global,
spew.Config. See ConfigState for options documentation.
-1
View File
@@ -5,4 +5,3 @@ profile.out
.tmp/
.git-dist/
.vscode
build/tools/
-4
View File
@@ -329,10 +329,6 @@ func (c *Config) unmarshalCore() {
if parsed := parseConfigBool(s.Options.Get(protectHFSKey)); parsed.IsSet() {
c.Core.ProtectHFS = parsed
}
if s.Options.Get(repositoryFormatVersionKey) == string(format.Version_1) {
c.Core.RepositoryFormatVersion = format.Version_1
}
}
func (c *Config) unmarshalUser() {
@@ -139,12 +139,8 @@ func (dw *deltaSelector) fixAndBreakChains(objectsToPack []*ObjectToPack) error
m[otp.Hash()] = otp
}
// visiting holds the objects on the current resolution path, so that a
// delta chain looping back on itself can be detected and broken.
visiting := make(map[plumbing.Hash]bool)
for _, otp := range objectsToPack {
if err := dw.fixAndBreakChainsOne(m, otp, visiting); err != nil {
if err := dw.fixAndBreakChainsOne(m, otp); err != nil {
return err
}
}
@@ -152,11 +148,7 @@ func (dw *deltaSelector) fixAndBreakChains(objectsToPack []*ObjectToPack) error
return nil
}
func (dw *deltaSelector) fixAndBreakChainsOne(
objectsToPack map[plumbing.Hash]*ObjectToPack,
otp *ObjectToPack,
visiting map[plumbing.Hash]bool,
) error {
func (dw *deltaSelector) fixAndBreakChainsOne(objectsToPack map[plumbing.Hash]*ObjectToPack, otp *ObjectToPack) error {
if !otp.Object.Type().IsDelta() {
return nil
}
@@ -182,21 +174,7 @@ func (dw *deltaSelector) fixAndBreakChainsOne(
return dw.undeltify(otp)
}
// Mark this object as being resolved before looking at its base, so that
// a delta based on itself is caught by the check below.
h := otp.Hash()
visiting[h] = true
defer delete(visiting, h)
// A delta chain that loops back onto an object we are already resolving
// cannot be written: every delta needs its base written first. Break the
// chain here instead of following the cycle, which would recurse until
// the goroutine stack is exhausted.
if visiting[do.BaseHash()] {
return dw.undeltify(otp)
}
if err := dw.fixAndBreakChainsOne(objectsToPack, base, visiting); err != nil {
if err := dw.fixAndBreakChainsOne(objectsToPack, base); err != nil {
return err
}
+2 -34
View File
@@ -474,14 +474,6 @@ type TreeWalker struct {
recursive bool
seen map[plumbing.Hash]bool
// skipPathValidation disables the pathutil.ValidTreePath check in Next.
// It is set by inspection-only callers (e.g. the revlist object walk)
// that never funnel entry names into the filesystem and must enumerate
// trees faithfully, including entries with names upstream Git accepts
// but that are unsafe to materialise (control characters, `.git`-shaped
// names).
skipPathValidation bool
s storer.EncodedObjectStorer
t *Tree
}
@@ -504,32 +496,10 @@ func NewTreeWalker(t *Tree, recursive bool, seen map[plumbing.Hash]bool) *TreeWa
}
}
// SkipPathValidation disables the pathutil.ValidTreePath check performed by
// Next, and must be called before the first Next.
//
// It is for inspection-only walks that never funnel an entry name into the
// filesystem — enumerating which objects exist, rather than materialising
// them — and that must therefore see the tree faithfully, including entries
// whose names upstream Git accepts but that are unsafe to check out. Callers
// that hand the returned name to filesystem or archive output must not use
// it; path safety for those is enforced at the materialisation boundaries
// (FindEntry, TreeEntryFile, archive, FileIter).
func (w *TreeWalker) SkipPathValidation() {
w.skipPathValidation = true
}
// Next returns the next object from the tree. Objects are returned in order
// and subtrees are included. After the last object has been returned further
// calls to Next() will return io.EOF.
//
// Each entry's name is validated against pathutil.ValidTreePath as it
// surfaces, so callers that funnel the returned name into filesystem
// or archive output can trust it is free of `.git`-shaped components,
// HFS+/NTFS variants, Windows reserved names, and traversal sequences.
// A malformed entry stops the walk with the validator's error;
// inspection-only callers that need to enumerate raw, unvalidated
// names can read Tree.Entries directly or call SkipPathValidation.
//
// In the current implementation any objects which cannot be found in the
// underlying repository will be skipped automatically. It is possible that this
// may change in future versions.
@@ -566,10 +536,8 @@ func (w *TreeWalker) Next() (name string, entry TreeEntry, err error) {
continue
}
if !w.skipPathValidation {
if err := pathutil.ValidTreePath(entry.Name); err != nil {
return name, entry, err
}
if err := pathutil.ValidTreePath(entry.Name); err != nil {
return name, entry, err
}
if entry.Mode == filemode.Dir {
-6
View File
@@ -106,12 +106,6 @@ func transformChildren(t *Tree) ([]noder.Noder, error) {
ret := make([]noder.Noder, 0, len(t.Entries))
walker := NewTreeWalker(t, false, nil) // don't recurse
// The diff walk is read-only and never materialises entry names into the
// filesystem, so it must enumerate the tree faithfully — including entries
// with names that are unsafe to check out but valid per upstream Git (e.g.
// control characters). Path safety is enforced at materialisation
// boundaries (FindEntry, TreeEntryFile, archive, FileIter), not here.
walker.SkipPathValidation()
// don't defer walker.Close() for efficiency reasons.
for {
_, e, err = walker.Next()
-42
View File
@@ -110,48 +110,6 @@ func (r ReferenceName) IsTag() bool {
return strings.HasPrefix(string(r), refTagPrefix)
}
// IsSafe reports whether the reference name can be safely turned into a path
// under the .git directory, mirroring Git's refname_is_safe (refs.c). A name
// is safe when it is either:
//
// - under "refs/", non-empty after the prefix, containing no backslash and
// no empty, "." or ".." path component (so it cannot escape the refs/
// sub-tree, or alias another name, once turned into a path); or
// - a one-level pseudo-ref whose spelling is restricted to [A-Z_]
// (e.g. HEAD, ORIG_HEAD, FETCH_HEAD).
//
// Everything else — a lowercase or mixed one-level name such as "config" or
// "index", an absolute or drive-prefixed name, or a refs/ name that escapes —
// is unsafe, because it could resolve onto unrelated repository metadata.
func (r ReferenceName) IsSafe() bool {
s := string(r)
if s == "" {
return false
}
if rest, ok := strings.CutPrefix(s, refPrefix); ok {
// '\' is a path separator on Windows, so a refs/ name containing one
// could escape the sub-tree or alias another name once turned into a
// path; reject it outright (check_refname_format forbids '\' too).
if rest == "" || strings.Contains(rest, "\\") {
return false
}
for part := range strings.SplitSeq(rest, "/") {
if part == "" || part == "." || part == ".." {
return false
}
}
return true
}
for i := 0; i < len(s); i++ {
if (s[i] < 'A' || s[i] > 'Z') && s[i] != '_' {
return false
}
}
return true
}
func (r ReferenceName) String() string {
return string(r)
}
-7
View File
@@ -187,13 +187,6 @@ func iterateCommitTrees(
cb(tree.Hash)
treeWalker := object.NewTreeWalker(tree, true, seen)
// This walk only enumerates which objects are reachable, to decide what
// to send; it never materialises an entry name into the filesystem. It
// must therefore enumerate the tree faithfully, including entries with
// names that are unsafe to check out but valid per upstream Git (e.g.
// control characters). Path safety is enforced at materialisation
// boundaries (FindEntry, TreeEntryFile, archive, FileIter), not here.
treeWalker.SkipPathValidation()
for {
_, e, err := treeWalker.Next()
+22 -379
View File
@@ -6,12 +6,8 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"reflect"
"strconv"
@@ -77,7 +73,7 @@ func advertisedReferences(ctx context.Context, s *session, serviceName string) (
s.endpoint.String(), infoRefsPath, serviceName,
)
req, err := newRequest(http.MethodGet, url, nil)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
@@ -181,24 +177,6 @@ func NewClient(c *http.Client) transport.Transport {
// and other custom options specific to the client.
// If the net/http client is nil or empty, it will use a net/http client configured
// with http.DefaultTransport.
//
// Credentials this client adds where the transport cannot see them are not
// subject to the redirect stripping described on AuthMethod: a RoundTripper
// injects after the hop is decided, and Client.Jar is consulted after
// CheckRedirect, so a domain cookie still follows a redirect to a subdomain
// the transport counts as another origin. Apply them in an AuthMethod instead
// if that is not wanted. A CheckRedirect hook set on this client runs
// alongside the transport's own, but any header it adds when a redirect
// leaves the repository's origin is discarded the same way.
//
// A RoundTripper is therefore also how to authenticate to a new origin a
// redirect has moved the repository to: match on the request URL and inject
// the credential only for that origin, so it is not sent anywhere else. The
// transport keeps its own CheckRedirect on the copy it makes of this client,
// so the policy and the origin checks still apply.
//
// None of this applies to a RoundTripper that follows redirects itself:
// CheckRedirect is not consulted then, so no stripping happens at all.
func NewClientWithOptions(c *http.Client, opts *ClientOptions) transport.Transport {
if c == nil {
c = &http.Client{
@@ -392,20 +370,11 @@ func (s *session) ModifyEndpointIfRedirect(res *http.Response) error {
if !strings.HasSuffix(r.URL.Path, infoRefsPath) {
return fmt.Errorf("http redirect: target %q does not end with %s", r.URL.Path, infoRefsPath)
}
// A scheme is case-insensitive per RFC 3986, and checkRedirect folds case
// when it reads the same hop, so fold here too rather than reject a
// spelling that check let through. url.Parse and transport.NewEndpoint
// both lowercase what they parse, so only a hand-built URL or Endpoint
// arrives uppercased. The folded form is what gets stored below, so every
// later request built from the endpoint carries the canonical spelling.
scheme := strings.ToLower(r.URL.Scheme)
if scheme != "http" && scheme != "https" {
if r.URL.Scheme != "http" && r.URL.Scheme != "https" {
return fmt.Errorf("http redirect: unsupported scheme %q", r.URL.Scheme)
}
// schemeUpgrade rather than an inline comparison, so the one cross-scheme
// change go-git permits has a single definition shared with
// credentialsMayFollow.
if !strings.EqualFold(scheme, s.endpoint.Protocol) && !schemeUpgrade(s.endpoint.Protocol, scheme) {
if r.URL.Scheme != s.endpoint.Protocol &&
!(s.endpoint.Protocol == "http" && r.URL.Scheme == "https") {
return fmt.Errorf("http redirect: changes scheme from %q to %q", s.endpoint.Protocol, r.URL.Scheme)
}
@@ -415,20 +384,7 @@ func (s *session) ModifyEndpointIfRedirect(res *http.Response) error {
return err
}
// The session stores the endpoint and re-applies its credentials on every
// later request, so clear them once the redirect has left the origin they
// were issued for. This uses the same predicate as stripCredentials, so
// both halves share one definition of an origin.
//
// The two are deliberately asymmetric in one respect: stripCredentials is
// sticky over the whole chain, so an origin -> evil -> origin redirect
// leaves the discovery GET's later hops unauthenticated even though the
// chain returned home. This compares the endpoint only against the final
// URL, so the same round trip leaves the session authenticated. That is
// not a leak - the final URL's origin is the original one - but it means
// such a chain can make the discovery GET anonymous while the session's
// POSTs are authenticated, which can surface as a confusing 401.
if !credentialsMayFollow(endpointURL(s.endpoint), r.URL) {
if host != s.endpoint.Host || effectivePort(r.URL.Scheme, port) != effectivePort(s.endpoint.Protocol, s.endpoint.Port) {
s.endpoint.User = ""
s.endpoint.Password = ""
s.auth = nil
@@ -437,7 +393,7 @@ func (s *session) ModifyEndpointIfRedirect(res *http.Response) error {
s.endpoint.Host = host
s.endpoint.Port = port
s.endpoint.Protocol = scheme
s.endpoint.Protocol = r.URL.Scheme
s.endpoint.Path = r.URL.Path[:len(r.URL.Path)-len(infoRefsPath)]
return nil
}
@@ -463,132 +419,19 @@ func endpointPort(port string) (int, error) {
return parsed, nil
}
// schemeUpgrade reports whether the scheme transition from one URL to another
// is the one cross-scheme change go-git permits: a plain-http origin upgrading
// to https. It strictly improves confidentiality and is how servers steer
// clients off cleartext.
//
// Permitting it at all is a deliberate deviation: curl, git and the Fetch
// standard all count scheme as part of host identity and drop credentials on
// the upgrade. Auth is sent pre-emptively here, so an http origin has already
// spent its credential in cleartext on the first request and refusing the
// upgrade would break the clone without unspending it. The host is unchanged,
// where an on-path attacker needs a valid certificate to receive anything.
func schemeUpgrade(from, to string) bool {
return strings.EqualFold(from, "http") && strings.EqualFold(to, "https")
}
func effectivePort(scheme string, port int) int {
if port != 0 {
return port
}
// canonicalHost returns u's hostname in the form origins are compared in.
//
// An address literal is normalised by netip, so the many spellings of one
// address are one origin. Two literals are the same origin exactly when netip
// parses them to the same Addr, which is also how the WHATWG URL Standard
// compares hosts. An IPv4-mapped literal is deliberately not unmapped onto
// the IPv4 it dials: reaching the same endpoint is not the same authority,
// since net/http sends the literal as written in Host and a server may route
// the two spellings to different virtual hosts.
//
// netip also keeps a scope zone verbatim, which is what origin comparison
// needs: net resolves a zone to an interface by exact name, so folding %eth0
// onto %ETH0 would call two hosts the same origin that net dials down
// different interfaces.
//
// A registered name is ASCII-lowercased. That fold is the only liberty taken;
// every other difference in spelling is a different origin.
//
// A trailing root dot is one such difference and is kept, for the same reason
// as the IPv4-mapped literal: curl and the WHATWG URL Standard both hold
// "example.com." and "example.com" to be distinct hosts, and although
// crypto/tls and crypto/x509 fold the dot when they authenticate the peer,
// net/http sends the name as written in Host.
//
// The fold is deliberately ASCII-only. strings.ToLower and strings.EqualFold
// apply Unicode case mapping, which folds U+03C2 onto U+03C3 and so would
// call two hosts the same origin when they resolve to different servers. An
// ASCII-only fold cannot merge two names DNS keeps apart.
//
// No IDNA mapping is applied either, so a unicode hostname is a different
// origin from the punycode encoding of it, and from another Unicode case of
// itself, even though all three reach the same server. Mapping through
// golang.org/x/net/idna would join them, but it can only widen this equality,
// never narrow it, so leaving it out can cost a credential across such a
// redirect and cannot forward one. Against that cost, go-git pins x/net while
// net/http uses the copy vendored into the toolchain: the two are versioned
// separately, so a release that moves the Unicode tables under one and not
// the other would have this merge origins net/http still dials apart. That is
// the failure this comparison exists to prevent, and comparing bytes has no
// such mode.
func canonicalHost(u *url.URL) string {
host := u.Hostname()
if addr, err := netip.ParseAddr(host); err == nil {
return addr.String()
switch strings.ToLower(scheme) {
case "http":
return 80
case "https":
return 443
default:
return 0
}
b := []byte(host)
for i := range b {
if b[i] >= 'A' && b[i] <= 'Z' {
b[i] += 'a' - 'A'
}
}
return string(b)
}
// effectivePort returns u's port as the connection will use it: the scheme's
// well-known port when the URL does not spell one out, and without leading
// zeroes, so "https://x", "https://x:443" and "https://x:0443" all agree.
func effectivePort(u *url.URL) string {
port := u.Port()
if port == "" {
switch strings.ToLower(u.Scheme) {
case "http":
return "80"
case "https":
return "443"
default:
return ""
}
}
if trimmed := strings.TrimLeft(port, "0"); trimmed != "" {
return trimmed
}
return "0"
}
// credentialsMayFollow reports whether credentials issued for one URL may be
// sent to another.
//
// The relation is deliberately asymmetric: scheme, host and effective port
// must all match, except that a plain http origin may upgrade to https on the
// same host (see schemeUpgrade). That exception is confined to the two
// default ports: 80 to 443 is the upgrade servers actually steer clients
// through, whereas a non-default port carries no such convention, so
// http://host:8080 to https://host:8443 is a move to another origin like any
// other port change.
//
// Host matching is exact. Unlike Go's http.Client, which forwards credentials
// from a host to any subdomain of it, a subdomain is a different origin here —
// matching canonical git and libcurl.
func credentialsMayFollow(from, to *url.URL) bool {
if canonicalHost(from) != canonicalHost(to) {
return false
}
if strings.EqualFold(from.Scheme, to.Scheme) {
return effectivePort(from) == effectivePort(to)
}
return schemeUpgrade(from.Scheme, to.Scheme) &&
effectivePort(from) == "80" && effectivePort(to) == "443"
}
// endpointURL renders an Endpoint's origin as a URL, so that the session's
// credential clearing and the per-hop stripping share one definition of an
// origin and cannot drift apart.
func endpointURL(ep *transport.Endpoint) *url.URL {
host := strings.Trim(ep.Host, "[]")
if ep.Port != 0 {
host = net.JoinHostPort(host, strconv.Itoa(ep.Port))
} else if strings.Contains(host, ":") {
host = "[" + host + "]"
}
return &url.URL{Scheme: ep.Protocol, Host: host}
}
func (c *client) cloneHTTPClient(transport http.RoundTripper) *http.Client {
@@ -605,215 +448,26 @@ func wrapCheckRedirect(policy RedirectPolicy, next func(*http.Request, []*http.R
if err := checkRedirect(req, via, policy); err != nil {
return err
}
// Strip before the caller's hook so it observes what will actually
// be sent, and again afterwards so a hook of the common "preserve
// my headers across redirects" shape - which copies from via[0],
// the original unsanitized request - cannot reinstate them.
// Carrying credentials across an origin boundary is deliberately
// unsupported.
stripCredentials(req, via)
if next != nil {
if err := next(req, via); err != nil {
return err
}
return next(req, via)
}
stripCredentials(req, via)
return nil
}
}
// safeHeaders lists the headers go-git sets itself, none of which can carry a
// caller credential. stripCredentials keeps only these when a redirect leaves
// the credential's origin. Adding a name here makes it forwardable across an
// origin boundary - do not add anything a caller can put a secret in.
//
// This narrows rather than eliminates the exposure: an AuthMethod that writes
// a credential into one of these names directly - for example
// Header.Set("User-Agent", "token "+secret) - still survives a cross-origin
// redirect. Such a value is also sent to the origin and to any proxy in path,
// so it should not be placed there whether or not a redirect follows.
var safeHeaders = map[string]struct{}{
"User-Agent": {},
"Host": {},
"Accept": {},
"Content-Type": {},
"Content-Length": {},
}
func filterHeaders(h http.Header) http.Header {
filtered := make(http.Header)
for key, values := range h {
if _, ok := safeHeaders[http.CanonicalHeaderKey(key)]; ok {
filtered[key] = values
}
}
return filtered
}
// stripCredentials removes credentials from req once the redirect chain has
// left the origin of the original, credential-bearing request.
//
// CheckRedirect is the only hook that runs while a redirected request's
// headers are still mutable: http.Client.Do performs the entire chain
// internally, so anything the transport does after Do returns - including
// ModifyEndpointIfRedirect - is too late for the hops themselves.
//
// Two subtleties:
//
// - net/http rebuilds every redirect request from the original request's
// headers before calling this, so a header removed at one hop reappears
// at the next. The decision is therefore recomputed per hop.
// - The decision is sticky: once the chain has left the origin, credentials
// stay gone even if a later hop returns to it. Stickiness is derived from
// via rather than stored, because this closure is shared across a
// session's requests.
//
// Stripping keeps only the headers go-git sets itself (safeHeaders). An
// allowlist is used rather than a list of credential header names because
// caller credentials arrive under names that cannot be enumerated -
// PRIVATE-TOKEN, X-Api-Key, gateway headers - which is exactly what
// net/http's fixed list of sensitive header names gets wrong. It is also
// immune to header-name canonicalisation: an AuthMethod that writes a raw map
// key is still removed.
func stripCredentials(req *http.Request, via []*http.Request) {
if len(via) == 0 {
return
}
// net/http sets a URL on every request it builds, and req.URL is non-nil
// by construction: checkRedirect dereferences req.URL.Scheme on each path
// that returns nil, so it runs first or not at all. This nil check and
// the two in crossedOrigin are defensive, against a synthetic caller.
// Each treats a URL it cannot read as an origin crossing; removing one
// panics in canonicalHost rather than leaking.
if origin := via[0].URL; origin != nil && !crossedOrigin(origin, req, via) {
return
}
req.Header = filterHeaders(req.Header)
if req.URL != nil {
req.URL.User = nil
}
}
// crossedOrigin reports whether any hop so far, including the pending one, has
// left origin.
func crossedOrigin(origin *url.URL, req *http.Request, via []*http.Request) bool {
if req.URL == nil || !credentialsMayFollow(origin, req.URL) {
return true
}
for _, prev := range via[1:] {
if prev.URL == nil || !credentialsMayFollow(origin, prev.URL) {
return true
}
}
return false
}
// redactedURL returns the string form of u with the userinfo password
// replaced, for use in error messages. (*url.URL).String() renders the
// password verbatim, and request URLs are built from the endpoint, which
// carries whatever credentials the caller put in the clone URL.
func redactedURL(u *url.URL) string {
if u == nil {
return ""
}
if u.User == nil {
return u.String()
}
if _, hasPassword := u.User.Password(); !hasPassword {
return u.String()
}
redacted := *u
redacted.User = url.UserPassword(u.User.Username(), "REDACTED")
return redacted.String()
}
// redactedRawURL is redactedURL for a string that may not parse. Request URLs
// are assembled from Endpoint.String(), which re-emits Endpoint.Path raw, so a
// path holding a stray percent produces a string url.Parse rejects. url.Parse
// reports the input verbatim and applies no redaction of its own; only
// http.Client strips a password, and only from errors it raises itself.
func redactedRawURL(raw string) string {
i := strings.Index(raw, "://")
if i < 0 {
return raw
}
authority := raw[i+3:]
if end := strings.IndexByte(authority, '/'); end >= 0 {
authority = authority[:end]
}
at := strings.LastIndexByte(authority, '@')
if at < 0 {
return raw
}
colon := strings.IndexByte(authority[:at], ':')
if colon < 0 {
// Username only, left alone, as redactedURL leaves it.
return raw
}
return raw[:i+3+colon+1] + "REDACTED" + raw[i+3+at:]
}
// newRequest wraps http.NewRequest so that a URL it cannot parse does not
// reach the caller with the endpoint's credentials still in it.
func newRequest(method, rawURL string, body io.Reader) (*http.Request, error) {
req, err := http.NewRequest(method, rawURL, body)
if err != nil {
var uerr *url.Error
if errors.As(err, &uerr) {
uerr.URL = redactedRawURL(uerr.URL)
}
return nil, err
}
return req, nil
}
func checkRedirect(req *http.Request, via []*http.Request, policy RedirectPolicy) error {
// CheckRedirect is the only hook that runs before the next hop leaves
// the client. ModifyEndpointIfRedirect inspects the chain after
// client.Do has followed all of it, so a hop rejected there has already
// carried the request headers to its server.
//
// The wording matches the message ModifyEndpointIfRedirect produces for
// the same hop, which this check reaches first.
if len(via) != 0 {
// A hop whose scheme cannot be read cannot be shown not to have been
// https, so it is assumed to have been, and a cleartext target is
// rejected. Skipping the comparison instead would let an
// undeterminable hop turn the check off, which is the wrong default
// for a credential control; crossedOrigin fails closed the same way.
// An empty scheme is as unreadable as a nil URL, so both take the
// assumed-https default.
//
// The comparisons fold case because a scheme is case-insensitive per
// RFC 3986. net/url lowercases what it parses, so only a hand-built
// URL reaches here uppercased - the same synthetic caller the nil
// checks guard against - and for that caller "HTTPS" to "http" is
// still a downgrade.
prevScheme := "https"
if prev := via[len(via)-1]; prev.URL != nil && prev.URL.Scheme != "" {
prevScheme = prev.URL.Scheme
}
if strings.EqualFold(prevScheme, "https") && strings.EqualFold(req.URL.Scheme, "http") {
return fmt.Errorf("http redirect: changes scheme from %q to %q: %s",
prevScheme, req.URL.Scheme, redactedURL(req.URL))
}
}
switch policy {
case FollowRedirects:
case NoFollowRedirects:
return fmt.Errorf("http redirect: redirects disabled to %s", redactedURL(req.URL))
return fmt.Errorf("http redirect: redirects disabled to %s", req.URL)
case "", FollowInitialRedirects:
if !isInitialRequest(req) {
return fmt.Errorf("http redirect: redirect on non-initial request to %s", redactedURL(req.URL))
return fmt.Errorf("http redirect: redirect on non-initial request to %s", req.URL)
}
default:
return fmt.Errorf("http redirect: invalid redirect policy %q", policy)
}
// Folded for the same reason as the guard above: a scheme is
// case-insensitive per RFC 3986, so a spelling the downgrade check read
// as https must not be rejected here as a scheme go-git cannot speak.
if !strings.EqualFold(req.URL.Scheme, "http") && !strings.EqualFold(req.URL.Scheme, "https") {
if req.URL.Scheme != "http" && req.URL.Scheme != "https" {
return fmt.Errorf("http redirect: unsupported scheme %q", req.URL.Scheme)
}
if len(via) >= 10 {
@@ -827,17 +481,6 @@ func (*session) Close() error {
}
// AuthMethod is concrete implementation of common.AuthMethod for HTTP services
//
// Headers SetAuth adds are dropped when a redirect leaves the repository's
// origin: only the headers the transport sets itself survive that boundary.
// This applies to non-credential headers too, so an implementation that adds a
// trace or tenant header loses it on such a hop.
//
// That filter matches header names, not values. An implementation that writes
// a credential into a name the transport also uses - User-Agent, Host, Accept,
// Content-Type, Content-Length - has that value carried across the boundary
// with the name. Such a credential is also sent to the origin and to any proxy
// in path, so it should not be placed there whether or not a redirect follows.
type AuthMethod interface {
transport.AuthMethod
SetAuth(r *http.Request)
@@ -955,6 +598,6 @@ func (e *Err) StatusCode() int {
func (e *Err) Error() string {
return fmt.Sprintf("unexpected requesting %q status code: %d",
redactedURL(e.Response.Request.URL), e.Response.StatusCode,
e.Response.Request.URL, e.Response.StatusCode,
)
}
@@ -88,7 +88,7 @@ func (s *rpSession) doRequest(
body = content
}
req, err := newRequest(method, url, body)
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, plumbing.NewPermanentError(err)
}
+1 -1
View File
@@ -86,7 +86,7 @@ func (s *upSession) doRequest(
body = content
}
req, err := newRequest(method, url, body)
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, plumbing.NewPermanentError(err)
}
-1
View File
@@ -433,7 +433,6 @@ func dotGitCommonDirectory(fs billy.Filesystem) (commonDir billy.Filesystem, err
if err != nil {
return nil, err
}
defer ioutil.CheckClose(f, &err)
b, err := io.ReadAll(f)
if err != nil {
-57
View File
@@ -16,7 +16,6 @@ import (
"strings"
"time"
"github.com/go-git/go-git/v5/internal/pathutil"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/hash"
"github.com/go-git/go-git/v5/storage"
@@ -80,52 +79,8 @@ var (
// resolve outside the modules/ subtree, mirroring canonical Git's
// "ignoring suspicious submodule name" defence.
ErrModuleNameEscape = errors.New("submodule name escapes modules/ directory")
// ErrReferenceNameEscape is returned when a reference name would
// resolve outside its reference sub-tree once turned into a path
// under the .git directory (e.g. a name with a ".." component).
ErrReferenceNameEscape = errors.New("reference name escapes the reference storage")
)
// isPathSep reports whether r is a path separator in reference names.
// It treats both '/' and '\\' as separators to harden against cross-OS paths.
func isPathSep(r rune) bool { return r == '/' || r == '\\' }
// validReferenceName rejects reference names that cannot be safely turned into
// a path under the .git directory. A loose reference is stored verbatim at
// ".git/<name>", so a crafted name — for instance one advertised by a malicious
// remote — could climb out of its reference sub-tree and read, overwrite, or
// delete unrelated metadata such as .git/config.
//
// The storage-safety gate is plumbing.ReferenceName.IsSafe, mirroring Git's
// refname_is_safe: a name must be under refs/ without escaping it, or be a
// [A-Z_] pseudo-ref. This alone rejects absolute, drive-prefixed, escaping and
// single-level metadata names. On top of it, this adds filesystem-specific
// hardening that IsSafe's literal check does not cover: control characters, and
// components a case-insensitive/NTFS/HFS+ filesystem would fold back to "." or
// ".." (trailing dots/spaces, Alternate Data Streams, ignorable Unicode code
// points), delegated to pathutil.IsHFSDot and pathutil.IsNTFSDot with "." as
// the needle — as validSubmoduleName does — and run regardless of host OS.
func validReferenceName(name plumbing.ReferenceName) error {
if !name.IsSafe() {
return fmt.Errorf("%w: %q is not under refs/ nor a valid pseudo-ref", ErrReferenceNameEscape, string(name))
}
s := string(name)
for i := 0; i < len(s); i++ {
if s[i] < 0x20 || s[i] == 0x7f {
return fmt.Errorf("%w: %q", ErrReferenceNameEscape, s)
}
}
for _, part := range strings.FieldsFunc(s, isPathSep) {
// IsNTFSDot/IsHFSDot with a "." needle match ".." and its disguises
// but not a bare ".", so reject that component explicitly too.
if part == "." || pathutil.IsHFSDot(part, ".") || pathutil.IsNTFSDot(part, ".", "") {
return fmt.Errorf("%w: %q", ErrReferenceNameEscape, s)
}
}
return nil
}
// Options holds configuration for the storage.
type Options struct {
// ExclusiveAccess means that the filesystem is not modified externally
@@ -751,10 +706,6 @@ func (d *DotGit) checkReferenceAndTruncate(f billy.File, old *plumbing.Reference
}
func (d *DotGit) SetRef(r, old *plumbing.Reference) error {
if err := validReferenceName(r.Name()); err != nil {
return err
}
var content string
switch r.Type() {
case plumbing.SymbolicReference:
@@ -790,10 +741,6 @@ func (d *DotGit) Refs() ([]*plumbing.Reference, error) {
// Ref returns the reference for a given reference name.
func (d *DotGit) Ref(name plumbing.ReferenceName) (*plumbing.Reference, error) {
if err := validReferenceName(name); err != nil {
return nil, err
}
ref, err := d.readReferenceFile(".", name.String())
if err == nil {
return ref, nil
@@ -857,10 +804,6 @@ func (d *DotGit) packedRef(name plumbing.ReferenceName) (*plumbing.Reference, er
// RemoveRef removes a reference by name.
func (d *DotGit) RemoveRef(name plumbing.ReferenceName) error {
if err := validReferenceName(name); err != nil {
return err
}
path := d.fs.Join(".", name.String())
_, err := d.fs.Stat(path)
if err == nil {
-67
View File
@@ -583,10 +583,6 @@ func (w *Worktree) checkoutChangeSubmodule(name string,
return err
}
if err := w.clearBlockingSymlinks(name); err != nil {
return err
}
if err := w.Filesystem.MkdirAll(name, mode); err != nil {
return err
}
@@ -630,70 +626,7 @@ func (w *Worktree) checkoutChangeRegularFile(name string,
return nil
}
// clearBlockingSymlinks removes a symlink that is in the way of
// materialising name, so the checkout writes a real entry in its place
// instead of following the link out of the worktree. Two cases:
//
// - a leading directory component that is a symlink (e.g. "s" while
// writing "s/config", where "s" links to ".git"): OpenFile/MkdirAll
// would traverse it, so the write would land under the link's target.
// - the final component itself being a symlink (e.g. writing "s" while
// "s" links to ".git/config"): OpenFile with O_TRUNC, or Symlink,
// would follow/replace through it and clobber the target.
//
// A symlink can never be a legitimate parent of, or the destination for,
// a tracked entry, so removing it is always correct. This mirrors upstream
// Git's forced checkout, which unlinks a blocking symlink in the leading
// path (create_directories) and unlinks an existing entry before
// write_entry.
// https://github.com/git/git/blob/v2.54.0/entry.c#L50
func (w *Worktree) clearBlockingSymlinks(name string) error {
var dirs []string
for dir := filepath.Dir(name); dir != "." && dir != "" && dir != string(filepath.Separator); dir = filepath.Dir(dir) {
dirs = append(dirs, dir)
}
// Leading components, shallowest-first: removing the shallowest symlink
// invalidates every component beneath it, so a single removal is enough.
for i := len(dirs) - 1; i >= 0; i-- {
fi, err := w.Filesystem.Lstat(dirs[i])
if err != nil {
// A missing component is created as a real directory by the
// checkout. Any other error means we cannot tell whether it is
// a symlink, so surface it instead of leaving a blocking link in
// place and failing later in a harder-to-diagnose way.
if os.IsNotExist(err) {
continue
}
return err
}
if fi.Mode()&os.ModeSymlink != 0 {
return w.Filesystem.Remove(dirs[i])
}
}
// Final component: an existing symlink here would be followed by the
// subsequent OpenFile/Symlink/MkdirAll, so replace it.
fi, err := w.Filesystem.Lstat(name)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if fi.Mode()&os.ModeSymlink != 0 {
return w.Filesystem.Remove(name)
}
return nil
}
func (w *Worktree) checkoutFile(f *object.File) (err error) {
// checkoutFile is the materialisation boundary for tracked entries.
// Remove any blocking symlink first so the subsequent OpenFile or
// Symlink call writes the entry itself instead of following a planted
// final-component link in the underlying filesystem.
if err := w.clearBlockingSymlinks(f.Name); err != nil {
return err
}
mode, err := f.Mode.ToOSFileMode()
if err != nil {
return
+15 -100
View File
@@ -35,25 +35,10 @@ func defaultProtectNTFS() bool {
return true
}
// worktreeFilesystem wraps a billy.Filesystem and validates every path it
// is handed, so worktree operations cannot use dangerous paths at the
// boundary. Two layers apply:
//
// - validPath rejects dangerous path *strings*: .git and its HFS+/NTFS
// variants, "..", control characters, volume names.
// - validNoLeadingSymlink rejects paths whose leading directories
// already exist on disk as symlinks, so a write or delete cannot
// follow a planted link out of the tree.
//
// Both layers run on every mutating operation (validWritePath) and every
// read (validReadPath). Chroot additionally refuses a symlink as the final
// component, so a sub-filesystem such as a submodule worktree cannot be
// scoped to a redirected target.
//
// The wrapper intentionally stops at leading-component traversal. Callers
// that need final-component no-follow semantics for materialisation
// (checkoutFile) enforce that directly by removing the blocking symlink
// before opening the destination path.
// worktreeFilesystem wraps a billy.Filesystem and validates every path passed
// to a mutating operation. This prevents writing to, or deleting from,
// dangerous locations (e.g. .git/*, ../) regardless of which worktree
// code path triggers the operation.
type worktreeFilesystem struct {
billy.Filesystem
protectNTFS bool
@@ -65,7 +50,7 @@ func newWorktreeFilesystem(fs billy.Filesystem, protectNTFS, protectHFS bool) *w
}
func (sfs *worktreeFilesystem) Create(filename string) (billy.File, error) {
if err := sfs.validWritePath(filename); err != nil {
if err := sfs.validPath(filename); err != nil {
return nil, fmt.Errorf("create: %w", err)
}
return sfs.Filesystem.Create(filename)
@@ -79,7 +64,7 @@ func (sfs *worktreeFilesystem) Open(filename string) (billy.File, error) {
}
func (sfs *worktreeFilesystem) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
if err := sfs.validWritePath(filename); err != nil {
if err := sfs.validPath(filename); err != nil {
return nil, fmt.Errorf("openfile: %w", err)
}
return sfs.Filesystem.OpenFile(filename, flag, perm)
@@ -93,14 +78,14 @@ func (sfs *worktreeFilesystem) Stat(filename string) (os.FileInfo, error) {
}
func (sfs *worktreeFilesystem) Remove(filename string) error {
if err := sfs.validWritePath(filename); err != nil {
if err := sfs.validPath(filename); err != nil {
return fmt.Errorf("remove: %w", err)
}
return sfs.Filesystem.Remove(filename)
}
func (sfs *worktreeFilesystem) Rename(from, to string) error {
if err := sfs.validWritePath(from, to); err != nil {
if err := sfs.validPath(from, to); err != nil {
return fmt.Errorf("rename: %w", err)
}
return sfs.Filesystem.Rename(from, to)
@@ -121,7 +106,7 @@ func (sfs *worktreeFilesystem) Lstat(filename string) (os.FileInfo, error) {
}
func (sfs *worktreeFilesystem) Symlink(target, link string) error {
if err := sfs.validWritePath(link); err != nil {
if err := sfs.validPath(link); err != nil {
return fmt.Errorf("symlink: %w", err)
}
if err := sfs.validSymlinkName(link); err != nil {
@@ -146,7 +131,7 @@ func (sfs *worktreeFilesystem) MkdirAll(path string, perm os.FileMode) error {
if path == "" || path == "." || path == "/" {
return nil
}
if err := sfs.validWritePath(path); err != nil {
if err := sfs.validPath(path); err != nil {
return fmt.Errorf("mkdirall: %w", err)
}
return sfs.Filesystem.MkdirAll(path, perm)
@@ -160,39 +145,18 @@ func (sfs *worktreeFilesystem) Chroot(path string) (billy.Filesystem, error) {
if err := sfs.validReadPath(path); err != nil {
return nil, fmt.Errorf("chroot: %w", err)
}
// Chroot scopes a sub-filesystem to path, so the final component must
// be a real directory too: a symlink there would silently redirect the
// scope (e.g. a submodule worktree) to a target outside the tree. This
// is the "valid path, wrong target" case that validNoLeadingSymlink,
// which only inspects leading components, does not cover.
//
// A non-existent target is fine: Chroot creates it as a real
// directory. Any other Lstat error means we cannot prove the target
// is not a symlink, so fail closed rather than scope through it.
if fi, err := sfs.Filesystem.Lstat(path); err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("chroot: cannot stat %q: %w", path, err)
}
} else if fi.Mode()&os.ModeSymlink != 0 {
return nil, fmt.Errorf("chroot: invalid path %q: is a symlink", path)
}
return sfs.Filesystem.Chroot(path)
}
// validReadPath is like validWritePath but treats the empty string and "."
// as valid references to the worktree root. Read-side operations on the
// root (e.g. ReadDir(""), Lstat(".")) are legitimate. Mutating the root
// itself is not, so write-side operations reject it via validPath. Reads
// are still refused through a leading symlink, so the wrapper never
// follows a planted link even on the read surface.
// validReadPath is like validPath but treats the empty string and "." as
// valid references to the worktree root. Read-side operations on the root
// (e.g. ReadDir(""), Lstat(".")) are legitimate; mutating the root itself
// is not, so write-side operations continue to use validPath directly.
func (sfs *worktreeFilesystem) validReadPath(p string) error {
if p == "" || p == "." || p == "/" {
return nil
}
if err := sfs.validPath(p); err != nil {
return err
}
return sfs.validNoLeadingSymlink(p)
return sfs.validPath(p)
}
var errUnsupportedOperation = errors.New("unsupported operation")
@@ -268,55 +232,6 @@ func (sfs *worktreeFilesystem) validPath(paths ...string) error {
return nil
}
// validWritePath validates paths for mutating operations. It layers the
// filesystem-state check validNoLeadingSymlink on top of the string-only
// checks in validPath, so a write can neither name a dangerous path nor
// reach one by traversing an existing symlink. Every mutating method on
// the wrapper funnels through here, so the leading-symlink invariant holds
// for all worktree writers without each call site having to remember it.
func (sfs *worktreeFilesystem) validWritePath(paths ...string) error {
if err := sfs.validPath(paths...); err != nil {
return err
}
return sfs.validNoLeadingSymlink(paths...)
}
// validNoLeadingSymlink rejects paths whose leading directory components
// resolve through a symlink that already exists on the underlying
// filesystem. validPath guards the path string. This guards the on-disk
// state, so a write or delete cannot reach outside the worktree by
// traversing a symlink that a tree or an earlier step left in place.
//
// This is the fail-closed backstop for the whole class. Callers that want
// upstream's replace-and-continue behaviour (checkout) remove the blocking
// symlink first via clearBlockingSymlinks, so no symlink remains when the
// write reaches the wrapper. Callers that do not get a safe error,
// matching upstream Git refusing rather than following the link. See
// has_symlink_leading_path (symlinks.c) and the check_leading_path guard
// in unlink_entry (entry.c).
func (sfs *worktreeFilesystem) validNoLeadingSymlink(paths ...string) error {
for _, p := range paths {
for dir := filepath.Dir(p); dir != "." && dir != "" && dir != string(filepath.Separator); dir = filepath.Dir(dir) {
fi, err := sfs.Filesystem.Lstat(dir)
if err != nil {
// A missing ancestor is materialised as a real directory,
// so it cannot be a symlink and is safe to skip. Any other
// error (permission, I/O) means we cannot prove the
// component is not a symlink, so fail closed rather than
// let the operation traverse an unverified component.
if os.IsNotExist(err) {
continue
}
return fmt.Errorf("invalid path %q: cannot stat leading component %q: %w", p, dir, err)
}
if fi.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("invalid path %q: leading component %q is a symlink", p, dir)
}
}
}
return nil
}
// validSymlinkName checks the per-component name of a symlink for
// dotfile names that attackers can use to trick a checkout into
// writing a dangerous symlink. Each path component is compared
+1 -1
View File
@@ -371,7 +371,7 @@ func (w *Worktree) doAdd(path string, ignorePattern []gitignore.Pattern, skipSta
}
}
path = filepath.ToSlash(filepath.Clean(path))
path = filepath.Clean(path)
if err != nil || !fi.IsDir() {
added, h, err = w.doAddFile(idx, s, path, ignorePattern)
+24
View File
@@ -0,0 +1,24 @@
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
+57
View File
@@ -0,0 +1,57 @@
version: 2
builds:
-
id: "cpuid"
binary: cpuid
main: ./cmd/cpuid/main.go
env:
- CGO_ENABLED=0
flags:
- -ldflags=-s -w
goos:
- aix
- linux
- freebsd
- netbsd
- windows
- darwin
goarch:
- 386
- amd64
- arm64
goarm:
- 7
archives:
-
id: cpuid
name_template: "cpuid-{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
format_overrides:
- goos: windows
format: zip
files:
- LICENSE
checksum:
name_template: 'checksums.txt'
changelog:
sort: asc
filters:
exclude:
- '^doc:'
- '^docs:'
- '^test:'
- '^tests:'
- '^Update\sREADME.md'
nfpms:
-
file_name_template: "cpuid_package_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
vendor: Klaus Post
homepage: https://github.com/klauspost/cpuid
maintainer: Klaus Post <klauspost@gmail.com>
description: CPUID Tool
license: BSD 3-Clause
formats:
- deb
- rpm
+35
View File
@@ -0,0 +1,35 @@
Developer Certificate of Origin
Version 1.1
Copyright (C) 2015- Klaus Post & Contributors.
Email: klauspost@gmail.com
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
+22
View File
@@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Klaus Post
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.
+512
View File
@@ -0,0 +1,512 @@
# cpuid
Package cpuid provides information about the CPU running the current program.
CPU features are detected on startup, and kept for fast access through the life of the application.
Currently x86 / x64 (AMD64/i386) and ARM (ARM64) is supported, and no external C (cgo) code is used, which should make the library very easy to use.
You can access the CPU information by accessing the shared CPU variable of the cpuid library.
Package home: https://github.com/klauspost/cpuid
[![PkgGoDev](https://pkg.go.dev/badge/github.com/klauspost/cpuid)](https://pkg.go.dev/github.com/klauspost/cpuid/v2)
[![Go](https://github.com/klauspost/cpuid/actions/workflows/go.yml/badge.svg)](https://github.com/klauspost/cpuid/actions/workflows/go.yml)
## installing
`go get -u github.com/klauspost/cpuid/v2` using modules.
Drop `v2` for others.
Installing binary:
`go install github.com/klauspost/cpuid/v2/cmd/cpuid@latest`
Or download binaries from release page: https://github.com/klauspost/cpuid/releases
### Homebrew
For macOS/Linux users, you can install via [brew](https://brew.sh/)
```sh
$ brew install cpuid
```
## example
```Go
package main
import (
"fmt"
"strings"
. "github.com/klauspost/cpuid/v2"
)
func main() {
// Print basic CPU information:
fmt.Println("Name:", CPU.BrandName)
fmt.Println("PhysicalCores:", CPU.PhysicalCores)
fmt.Println("ThreadsPerCore:", CPU.ThreadsPerCore)
fmt.Println("LogicalCores:", CPU.LogicalCores)
fmt.Println("Family", CPU.Family, "Model:", CPU.Model, "Vendor ID:", CPU.VendorID)
fmt.Println("Features:", strings.Join(CPU.FeatureSet(), ","))
fmt.Println("Cacheline bytes:", CPU.CacheLine)
fmt.Println("L1 Data Cache:", CPU.Cache.L1D, "bytes")
fmt.Println("L1 Instruction Cache:", CPU.Cache.L1I, "bytes")
fmt.Println("L2 Cache:", CPU.Cache.L2, "bytes")
fmt.Println("L3 Cache:", CPU.Cache.L3, "bytes")
fmt.Println("Frequency", CPU.Hz, "hz")
// Test if we have these specific features:
if CPU.Supports(SSE, SSE2) {
fmt.Println("We have Streaming SIMD 2 Extensions")
}
}
```
Sample output:
```
>go run main.go
Name: AMD Ryzen 9 3950X 16-Core Processor
PhysicalCores: 16
ThreadsPerCore: 2
LogicalCores: 32
Family 23 Model: 113 Vendor ID: AMD
Features: ADX,AESNI,AVX,AVX2,BMI1,BMI2,CLMUL,CMOV,CX16,F16C,FMA3,HTT,HYPERVISOR,LZCNT,MMX,MMXEXT,NX,POPCNT,RDRAND,RDSEED,RDTSCP,SHA,SSE,SSE2,SSE3,SSE4,SSE42,SSE4A,SSSE3
Cacheline bytes: 64
L1 Data Cache: 32768 bytes
L1 Instruction Cache: 32768 bytes
L2 Cache: 524288 bytes
L3 Cache: 16777216 bytes
Frequency 0 hz
We have Streaming SIMD 2 Extensions
```
# usage
The `cpuid.CPU` provides access to CPU features. Use `cpuid.CPU.Supports()` to check for CPU features.
A faster `cpuid.CPU.Has()` is provided which will usually be inlined by the gc compiler.
To test a larger number of features, they can be combined using `f := CombineFeatures(CMOV, CMPXCHG8, X87, FXSR, MMX, SYSCALL, SSE, SSE2)`, etc.
This can be using with `cpuid.CPU.HasAll(f)` to quickly test if all features are supported.
Note that for some cpu/os combinations some features will not be detected.
`amd64` has rather good support and should work reliably on all platforms.
Note that hypervisors may not pass through all CPU features through to the guest OS,
so even if your host supports a feature it may not be visible on guests.
## arm64 feature detection
Not all operating systems provide ARM features directly
and there is no safe way to do so for the rest.
Currently `arm64/linux` and `arm64/freebsd` should be quite reliable.
`arm64/darwin` adds features expected from the M1 processor, but a lot remains undetected.
A `DetectARM()` can be used if you are able to control your deployment,
it will detect CPU features, but may crash if the OS doesn't intercept the calls.
A `-cpu.arm` flag for detecting unsafe ARM features can be added. See below.
Note that currently only features are detected on ARM,
no additional information is currently available.
## flags
It is possible to add flags that affects cpu detection.
For this the `Flags()` command is provided.
This must be called *before* `flag.Parse()` AND after the flags have been parsed `Detect()` must be called.
This means that any detection used in `init()` functions will not contain these flags.
Example:
```Go
package main
import (
"flag"
"fmt"
"strings"
"github.com/klauspost/cpuid/v2"
)
func main() {
cpuid.Flags()
flag.Parse()
cpuid.Detect()
// Test if we have these specific features:
if cpuid.CPU.Supports(cpuid.SSE, cpuid.SSE2) {
fmt.Println("We have Streaming SIMD 2 Extensions")
}
}
```
## commandline
Download as binary from: https://github.com/klauspost/cpuid/releases
Install from source:
`go install github.com/klauspost/cpuid/v2/cmd/cpuid@latest`
### Example
```
λ cpuid
Name: AMD Ryzen 9 3950X 16-Core Processor
Vendor String: AuthenticAMD
Vendor ID: AMD
PhysicalCores: 16
Threads Per Core: 2
Logical Cores: 32
CPU Family 23 Model: 113
Features: ADX,AESNI,AVX,AVX2,BMI1,BMI2,CLMUL,CLZERO,CMOV,CMPXCHG8,CPBOOST,CX16,F16C,FMA3,FXSR,FXSROPT,HTT,HYPERVISOR,LAHF,LZCNT,MCAOVERFLOW,MMX,MMXEXT,MOVBE,NX,OSXSAVE,POPCNT,RDRAND,RDSEED,RDTSCP,SCE,SHA,SSE,SSE2,SSE3,SSE4,SSE42,SSE4A,SSSE3,SUCCOR,X87,XSAVE
Microarchitecture level: 3
Cacheline bytes: 64
L1 Instruction Cache: 32768 bytes
L1 Data Cache: 32768 bytes
L2 Cache: 524288 bytes
L3 Cache: 16777216 bytes
```
### JSON Output:
```
λ cpuid --json
{
"BrandName": "AMD Ryzen 9 3950X 16-Core Processor",
"VendorID": 2,
"VendorString": "AuthenticAMD",
"PhysicalCores": 16,
"ThreadsPerCore": 2,
"LogicalCores": 32,
"Family": 23,
"Model": 113,
"CacheLine": 64,
"Hz": 0,
"BoostFreq": 0,
"Cache": {
"L1I": 32768,
"L1D": 32768,
"L2": 524288,
"L3": 16777216
},
"SGX": {
"Available": false,
"LaunchControl": false,
"SGX1Supported": false,
"SGX2Supported": false,
"MaxEnclaveSizeNot64": 0,
"MaxEnclaveSize64": 0,
"EPCSections": null
},
"Features": [
"ADX",
"AESNI",
"AVX",
"AVX2",
"BMI1",
"BMI2",
"CLMUL",
"CLZERO",
"CMOV",
"CMPXCHG8",
"CPBOOST",
"CX16",
"F16C",
"FMA3",
"FXSR",
"FXSROPT",
"HTT",
"HYPERVISOR",
"LAHF",
"LZCNT",
"MCAOVERFLOW",
"MMX",
"MMXEXT",
"MOVBE",
"NX",
"OSXSAVE",
"POPCNT",
"RDRAND",
"RDSEED",
"RDTSCP",
"SCE",
"SHA",
"SSE",
"SSE2",
"SSE3",
"SSE4",
"SSE42",
"SSE4A",
"SSSE3",
"SUCCOR",
"X87",
"XSAVE"
],
"X64Level": 3
}
```
### Check CPU microarch level
```
λ cpuid --check-level=3
2022/03/18 17:04:40 AMD Ryzen 9 3950X 16-Core Processor
2022/03/18 17:04:40 Microarchitecture level 3 is supported. Max level is 3.
Exit Code 0
λ cpuid --check-level=4
2022/03/18 17:06:18 AMD Ryzen 9 3950X 16-Core Processor
2022/03/18 17:06:18 Microarchitecture level 4 not supported. Max level is 3.
Exit Code 1
```
## Available flags
### x86 & amd64
| Feature Flag | Description |
|--------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ADX | Intel ADX (Multi-Precision Add-Carry Instruction Extensions) |
| AESNI | Advanced Encryption Standard New Instructions |
| AMD3DNOW | AMD 3DNOW |
| AMD3DNOWEXT | AMD 3DNowExt |
| AMXBF16 | Tile computational operations on BFLOAT16 numbers |
| AMXINT8 | Tile computational operations on 8-bit integers |
| AMXFP16 | Tile computational operations on FP16 numbers |
| AMXFP8 | Tile computational operations on FP8 numbers |
| AMXCOMPLEX | Tile computational operations on complex numbers |
| AMXTILE | Tile architecture |
| AMXTF32 | Matrix Multiplication of TF32 Tiles into Packed Single Precision Tile |
| AMXTRANSPOSE | Tile multiply where the first operand is transposed |
| APX_F | Intel APX |
| AVX | AVX functions |
| AVX10 | If set the Intel AVX10 Converged Vector ISA is supported |
| AVX10_128 | If set indicates that AVX10 128-bit vector support is present |
| AVX10_256 | If set indicates that AVX10 256-bit vector support is present |
| AVX10_512 | If set indicates that AVX10 512-bit vector support is present |
| AVX2 | AVX2 functions |
| AVX512BF16 | AVX-512 BFLOAT16 Instructions |
| AVX512BITALG | AVX-512 Bit Algorithms |
| AVX512BW | AVX-512 Byte and Word Instructions |
| AVX512CD | AVX-512 Conflict Detection Instructions |
| AVX512DQ | AVX-512 Doubleword and Quadword Instructions |
| AVX512ER | AVX-512 Exponential and Reciprocal Instructions |
| AVX512F | AVX-512 Foundation |
| AVX512FP16 | AVX-512 FP16 Instructions |
| AVX512IFMA | AVX-512 Integer Fused Multiply-Add Instructions |
| AVX512PF | AVX-512 Prefetch Instructions |
| AVX512VBMI | AVX-512 Vector Bit Manipulation Instructions |
| AVX512VBMI2 | AVX-512 Vector Bit Manipulation Instructions, Version 2 |
| AVX512VL | AVX-512 Vector Length Extensions |
| AVX512VNNI | AVX-512 Vector Neural Network Instructions |
| AVX512VP2INTERSECT | AVX-512 Intersect for D/Q |
| AVX512VPOPCNTDQ | AVX-512 Vector Population Count Doubleword and Quadword |
| AVXIFMA | AVX-IFMA instructions |
| AVXNECONVERT | AVX-NE-CONVERT instructions |
| AVXSLOW | Indicates the CPU performs 2 128 bit operations instead of one |
| AVXVNNI | AVX (VEX encoded) VNNI neural network instructions |
| AVXVNNIINT8 | AVX-VNNI-INT8 instructions |
| AVXVNNIINT16 | AVX-VNNI-INT16 instructions |
| BHI_CTRL | Branch History Injection and Intra-mode Branch Target Injection / CVE-2022-0001, CVE-2022-0002 / INTEL-SA-00598 |
| BMI1 | Bit Manipulation Instruction Set 1 |
| BMI2 | Bit Manipulation Instruction Set 2 |
| CETIBT | Intel CET Indirect Branch Tracking |
| CETSS | Intel CET Shadow Stack |
| CLDEMOTE | Cache Line Demote |
| CLMUL | Carry-less Multiplication |
| CLZERO | CLZERO instruction supported |
| CMOV | i686 CMOV |
| CMPCCXADD | CMPCCXADD instructions |
| CMPSB_SCADBS_SHORT | Fast short CMPSB and SCASB |
| CMPXCHG8 | CMPXCHG8 instruction |
| CPBOOST | Core Performance Boost |
| CPPC | AMD: Collaborative Processor Performance Control |
| CX16 | CMPXCHG16B Instruction |
| EFER_LMSLE_UNS | AMD: =Core::X86::Msr::EFER[LMSLE] is not supported, and MBZ |
| ENQCMD | Enqueue Command |
| ERMS | Enhanced REP MOVSB/STOSB |
| F16C | Half-precision floating-point conversion |
| FLUSH_L1D | Flush L1D cache |
| FMA3 | Intel FMA 3. Does not imply AVX. |
| FMA4 | Bulldozer FMA4 functions |
| FP128 | AMD: When set, the internal FP/SIMD execution datapath is 128-bits wide |
| FP256 | AMD: When set, the internal FP/SIMD execution datapath is 256-bits wide |
| FSRM | Fast Short Rep Mov |
| FXSR | FXSAVE, FXRESTOR instructions, CR4 bit 9 |
| FXSROPT | FXSAVE/FXRSTOR optimizations |
| GFNI | Galois Field New Instructions. May require other features (AVX, AVX512VL,AVX512F) based on usage. |
| HLE | Hardware Lock Elision |
| HRESET | If set CPU supports history reset and the IA32_HRESET_ENABLE MSR |
| HTT | Hyperthreading (enabled) |
| HWA | Hardware assert supported. Indicates support for MSRC001_10 |
| HYBRID_CPU | This part has CPUs of more than one type. |
| HYPERVISOR | This bit has been reserved by Intel & AMD for use by hypervisors |
| IA32_ARCH_CAP | IA32_ARCH_CAPABILITIES MSR (Intel) |
| IA32_CORE_CAP | IA32_CORE_CAPABILITIES MSR |
| IBPB | Indirect Branch Restricted Speculation (IBRS) and Indirect Branch Predictor Barrier (IBPB) |
| IBRS | AMD: Indirect Branch Restricted Speculation |
| IBRS_PREFERRED | AMD: IBRS is preferred over software solution |
| IBRS_PROVIDES_SMP | AMD: IBRS provides Same Mode Protection |
| IBS | Instruction Based Sampling (AMD) |
| IBSBRNTRGT | Instruction Based Sampling Feature (AMD) |
| IBSFETCHSAM | Instruction Based Sampling Feature (AMD) |
| IBSFFV | Instruction Based Sampling Feature (AMD) |
| IBSOPCNT | Instruction Based Sampling Feature (AMD) |
| IBSOPCNTEXT | Instruction Based Sampling Feature (AMD) |
| IBSOPSAM | Instruction Based Sampling Feature (AMD) |
| IBSRDWROPCNT | Instruction Based Sampling Feature (AMD) |
| IBSRIPINVALIDCHK | Instruction Based Sampling Feature (AMD) |
| IBS_FETCH_CTLX | AMD: IBS fetch control extended MSR supported |
| IBS_OPDATA4 | AMD: IBS op data 4 MSR supported |
| IBS_OPFUSE | AMD: Indicates support for IbsOpFuse |
| IBS_PREVENTHOST | Disallowing IBS use by the host supported |
| IBS_ZEN4 | Fetch and Op IBS support IBS extensions added with Zen4 |
| IDPRED_CTRL | IPRED_DIS |
| INT_WBINVD | WBINVD/WBNOINVD are interruptible. |
| INVLPGB | NVLPGB and TLBSYNC instruction supported |
| KEYLOCKER | Key locker |
| KEYLOCKERW | Key locker wide |
| LAHF | LAHF/SAHF in long mode |
| LAM | If set, CPU supports Linear Address Masking |
| LBRVIRT | LBR virtualization |
| LZCNT | LZCNT instruction |
| MCAOVERFLOW | MCA overflow recovery support. |
| MCDT_NO | Processor do not exhibit MXCSR Configuration Dependent Timing behavior and do not need to mitigate it. |
| MCOMMIT | MCOMMIT instruction supported |
| MD_CLEAR | VERW clears CPU buffers |
| MMX | standard MMX |
| MMXEXT | SSE integer functions or AMD MMX ext |
| MOVBE | MOVBE instruction (big-endian) |
| MOVDIR64B | Move 64 Bytes as Direct Store |
| MOVDIRI | Move Doubleword as Direct Store |
| MOVSB_ZL | Fast Zero-Length MOVSB |
| MPX | Intel MPX (Memory Protection Extensions) |
| MOVU | MOVU SSE instructions are more efficient and should be preferred to SSE MOVL/MOVH. MOVUPS is more efficient than MOVLPS/MOVHPS. MOVUPD is more efficient than MOVLPD/MOVHPD |
| MSRIRC | Instruction Retired Counter MSR available |
| MSRLIST | Read/Write List of Model Specific Registers |
| MSR_PAGEFLUSH | Page Flush MSR available |
| NRIPS | Indicates support for NRIP save on VMEXIT |
| NX | NX (No-Execute) bit |
| OSXSAVE | XSAVE enabled by OS |
| PCONFIG | PCONFIG for Intel Multi-Key Total Memory Encryption |
| POPCNT | POPCNT instruction |
| PPIN | AMD: Protected Processor Inventory Number support. Indicates that Protected Processor Inventory Number (PPIN) capability can be enabled |
| PREFETCHI | PREFETCHIT0/1 instructions |
| PSFD | Predictive Store Forward Disable |
| RDPRU | RDPRU instruction supported |
| RDRAND | RDRAND instruction is available |
| RDSEED | RDSEED instruction is available |
| RDTSCP | RDTSCP Instruction |
| RRSBA_CTRL | Restricted RSB Alternate |
| RTM | Restricted Transactional Memory |
| RTM_ALWAYS_ABORT | Indicates that the loaded microcode is forcing RTM abort. |
| SERIALIZE | Serialize Instruction Execution |
| SEV | AMD Secure Encrypted Virtualization supported |
| SEV_64BIT | AMD SEV guest execution only allowed from a 64-bit host |
| SEV_ALTERNATIVE | AMD SEV Alternate Injection supported |
| SEV_DEBUGSWAP | Full debug state swap supported for SEV-ES guests |
| SEV_ES | AMD SEV Encrypted State supported |
| SEV_RESTRICTED | AMD SEV Restricted Injection supported |
| SEV_SNP | AMD SEV Secure Nested Paging supported |
| SGX | Software Guard Extensions |
| SGXLC | Software Guard Extensions Launch Control |
| SGXPQC | Software Guard Extensions 256-bit Encryption |
| SHA | Intel SHA Extensions |
| SME | AMD Secure Memory Encryption supported |
| SME_COHERENT | AMD Hardware cache coherency across encryption domains enforced |
| SM3_X86 | SM3 instructions |
| SM4_X86 | SM4 instructions |
| SPEC_CTRL_SSBD | Speculative Store Bypass Disable |
| SRBDS_CTRL | SRBDS mitigation MSR available |
| SSE | SSE functions |
| SSE2 | P4 SSE functions |
| SSE3 | Prescott SSE3 functions |
| SSE4 | Penryn SSE4.1 functions |
| SSE42 | Nehalem SSE4.2 functions |
| SSE4A | AMD Barcelona microarchitecture SSE4a instructions |
| SSSE3 | Conroe SSSE3 functions |
| STIBP | Single Thread Indirect Branch Predictors |
| STIBP_ALWAYSON | AMD: Single Thread Indirect Branch Prediction Mode has Enhanced Performance and may be left Always On |
| STOSB_SHORT | Fast short STOSB |
| SUCCOR | Software uncorrectable error containment and recovery capability. |
| SVM | AMD Secure Virtual Machine |
| SVMDA | Indicates support for the SVM decode assists. |
| SVMFBASID | SVM, Indicates that TLB flush events, including CR3 writes and CR4.PGE toggles, flush only the current ASID's TLB entries. Also indicates support for the extended VMCBTLB_Control |
| SVML | AMD SVM lock. Indicates support for SVM-Lock. |
| SVMNP | AMD SVM nested paging |
| SVMPF | SVM pause intercept filter. Indicates support for the pause intercept filter |
| SVMPFT | SVM PAUSE filter threshold. Indicates support for the PAUSE filter cycle count threshold |
| SYSCALL | System-Call Extension (SCE): SYSCALL and SYSRET instructions. |
| SYSEE | SYSENTER and SYSEXIT instructions |
| TBM | AMD Trailing Bit Manipulation |
| TDX_GUEST | Intel Trust Domain Extensions Guest |
| TLB_FLUSH_NESTED | AMD: Flushing includes all the nested translations for guest translations |
| TME | Intel Total Memory Encryption. The following MSRs are supported: IA32_TME_CAPABILITY, IA32_TME_ACTIVATE, IA32_TME_EXCLUDE_MASK, and IA32_TME_EXCLUDE_BASE. |
| TOPEXT | TopologyExtensions: topology extensions support. Indicates support for CPUID Fn8000_001D_EAX_x[N:0]-CPUID Fn8000_001E_EDX. |
| TSA_L1_NO | AMD only: Not vulnerable to TSA-L1 |
| TSA_SQ_NO | AMD only: Not vulnerable to TSA-SQ |
| TSA_VERW_CLEAR | AMD: If set, the memory form of the VERW instruction may be used to help mitigate TSA |
| TSCRATEMSR | MSR based TSC rate control. Indicates support for MSR TSC ratio MSRC000_0104 |
| TSXLDTRK | Intel TSX Suspend Load Address Tracking |
| VAES | Vector AES. AVX(512) versions requires additional checks. |
| VMCBCLEAN | VMCB clean bits. Indicates support for VMCB clean bits. |
| VMPL | AMD VM Permission Levels supported |
| VMSA_REGPROT | AMD VMSA Register Protection supported |
| VMX | Virtual Machine Extensions |
| VPCLMULQDQ | Carry-Less Multiplication Quadword. Requires AVX for 3 register versions. |
| VTE | AMD Virtual Transparent Encryption supported |
| WAITPKG | TPAUSE, UMONITOR, UMWAIT |
| WBNOINVD | Write Back and Do Not Invalidate Cache |
| WRMSRNS | Non-Serializing Write to Model Specific Register |
| X87 | FPU |
| XGETBV1 | Supports XGETBV with ECX = 1 |
| XOP | Bulldozer XOP functions |
| XSAVE | XSAVE, XRESTOR, XSETBV, XGETBV |
| XSAVEC | Supports XSAVEC and the compacted form of XRSTOR. |
| XSAVEOPT | XSAVEOPT available |
| XSAVES | Supports XSAVES/XRSTORS and IA32_XSS |
# ARM features:
| Feature Flag | Description |
|--------------|------------------------------------------------------------------|
| AESARM | AES instructions |
| ARMCPUID | Some CPU ID registers readable at user-level |
| ASIMD | Advanced SIMD |
| ASIMDDP | SIMD Dot Product |
| ASIMDHP | Advanced SIMD half-precision floating point |
| ASIMDRDM | Rounding Double Multiply Accumulate/Subtract (SQRDMLAH/SQRDMLSH) |
| ATOMICS | Large System Extensions (LSE) |
| CRC32 | CRC32/CRC32C instructions |
| DCPOP | Data cache clean to Point of Persistence (DC CVAP) |
| EVTSTRM | Generic timer |
| FCMA | Floatin point complex number addition and multiplication |
| FHM | FMLAL and FMLSL instructions |
| FP | Single-precision and double-precision floating point |
| FPHP | Half-precision floating point |
| GPA | Generic Pointer Authentication |
| JSCVT | Javascript-style double->int convert (FJCVTZS) |
| LRCPC | Weaker release consistency (LDAPR, etc) |
| PMULL | Polynomial Multiply instructions (PMULL/PMULL2) |
| RNDR | Random Number instructions |
| TLB | Outer Shareable and TLB range maintenance instructions |
| TS | Flag manipulation instructions |
| SHA1 | SHA-1 instructions (SHA1C, etc) |
| SHA2 | SHA-2 instructions (SHA256H, etc) |
| SHA3 | SHA-3 instructions (EOR3, RAXI, XAR, BCAX) |
| SHA512 | SHA512 instructions |
| SM3 | SM3 instructions |
| SM4 | SM4 instructions |
| SVE | Scalable Vector Extension |
# license
This code is published under an MIT license. See LICENSE file for more information.
+1679
View File
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//+build 386,!gccgo,!noasm,!appengine
// func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32)
TEXT ·asmCpuid(SB), 7, $0
XORL CX, CX
MOVL op+0(FP), AX
CPUID
MOVL AX, eax+4(FP)
MOVL BX, ebx+8(FP)
MOVL CX, ecx+12(FP)
MOVL DX, edx+16(FP)
RET
// func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32)
TEXT ·asmCpuidex(SB), 7, $0
MOVL op+0(FP), AX
MOVL op2+4(FP), CX
CPUID
MOVL AX, eax+8(FP)
MOVL BX, ebx+12(FP)
MOVL CX, ecx+16(FP)
MOVL DX, edx+20(FP)
RET
// func xgetbv(index uint32) (eax, edx uint32)
TEXT ·asmXgetbv(SB), 7, $0
MOVL index+0(FP), CX
BYTE $0x0f; BYTE $0x01; BYTE $0xd0 // XGETBV
MOVL AX, eax+4(FP)
MOVL DX, edx+8(FP)
RET
// func asmRdtscpAsm() (eax, ebx, ecx, edx uint32)
TEXT ·asmRdtscpAsm(SB), 7, $0
BYTE $0x0F; BYTE $0x01; BYTE $0xF9 // RDTSCP
MOVL AX, eax+0(FP)
MOVL BX, ebx+4(FP)
MOVL CX, ecx+8(FP)
MOVL DX, edx+12(FP)
RET
// func asmDarwinHasAVX512() bool
TEXT ·asmDarwinHasAVX512(SB), 7, $0
MOVL $0, eax+0(FP)
RET
+72
View File
@@ -0,0 +1,72 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//+build amd64,!gccgo,!noasm,!appengine
// func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32)
TEXT ·asmCpuid(SB), 7, $0
XORQ CX, CX
MOVL op+0(FP), AX
CPUID
MOVL AX, eax+8(FP)
MOVL BX, ebx+12(FP)
MOVL CX, ecx+16(FP)
MOVL DX, edx+20(FP)
RET
// func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32)
TEXT ·asmCpuidex(SB), 7, $0
MOVL op+0(FP), AX
MOVL op2+4(FP), CX
CPUID
MOVL AX, eax+8(FP)
MOVL BX, ebx+12(FP)
MOVL CX, ecx+16(FP)
MOVL DX, edx+20(FP)
RET
// func asmXgetbv(index uint32) (eax, edx uint32)
TEXT ·asmXgetbv(SB), 7, $0
MOVL index+0(FP), CX
BYTE $0x0f; BYTE $0x01; BYTE $0xd0 // XGETBV
MOVL AX, eax+8(FP)
MOVL DX, edx+12(FP)
RET
// func asmRdtscpAsm() (eax, ebx, ecx, edx uint32)
TEXT ·asmRdtscpAsm(SB), 7, $0
BYTE $0x0F; BYTE $0x01; BYTE $0xF9 // RDTSCP
MOVL AX, eax+0(FP)
MOVL BX, ebx+4(FP)
MOVL CX, ecx+8(FP)
MOVL DX, edx+12(FP)
RET
// From https://go-review.googlesource.com/c/sys/+/285572/
// func asmDarwinHasAVX512() bool
TEXT ·asmDarwinHasAVX512(SB), 7, $0-1
MOVB $0, ret+0(FP) // default to false
#ifdef GOOS_darwin // return if not darwin
#ifdef GOARCH_amd64 // return if not amd64
// These values from:
// https://github.com/apple/darwin-xnu/blob/xnu-4570.1.46/osfmk/i386/cpu_capabilities.h
#define commpage64_base_address 0x00007fffffe00000
#define commpage64_cpu_capabilities64 (commpage64_base_address+0x010)
#define commpage64_version (commpage64_base_address+0x01E)
#define hasAVX512F 0x0000004000000000
MOVQ $commpage64_version, BX
MOVW (BX), AX
CMPW AX, $13 // versions < 13 do not support AVX512
JL no_avx512
MOVQ $commpage64_cpu_capabilities64, BX
MOVQ (BX), AX
MOVQ $hasAVX512F, CX
ANDQ CX, AX
JZ no_avx512
MOVB $1, ret+0(FP)
no_avx512:
#endif
#endif
RET
+36
View File
@@ -0,0 +1,36 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//+build arm64,!gccgo,!noasm,!appengine
// See https://www.kernel.org/doc/Documentation/arm64/cpu-feature-registers.txt
// func getMidr
TEXT ·getMidr(SB), 7, $0
WORD $0xd5380000 // mrs x0, midr_el1 /* Main ID Register */
MOVD R0, midr+0(FP)
RET
// func getProcFeatures
TEXT ·getProcFeatures(SB), 7, $0
WORD $0xd5380400 // mrs x0, id_aa64pfr0_el1 /* Processor Feature Register 0 */
MOVD R0, procFeatures+0(FP)
RET
// func getInstAttributes
TEXT ·getInstAttributes(SB), 7, $0
WORD $0xd5380600 // mrs x0, id_aa64isar0_el1 /* Instruction Set Attribute Register 0 */
WORD $0xd5380621 // mrs x1, id_aa64isar1_el1 /* Instruction Set Attribute Register 1 */
MOVD R0, instAttrReg0+0(FP)
MOVD R1, instAttrReg1+8(FP)
RET
TEXT ·getVectorLength(SB), 7, $0
WORD $0xd2800002 // mov x2, #0
WORD $0x04225022 // addvl x2, x2, #1
WORD $0xd37df042 // lsl x2, x2, #3
WORD $0xd2800003 // mov x3, #0
WORD $0x04635023 // addpl x3, x3, #1
WORD $0xd37df063 // lsl x3, x3, #3
MOVD R2, vl+0(FP)
MOVD R3, pl+8(FP)
RET
+250
View File
@@ -0,0 +1,250 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//go:build arm64 && !gccgo && !noasm && !appengine
// +build arm64,!gccgo,!noasm,!appengine
package cpuid
import "runtime"
func getMidr() (midr uint64)
func getProcFeatures() (procFeatures uint64)
func getInstAttributes() (instAttrReg0, instAttrReg1 uint64)
func getVectorLength() (vl, pl uint64)
func initCPU() {
cpuid = func(uint32) (a, b, c, d uint32) { return 0, 0, 0, 0 }
cpuidex = func(x, y uint32) (a, b, c, d uint32) { return 0, 0, 0, 0 }
xgetbv = func(uint32) (a, b uint32) { return 0, 0 }
rdtscpAsm = func() (a, b, c, d uint32) { return 0, 0, 0, 0 }
}
func addInfo(c *CPUInfo, safe bool) {
// Seems to be safe to assume on ARM64
c.CacheLine = 64
detectOS(c)
// ARM64 disabled since it may crash if interrupt is not intercepted by OS.
if safe && !c.Has(ARMCPUID) && runtime.GOOS != "freebsd" {
return
}
midr := getMidr()
// MIDR_EL1 - Main ID Register
// https://developer.arm.com/docs/ddi0595/h/aarch64-system-registers/midr_el1
// x--------------------------------------------------x
// | Name | bits | visible |
// |--------------------------------------------------|
// | Implementer | [31-24] | y |
// |--------------------------------------------------|
// | Variant | [23-20] | y |
// |--------------------------------------------------|
// | Architecture | [19-16] | y |
// |--------------------------------------------------|
// | PartNum | [15-4] | y |
// |--------------------------------------------------|
// | Revision | [3-0] | y |
// x--------------------------------------------------x
switch (midr >> 24) & 0xff {
case 0xC0:
c.VendorString = "Ampere Computing"
c.VendorID = Ampere
case 0x41:
c.VendorString = "Arm Limited"
c.VendorID = ARM
case 0x42:
c.VendorString = "Broadcom Corporation"
c.VendorID = Broadcom
case 0x43:
c.VendorString = "Cavium Inc"
c.VendorID = Cavium
case 0x44:
c.VendorString = "Digital Equipment Corporation"
c.VendorID = DEC
case 0x46:
c.VendorString = "Fujitsu Ltd"
c.VendorID = Fujitsu
case 0x49:
c.VendorString = "Infineon Technologies AG"
c.VendorID = Infineon
case 0x4D:
c.VendorString = "Motorola or Freescale Semiconductor Inc"
c.VendorID = Motorola
case 0x4E:
c.VendorString = "NVIDIA Corporation"
c.VendorID = NVIDIA
case 0x50:
c.VendorString = "Applied Micro Circuits Corporation"
c.VendorID = AMCC
case 0x51:
c.VendorString = "Qualcomm Inc"
c.VendorID = Qualcomm
case 0x56:
c.VendorString = "Marvell International Ltd"
c.VendorID = Marvell
case 0x69:
c.VendorString = "Intel Corporation"
c.VendorID = Intel
}
// Lower 4 bits: Architecture
// Architecture Meaning
// 0b0001 Armv4.
// 0b0010 Armv4T.
// 0b0011 Armv5 (obsolete).
// 0b0100 Armv5T.
// 0b0101 Armv5TE.
// 0b0110 Armv5TEJ.
// 0b0111 Armv6.
// 0b1111 Architectural features are individually identified in the ID_* registers, see 'ID registers'.
// Upper 4 bit: Variant
// An IMPLEMENTATION DEFINED variant number.
// Typically, this field is used to distinguish between different product variants, or major revisions of a product.
c.Family = int(midr>>16) & 0xff
// PartNum, bits [15:4]
// An IMPLEMENTATION DEFINED primary part number for the device.
// On processors implemented by Arm, if the top four bits of the primary
// part number are 0x0 or 0x7, the variant and architecture are encoded differently.
// Revision, bits [3:0]
// An IMPLEMENTATION DEFINED revision number for the device.
c.Model = int(midr) & 0xffff
procFeatures := getProcFeatures()
// ID_AA64PFR0_EL1 - Processor Feature Register 0
// x--------------------------------------------------x
// | Name | bits | visible |
// |--------------------------------------------------|
// | DIT | [51-48] | y |
// |--------------------------------------------------|
// | SVE | [35-32] | y |
// |--------------------------------------------------|
// | GIC | [27-24] | n |
// |--------------------------------------------------|
// | AdvSIMD | [23-20] | y |
// |--------------------------------------------------|
// | FP | [19-16] | y |
// |--------------------------------------------------|
// | EL3 | [15-12] | n |
// |--------------------------------------------------|
// | EL2 | [11-8] | n |
// |--------------------------------------------------|
// | EL1 | [7-4] | n |
// |--------------------------------------------------|
// | EL0 | [3-0] | n |
// x--------------------------------------------------x
var f flagSet
// if procFeatures&(0xf<<48) != 0 {
// fmt.Println("DIT")
// }
f.setIf(procFeatures&(0xf<<32) != 0, SVE)
if procFeatures&(0xf<<20) != 15<<20 {
f.set(ASIMD)
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64pfr0_el1
// 0b0001 --> As for 0b0000, and also includes support for half-precision floating-point arithmetic.
f.setIf(procFeatures&(0xf<<20) == 1<<20, FPHP, ASIMDHP)
}
f.setIf(procFeatures&(0xf<<16) != 0, FP)
instAttrReg0, instAttrReg1 := getInstAttributes()
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64isar0_el1
//
// ID_AA64ISAR0_EL1 - Instruction Set Attribute Register 0
// x--------------------------------------------------x
// | Name | bits | visible |
// |--------------------------------------------------|
// | RNDR | [63-60] | y |
// |--------------------------------------------------|
// | TLB | [59-56] | y |
// |--------------------------------------------------|
// | TS | [55-52] | y |
// |--------------------------------------------------|
// | FHM | [51-48] | y |
// |--------------------------------------------------|
// | DP | [47-44] | y |
// |--------------------------------------------------|
// | SM4 | [43-40] | y |
// |--------------------------------------------------|
// | SM3 | [39-36] | y |
// |--------------------------------------------------|
// | SHA3 | [35-32] | y |
// |--------------------------------------------------|
// | RDM | [31-28] | y |
// |--------------------------------------------------|
// | ATOMICS | [23-20] | y |
// |--------------------------------------------------|
// | CRC32 | [19-16] | y |
// |--------------------------------------------------|
// | SHA2 | [15-12] | y |
// |--------------------------------------------------|
// | SHA1 | [11-8] | y |
// |--------------------------------------------------|
// | AES | [7-4] | y |
// x--------------------------------------------------x
f.setIf(instAttrReg0&(0xf<<60) != 0, RNDR)
f.setIf(instAttrReg0&(0xf<<56) != 0, TLB)
f.setIf(instAttrReg0&(0xf<<52) != 0, TS)
f.setIf(instAttrReg0&(0xf<<48) != 0, FHM)
f.setIf(instAttrReg0&(0xf<<44) != 0, ASIMDDP)
f.setIf(instAttrReg0&(0xf<<40) != 0, SM4)
f.setIf(instAttrReg0&(0xf<<36) != 0, SM3)
f.setIf(instAttrReg0&(0xf<<32) != 0, SHA3)
f.setIf(instAttrReg0&(0xf<<28) != 0, ASIMDRDM)
f.setIf(instAttrReg0&(0xf<<20) != 0, ATOMICS)
f.setIf(instAttrReg0&(0xf<<16) != 0, CRC32)
f.setIf(instAttrReg0&(0xf<<12) != 0, SHA2)
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64isar0_el1
// 0b0010 --> As 0b0001, plus SHA512H, SHA512H2, SHA512SU0, and SHA512SU1 instructions implemented.
f.setIf(instAttrReg0&(0xf<<12) == 2<<12, SHA512)
f.setIf(instAttrReg0&(0xf<<8) != 0, SHA1)
f.setIf(instAttrReg0&(0xf<<4) != 0, AESARM)
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64isar0_el1
// 0b0010 --> As for 0b0001, plus PMULL/PMULL2 instructions operating on 64-bit data quantities.
f.setIf(instAttrReg0&(0xf<<4) == 2<<4, PMULL)
// https://developer.arm.com/docs/ddi0595/b/aarch64-system-registers/id_aa64isar1_el1
//
// ID_AA64ISAR1_EL1 - Instruction set attribute register 1
// x--------------------------------------------------x
// | Name | bits | visible |
// |--------------------------------------------------|
// | GPI | [31-28] | y |
// |--------------------------------------------------|
// | GPA | [27-24] | y |
// |--------------------------------------------------|
// | LRCPC | [23-20] | y |
// |--------------------------------------------------|
// | FCMA | [19-16] | y |
// |--------------------------------------------------|
// | JSCVT | [15-12] | y |
// |--------------------------------------------------|
// | API | [11-8] | y |
// |--------------------------------------------------|
// | APA | [7-4] | y |
// |--------------------------------------------------|
// | DPB | [3-0] | y |
// x--------------------------------------------------x
// if instAttrReg1&(0xf<<28) != 0 {
// fmt.Println("GPI")
// }
f.setIf(instAttrReg1&(0xf<<28) != 24, GPA)
f.setIf(instAttrReg1&(0xf<<20) != 0, LRCPC)
f.setIf(instAttrReg1&(0xf<<16) != 0, FCMA)
f.setIf(instAttrReg1&(0xf<<12) != 0, JSCVT)
// if instAttrReg1&(0xf<<8) != 0 {
// fmt.Println("API")
// }
// if instAttrReg1&(0xf<<4) != 0 {
// fmt.Println("APA")
// }
f.setIf(instAttrReg1&(0xf<<0) != 0, DCPOP)
// Store
c.featureSet.or(f)
}
+17
View File
@@ -0,0 +1,17 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//go:build (!amd64 && !386 && !arm64) || gccgo || noasm || appengine
// +build !amd64,!386,!arm64 gccgo noasm appengine
package cpuid
func initCPU() {
cpuid = func(uint32) (a, b, c, d uint32) { return 0, 0, 0, 0 }
cpuidex = func(x, y uint32) (a, b, c, d uint32) { return 0, 0, 0, 0 }
xgetbv = func(uint32) (a, b uint32) { return 0, 0 }
rdtscpAsm = func() (a, b, c, d uint32) { return 0, 0, 0, 0 }
}
func addInfo(info *CPUInfo, safe bool) {}
func getVectorLength() (vl, pl uint64) { return 0, 0 }
+45
View File
@@ -0,0 +1,45 @@
// Copyright (c) 2015 Klaus Post, released under MIT License. See LICENSE file.
//go:build (386 && !gccgo && !noasm && !appengine) || (amd64 && !gccgo && !noasm && !appengine)
// +build 386,!gccgo,!noasm,!appengine amd64,!gccgo,!noasm,!appengine
package cpuid
func asmCpuid(op uint32) (eax, ebx, ecx, edx uint32)
func asmCpuidex(op, op2 uint32) (eax, ebx, ecx, edx uint32)
func asmXgetbv(index uint32) (eax, edx uint32)
func asmRdtscpAsm() (eax, ebx, ecx, edx uint32)
func asmDarwinHasAVX512() bool
func initCPU() {
cpuid = asmCpuid
cpuidex = asmCpuidex
xgetbv = asmXgetbv
rdtscpAsm = asmRdtscpAsm
darwinHasAVX512 = asmDarwinHasAVX512
}
func addInfo(c *CPUInfo, safe bool) {
c.maxFunc = maxFunctionID()
c.maxExFunc = maxExtendedFunction()
c.BrandName = brandName()
c.CacheLine = cacheLine()
c.Family, c.Model, c.Stepping = familyModel()
c.featureSet = support()
c.SGX = hasSGX(c.featureSet.inSet(SGX), c.featureSet.inSet(SGXLC))
c.AMDMemEncryption = hasAMDMemEncryption(c.featureSet.inSet(SME) || c.featureSet.inSet(SEV))
c.ThreadsPerCore = threadsPerCore()
c.LogicalCores = logicalCores()
c.PhysicalCores = physicalCores()
c.VendorID, c.VendorString = vendorID()
c.HypervisorVendorID, c.HypervisorVendorString = hypervisorVendorID()
c.AVX10Level = c.supportAVX10()
c.cacheSize()
c.frequencies()
if c.maxFunc >= 0x0A {
eax, ebx, _, edx := cpuid(0x0A)
c.PMU = parseLeaf0AH(c, eax, ebx, edx)
}
}
func getVectorLength() (vl, pl uint64) { return 0, 0 }
+308
View File
@@ -0,0 +1,308 @@
// Code generated by "stringer -type=FeatureID,Vendor"; DO NOT EDIT.
package cpuid
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[ADX-1]
_ = x[AESNI-2]
_ = x[AMD3DNOW-3]
_ = x[AMD3DNOWEXT-4]
_ = x[AMXBF16-5]
_ = x[AMXFP16-6]
_ = x[AMXINT8-7]
_ = x[AMXFP8-8]
_ = x[AMXTILE-9]
_ = x[AMXTF32-10]
_ = x[AMXCOMPLEX-11]
_ = x[AMXTRANSPOSE-12]
_ = x[APX_F-13]
_ = x[AVX-14]
_ = x[AVX10-15]
_ = x[AVX10_128-16]
_ = x[AVX10_256-17]
_ = x[AVX10_512-18]
_ = x[AVX2-19]
_ = x[AVX512BF16-20]
_ = x[AVX512BITALG-21]
_ = x[AVX512BW-22]
_ = x[AVX512CD-23]
_ = x[AVX512DQ-24]
_ = x[AVX512ER-25]
_ = x[AVX512F-26]
_ = x[AVX512FP16-27]
_ = x[AVX512IFMA-28]
_ = x[AVX512PF-29]
_ = x[AVX512VBMI-30]
_ = x[AVX512VBMI2-31]
_ = x[AVX512VL-32]
_ = x[AVX512VNNI-33]
_ = x[AVX512VP2INTERSECT-34]
_ = x[AVX512VPOPCNTDQ-35]
_ = x[AVXIFMA-36]
_ = x[AVXNECONVERT-37]
_ = x[AVXSLOW-38]
_ = x[AVXVNNI-39]
_ = x[AVXVNNIINT8-40]
_ = x[AVXVNNIINT16-41]
_ = x[BHI_CTRL-42]
_ = x[BMI1-43]
_ = x[BMI2-44]
_ = x[CETIBT-45]
_ = x[CETSS-46]
_ = x[CLDEMOTE-47]
_ = x[CLMUL-48]
_ = x[CLZERO-49]
_ = x[CMOV-50]
_ = x[CMPCCXADD-51]
_ = x[CMPSB_SCADBS_SHORT-52]
_ = x[CMPXCHG8-53]
_ = x[CPBOOST-54]
_ = x[CPPC-55]
_ = x[CX16-56]
_ = x[EFER_LMSLE_UNS-57]
_ = x[ENQCMD-58]
_ = x[ERMS-59]
_ = x[F16C-60]
_ = x[FLUSH_L1D-61]
_ = x[FMA3-62]
_ = x[FMA4-63]
_ = x[FP128-64]
_ = x[FP256-65]
_ = x[FSRM-66]
_ = x[FXSR-67]
_ = x[FXSROPT-68]
_ = x[GFNI-69]
_ = x[HLE-70]
_ = x[HRESET-71]
_ = x[HTT-72]
_ = x[HWA-73]
_ = x[HYBRID_CPU-74]
_ = x[HYPERVISOR-75]
_ = x[IA32_ARCH_CAP-76]
_ = x[IA32_CORE_CAP-77]
_ = x[IBPB-78]
_ = x[IBPB_BRTYPE-79]
_ = x[IBRS-80]
_ = x[IBRS_PREFERRED-81]
_ = x[IBRS_PROVIDES_SMP-82]
_ = x[IBS-83]
_ = x[IBSBRNTRGT-84]
_ = x[IBSFETCHSAM-85]
_ = x[IBSFFV-86]
_ = x[IBSOPCNT-87]
_ = x[IBSOPCNTEXT-88]
_ = x[IBSOPSAM-89]
_ = x[IBSRDWROPCNT-90]
_ = x[IBSRIPINVALIDCHK-91]
_ = x[IBS_FETCH_CTLX-92]
_ = x[IBS_OPDATA4-93]
_ = x[IBS_OPFUSE-94]
_ = x[IBS_PREVENTHOST-95]
_ = x[IBS_ZEN4-96]
_ = x[IDPRED_CTRL-97]
_ = x[INT_WBINVD-98]
_ = x[INVLPGB-99]
_ = x[KEYLOCKER-100]
_ = x[KEYLOCKERW-101]
_ = x[LAHF-102]
_ = x[LAM-103]
_ = x[LBRVIRT-104]
_ = x[LZCNT-105]
_ = x[MCAOVERFLOW-106]
_ = x[MCDT_NO-107]
_ = x[MCOMMIT-108]
_ = x[MD_CLEAR-109]
_ = x[MMX-110]
_ = x[MMXEXT-111]
_ = x[MOVBE-112]
_ = x[MOVDIR64B-113]
_ = x[MOVDIRI-114]
_ = x[MOVSB_ZL-115]
_ = x[MOVU-116]
_ = x[MPX-117]
_ = x[MSRIRC-118]
_ = x[MSRLIST-119]
_ = x[MSR_PAGEFLUSH-120]
_ = x[NRIPS-121]
_ = x[NX-122]
_ = x[OSXSAVE-123]
_ = x[PCONFIG-124]
_ = x[POPCNT-125]
_ = x[PPIN-126]
_ = x[PREFETCHI-127]
_ = x[PSFD-128]
_ = x[RDPRU-129]
_ = x[RDRAND-130]
_ = x[RDSEED-131]
_ = x[RDTSCP-132]
_ = x[RRSBA_CTRL-133]
_ = x[RTM-134]
_ = x[RTM_ALWAYS_ABORT-135]
_ = x[SBPB-136]
_ = x[SERIALIZE-137]
_ = x[SEV-138]
_ = x[SEV_64BIT-139]
_ = x[SEV_ALTERNATIVE-140]
_ = x[SEV_DEBUGSWAP-141]
_ = x[SEV_ES-142]
_ = x[SEV_RESTRICTED-143]
_ = x[SEV_SNP-144]
_ = x[SGX-145]
_ = x[SGXLC-146]
_ = x[SGXPQC-147]
_ = x[SHA-148]
_ = x[SME-149]
_ = x[SME_COHERENT-150]
_ = x[SM3_X86-151]
_ = x[SM4_X86-152]
_ = x[SPEC_CTRL_SSBD-153]
_ = x[SRBDS_CTRL-154]
_ = x[SRSO_MSR_FIX-155]
_ = x[SRSO_NO-156]
_ = x[SRSO_USER_KERNEL_NO-157]
_ = x[SSE-158]
_ = x[SSE2-159]
_ = x[SSE3-160]
_ = x[SSE4-161]
_ = x[SSE42-162]
_ = x[SSE4A-163]
_ = x[SSSE3-164]
_ = x[STIBP-165]
_ = x[STIBP_ALWAYSON-166]
_ = x[STOSB_SHORT-167]
_ = x[SUCCOR-168]
_ = x[SVM-169]
_ = x[SVMDA-170]
_ = x[SVMFBASID-171]
_ = x[SVML-172]
_ = x[SVMNP-173]
_ = x[SVMPF-174]
_ = x[SVMPFT-175]
_ = x[SYSCALL-176]
_ = x[SYSEE-177]
_ = x[TBM-178]
_ = x[TDX_GUEST-179]
_ = x[TLB_FLUSH_NESTED-180]
_ = x[TME-181]
_ = x[TOPEXT-182]
_ = x[TSA_L1_NO-183]
_ = x[TSA_SQ_NO-184]
_ = x[TSA_VERW_CLEAR-185]
_ = x[TSCRATEMSR-186]
_ = x[TSXLDTRK-187]
_ = x[VAES-188]
_ = x[VMCBCLEAN-189]
_ = x[VMPL-190]
_ = x[VMSA_REGPROT-191]
_ = x[VMX-192]
_ = x[VPCLMULQDQ-193]
_ = x[VTE-194]
_ = x[WAITPKG-195]
_ = x[WBNOINVD-196]
_ = x[WRMSRNS-197]
_ = x[X87-198]
_ = x[XGETBV1-199]
_ = x[XOP-200]
_ = x[XSAVE-201]
_ = x[XSAVEC-202]
_ = x[XSAVEOPT-203]
_ = x[XSAVES-204]
_ = x[AESARM-205]
_ = x[ARMCPUID-206]
_ = x[ASIMD-207]
_ = x[ASIMDDP-208]
_ = x[ASIMDHP-209]
_ = x[ASIMDRDM-210]
_ = x[ATOMICS-211]
_ = x[CRC32-212]
_ = x[DCPOP-213]
_ = x[EVTSTRM-214]
_ = x[FCMA-215]
_ = x[FHM-216]
_ = x[FP-217]
_ = x[FPHP-218]
_ = x[GPA-219]
_ = x[JSCVT-220]
_ = x[LRCPC-221]
_ = x[PMULL-222]
_ = x[RNDR-223]
_ = x[TLB-224]
_ = x[TS-225]
_ = x[SHA1-226]
_ = x[SHA2-227]
_ = x[SHA3-228]
_ = x[SHA512-229]
_ = x[SM3-230]
_ = x[SM4-231]
_ = x[SVE-232]
_ = x[PMU_FIXEDCOUNTER_CYCLES-233]
_ = x[PMU_FIXEDCOUNTER_REFCYCLES-234]
_ = x[PMU_FIXEDCOUNTER_INSTRUCTIONS-235]
_ = x[PMU_FIXEDCOUNTER_TOPDOWN_SLOTS-236]
_ = x[lastID-237]
_ = x[firstID-0]
}
const _FeatureID_name = "firstIDADXAESNIAMD3DNOWAMD3DNOWEXTAMXBF16AMXFP16AMXINT8AMXFP8AMXTILEAMXTF32AMXCOMPLEXAMXTRANSPOSEAPX_FAVXAVX10AVX10_128AVX10_256AVX10_512AVX2AVX512BF16AVX512BITALGAVX512BWAVX512CDAVX512DQAVX512ERAVX512FAVX512FP16AVX512IFMAAVX512PFAVX512VBMIAVX512VBMI2AVX512VLAVX512VNNIAVX512VP2INTERSECTAVX512VPOPCNTDQAVXIFMAAVXNECONVERTAVXSLOWAVXVNNIAVXVNNIINT8AVXVNNIINT16BHI_CTRLBMI1BMI2CETIBTCETSSCLDEMOTECLMULCLZEROCMOVCMPCCXADDCMPSB_SCADBS_SHORTCMPXCHG8CPBOOSTCPPCCX16EFER_LMSLE_UNSENQCMDERMSF16CFLUSH_L1DFMA3FMA4FP128FP256FSRMFXSRFXSROPTGFNIHLEHRESETHTTHWAHYBRID_CPUHYPERVISORIA32_ARCH_CAPIA32_CORE_CAPIBPBIBPB_BRTYPEIBRSIBRS_PREFERREDIBRS_PROVIDES_SMPIBSIBSBRNTRGTIBSFETCHSAMIBSFFVIBSOPCNTIBSOPCNTEXTIBSOPSAMIBSRDWROPCNTIBSRIPINVALIDCHKIBS_FETCH_CTLXIBS_OPDATA4IBS_OPFUSEIBS_PREVENTHOSTIBS_ZEN4IDPRED_CTRLINT_WBINVDINVLPGBKEYLOCKERKEYLOCKERWLAHFLAMLBRVIRTLZCNTMCAOVERFLOWMCDT_NOMCOMMITMD_CLEARMMXMMXEXTMOVBEMOVDIR64BMOVDIRIMOVSB_ZLMOVUMPXMSRIRCMSRLISTMSR_PAGEFLUSHNRIPSNXOSXSAVEPCONFIGPOPCNTPPINPREFETCHIPSFDRDPRURDRANDRDSEEDRDTSCPRRSBA_CTRLRTMRTM_ALWAYS_ABORTSBPBSERIALIZESEVSEV_64BITSEV_ALTERNATIVESEV_DEBUGSWAPSEV_ESSEV_RESTRICTEDSEV_SNPSGXSGXLCSGXPQCSHASMESME_COHERENTSM3_X86SM4_X86SPEC_CTRL_SSBDSRBDS_CTRLSRSO_MSR_FIXSRSO_NOSRSO_USER_KERNEL_NOSSESSE2SSE3SSE4SSE42SSE4ASSSE3STIBPSTIBP_ALWAYSONSTOSB_SHORTSUCCORSVMSVMDASVMFBASIDSVMLSVMNPSVMPFSVMPFTSYSCALLSYSEETBMTDX_GUESTTLB_FLUSH_NESTEDTMETOPEXTTSA_L1_NOTSA_SQ_NOTSA_VERW_CLEARTSCRATEMSRTSXLDTRKVAESVMCBCLEANVMPLVMSA_REGPROTVMXVPCLMULQDQVTEWAITPKGWBNOINVDWRMSRNSX87XGETBV1XOPXSAVEXSAVECXSAVEOPTXSAVESAESARMARMCPUIDASIMDASIMDDPASIMDHPASIMDRDMATOMICSCRC32DCPOPEVTSTRMFCMAFHMFPFPHPGPAJSCVTLRCPCPMULLRNDRTLBTSSHA1SHA2SHA3SHA512SM3SM4SVEPMU_FIXEDCOUNTER_CYCLESPMU_FIXEDCOUNTER_REFCYCLESPMU_FIXEDCOUNTER_INSTRUCTIONSPMU_FIXEDCOUNTER_TOPDOWN_SLOTSlastID"
var _FeatureID_index = [...]uint16{0, 7, 10, 15, 23, 34, 41, 48, 55, 61, 68, 75, 85, 97, 102, 105, 110, 119, 128, 137, 141, 151, 163, 171, 179, 187, 195, 202, 212, 222, 230, 240, 251, 259, 269, 287, 302, 309, 321, 328, 335, 346, 358, 366, 370, 374, 380, 385, 393, 398, 404, 408, 417, 435, 443, 450, 454, 458, 472, 478, 482, 486, 495, 499, 503, 508, 513, 517, 521, 528, 532, 535, 541, 544, 547, 557, 567, 580, 593, 597, 608, 612, 626, 643, 646, 656, 667, 673, 681, 692, 700, 712, 728, 742, 753, 763, 778, 786, 797, 807, 814, 823, 833, 837, 840, 847, 852, 863, 870, 877, 885, 888, 894, 899, 908, 915, 923, 927, 930, 936, 943, 956, 961, 963, 970, 977, 983, 987, 996, 1000, 1005, 1011, 1017, 1023, 1033, 1036, 1052, 1056, 1065, 1068, 1077, 1092, 1105, 1111, 1125, 1132, 1135, 1140, 1146, 1149, 1152, 1164, 1171, 1178, 1192, 1202, 1214, 1221, 1240, 1243, 1247, 1251, 1255, 1260, 1265, 1270, 1275, 1289, 1300, 1306, 1309, 1314, 1323, 1327, 1332, 1337, 1343, 1350, 1355, 1358, 1367, 1383, 1386, 1392, 1401, 1410, 1424, 1434, 1442, 1446, 1455, 1459, 1471, 1474, 1484, 1487, 1494, 1502, 1509, 1512, 1519, 1522, 1527, 1533, 1541, 1547, 1553, 1561, 1566, 1573, 1580, 1588, 1595, 1600, 1605, 1612, 1616, 1619, 1621, 1625, 1628, 1633, 1638, 1643, 1647, 1650, 1652, 1656, 1660, 1664, 1670, 1673, 1676, 1679, 1702, 1728, 1757, 1787, 1793}
func (i FeatureID) String() string {
if i < 0 || i >= FeatureID(len(_FeatureID_index)-1) {
return "FeatureID(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _FeatureID_name[_FeatureID_index[i]:_FeatureID_index[i+1]]
}
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[VendorUnknown-0]
_ = x[Intel-1]
_ = x[AMD-2]
_ = x[VIA-3]
_ = x[Transmeta-4]
_ = x[NSC-5]
_ = x[KVM-6]
_ = x[MSVM-7]
_ = x[VMware-8]
_ = x[XenHVM-9]
_ = x[Bhyve-10]
_ = x[Hygon-11]
_ = x[SiS-12]
_ = x[RDC-13]
_ = x[Ampere-14]
_ = x[ARM-15]
_ = x[Broadcom-16]
_ = x[Cavium-17]
_ = x[DEC-18]
_ = x[Fujitsu-19]
_ = x[Infineon-20]
_ = x[Motorola-21]
_ = x[NVIDIA-22]
_ = x[AMCC-23]
_ = x[Qualcomm-24]
_ = x[Marvell-25]
_ = x[QEMU-26]
_ = x[QNX-27]
_ = x[ACRN-28]
_ = x[SRE-29]
_ = x[Apple-30]
_ = x[lastVendor-31]
}
const _Vendor_name = "VendorUnknownIntelAMDVIATransmetaNSCKVMMSVMVMwareXenHVMBhyveHygonSiSRDCAmpereARMBroadcomCaviumDECFujitsuInfineonMotorolaNVIDIAAMCCQualcommMarvellQEMUQNXACRNSREApplelastVendor"
var _Vendor_index = [...]uint8{0, 13, 18, 21, 24, 33, 36, 39, 43, 49, 55, 60, 65, 68, 71, 77, 80, 88, 94, 97, 104, 112, 120, 126, 130, 138, 145, 149, 152, 156, 159, 164, 174}
func (i Vendor) String() string {
if i < 0 || i >= Vendor(len(_Vendor_index)-1) {
return "Vendor(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _Vendor_name[_Vendor_index[i]:_Vendor_index[i+1]]
}
+129
View File
@@ -0,0 +1,129 @@
// Copyright (c) 2020 Klaus Post, released under MIT License. See LICENSE file.
package cpuid
import (
"runtime"
"strings"
"golang.org/x/sys/unix"
)
func detectOS(c *CPUInfo) bool {
if runtime.GOOS != "ios" {
tryToFillCPUInfoFomSysctl(c)
}
// There are no hw.optional sysctl values for the below features on Mac OS 11.0
// to detect their supported state dynamically. Assume the CPU features that
// Apple Silicon M1 supports to be available as a minimal set of features
// to all Go programs running on darwin/arm64.
// TODO: Add more if we know them.
c.featureSet.setIf(runtime.GOOS != "ios", AESARM, PMULL, SHA1, SHA2)
return true
}
func sysctlGetBool(name string) bool {
value, err := unix.SysctlUint32(name)
if err != nil {
return false
}
return value != 0
}
func sysctlGetString(name string) string {
value, err := unix.Sysctl(name)
if err != nil {
return ""
}
return value
}
func sysctlGetInt(unknown int, names ...string) int {
for _, name := range names {
value, err := unix.SysctlUint32(name)
if err != nil {
continue
}
if value != 0 {
return int(value)
}
}
return unknown
}
func sysctlGetInt64(unknown int, names ...string) int {
for _, name := range names {
value64, err := unix.SysctlUint64(name)
if err != nil {
continue
}
if int(value64) != unknown {
return int(value64)
}
}
return unknown
}
func setFeature(c *CPUInfo, feature FeatureID, aliases ...string) {
for _, alias := range aliases {
set := sysctlGetBool(alias)
c.featureSet.setIf(set, feature)
if set {
break
}
}
}
func tryToFillCPUInfoFomSysctl(c *CPUInfo) {
c.BrandName = sysctlGetString("machdep.cpu.brand_string")
if len(c.BrandName) != 0 {
c.VendorString = strings.Fields(c.BrandName)[0]
}
c.PhysicalCores = sysctlGetInt(runtime.NumCPU(), "hw.physicalcpu")
c.ThreadsPerCore = sysctlGetInt(1, "machdep.cpu.thread_count", "kern.num_threads") /
sysctlGetInt(1, "hw.physicalcpu")
c.LogicalCores = sysctlGetInt(runtime.NumCPU(), "machdep.cpu.core_count")
c.Family = sysctlGetInt(0, "machdep.cpu.family", "hw.cpufamily")
c.Model = sysctlGetInt(0, "machdep.cpu.model")
c.CacheLine = sysctlGetInt64(0, "hw.cachelinesize")
c.Cache.L1I = sysctlGetInt64(-1, "hw.l1icachesize")
c.Cache.L1D = sysctlGetInt64(-1, "hw.l1dcachesize")
c.Cache.L2 = sysctlGetInt64(-1, "hw.l2cachesize")
c.Cache.L3 = sysctlGetInt64(-1, "hw.l3cachesize")
// ARM features:
//
// Note: On some Apple Silicon system, some feats have aliases. See:
// https://developer.apple.com/documentation/kernel/1387446-sysctlbyname/determining_instruction_set_characteristics
// When so, we look at all aliases and consider a feature available when at least one identifier matches.
setFeature(c, AESARM, "hw.optional.arm.FEAT_AES") // AES instructions
setFeature(c, ASIMD, "hw.optional.arm.AdvSIMD", "hw.optional.neon") // Advanced SIMD
setFeature(c, ASIMDDP, "hw.optional.arm.FEAT_DotProd") // SIMD Dot Product
setFeature(c, ASIMDHP, "hw.optional.arm.AdvSIMD_HPFPCvt", "hw.optional.neon_hpfp") // Advanced SIMD half-precision floating point
setFeature(c, ASIMDRDM, "hw.optional.arm.FEAT_RDM") // Rounding Double Multiply Accumulate/Subtract
setFeature(c, ATOMICS, "hw.optional.arm.FEAT_LSE", "hw.optional.armv8_1_atomics") // Large System Extensions (LSE)
setFeature(c, CRC32, "hw.optional.arm.FEAT_CRC32", "hw.optional.armv8_crc32") // CRC32/CRC32C instructions
setFeature(c, DCPOP, "hw.optional.arm.FEAT_DPB") // Data cache clean to Point of Persistence (DC CVAP)
setFeature(c, EVTSTRM, "hw.optional.arm.FEAT_ECV") // Generic timer
setFeature(c, FCMA, "hw.optional.arm.FEAT_FCMA", "hw.optional.armv8_3_compnum") // Floating point complex number addition and multiplication
setFeature(c, FHM, "hw.optional.armv8_2_fhm", "hw.optional.arm.FEAT_FHM") // FMLAL and FMLSL instructions
setFeature(c, FP, "hw.optional.floatingpoint") // Single-precision and double-precision floating point
setFeature(c, FPHP, "hw.optional.arm.FEAT_FP16", "hw.optional.neon_fp16") // Half-precision floating point
setFeature(c, GPA, "hw.optional.arm.FEAT_PAuth") // Generic Pointer Authentication
setFeature(c, JSCVT, "hw.optional.arm.FEAT_JSCVT") // Javascript-style double->int convert (FJCVTZS)
setFeature(c, LRCPC, "hw.optional.arm.FEAT_LRCPC") // Weaker release consistency (LDAPR, etc)
setFeature(c, PMULL, "hw.optional.arm.FEAT_PMULL") // Polynomial Multiply instructions (PMULL/PMULL2)
setFeature(c, RNDR, "hw.optional.arm.FEAT_RNG") // Random Number instructions
setFeature(c, TLB, "hw.optional.arm.FEAT_TLBIOS", "hw.optional.arm.FEAT_TLBIRANGE") // Outer Shareable and TLB range maintenance instructions
setFeature(c, TS, "hw.optional.arm.FEAT_FlagM", "hw.optional.arm.FEAT_FlagM2") // Flag manipulation instructions
setFeature(c, SHA1, "hw.optional.arm.FEAT_SHA1") // SHA-1 instructions (SHA1C, etc)
setFeature(c, SHA2, "hw.optional.arm.FEAT_SHA256") // SHA-2 instructions (SHA256H, etc)
setFeature(c, SHA3, "hw.optional.arm.FEAT_SHA3") // SHA-3 instructions (EOR3, RAXI, XAR, BCAX)
setFeature(c, SHA512, "hw.optional.arm.FEAT_SHA512") // SHA512 instructions
setFeature(c, SM3, "hw.optional.arm.FEAT_SM3") // SM3 instructions
setFeature(c, SM4, "hw.optional.arm.FEAT_SM4") // SM4 instructions
setFeature(c, SVE, "hw.optional.arm.FEAT_SVE") // Scalable Vector Extension
}
+208
View File
@@ -0,0 +1,208 @@
// Copyright (c) 2020 Klaus Post, released under MIT License. See LICENSE file.
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file located
// here https://github.com/golang/sys/blob/master/LICENSE
package cpuid
import (
"encoding/binary"
"io/ioutil"
"runtime"
)
// HWCAP bits.
const (
hwcap_FP = 1 << 0
hwcap_ASIMD = 1 << 1
hwcap_EVTSTRM = 1 << 2
hwcap_AES = 1 << 3
hwcap_PMULL = 1 << 4
hwcap_SHA1 = 1 << 5
hwcap_SHA2 = 1 << 6
hwcap_CRC32 = 1 << 7
hwcap_ATOMICS = 1 << 8
hwcap_FPHP = 1 << 9
hwcap_ASIMDHP = 1 << 10
hwcap_CPUID = 1 << 11
hwcap_ASIMDRDM = 1 << 12
hwcap_JSCVT = 1 << 13
hwcap_FCMA = 1 << 14
hwcap_LRCPC = 1 << 15
hwcap_DCPOP = 1 << 16
hwcap_SHA3 = 1 << 17
hwcap_SM3 = 1 << 18
hwcap_SM4 = 1 << 19
hwcap_ASIMDDP = 1 << 20
hwcap_SHA512 = 1 << 21
hwcap_SVE = 1 << 22
hwcap_ASIMDFHM = 1 << 23
hwcap_DIT = 1 << 24
hwcap_USCAT = 1 << 25
hwcap_ILRCPC = 1 << 26
hwcap_FLAGM = 1 << 27
hwcap_SSBS = 1 << 28
hwcap_SB = 1 << 29
hwcap_PACA = 1 << 30
hwcap_PACG = 1 << 31
hwcap_GCS = 1 << 32
hwcap2_DCPODP = 1 << 0
hwcap2_SVE2 = 1 << 1
hwcap2_SVEAES = 1 << 2
hwcap2_SVEPMULL = 1 << 3
hwcap2_SVEBITPERM = 1 << 4
hwcap2_SVESHA3 = 1 << 5
hwcap2_SVESM4 = 1 << 6
hwcap2_FLAGM2 = 1 << 7
hwcap2_FRINT = 1 << 8
hwcap2_SVEI8MM = 1 << 9
hwcap2_SVEF32MM = 1 << 10
hwcap2_SVEF64MM = 1 << 11
hwcap2_SVEBF16 = 1 << 12
hwcap2_I8MM = 1 << 13
hwcap2_BF16 = 1 << 14
hwcap2_DGH = 1 << 15
hwcap2_RNG = 1 << 16
hwcap2_BTI = 1 << 17
hwcap2_MTE = 1 << 18
hwcap2_ECV = 1 << 19
hwcap2_AFP = 1 << 20
hwcap2_RPRES = 1 << 21
hwcap2_MTE3 = 1 << 22
hwcap2_SME = 1 << 23
hwcap2_SME_I16I64 = 1 << 24
hwcap2_SME_F64F64 = 1 << 25
hwcap2_SME_I8I32 = 1 << 26
hwcap2_SME_F16F32 = 1 << 27
hwcap2_SME_B16F32 = 1 << 28
hwcap2_SME_F32F32 = 1 << 29
hwcap2_SME_FA64 = 1 << 30
hwcap2_WFXT = 1 << 31
hwcap2_EBF16 = 1 << 32
hwcap2_SVE_EBF16 = 1 << 33
hwcap2_CSSC = 1 << 34
hwcap2_RPRFM = 1 << 35
hwcap2_SVE2P1 = 1 << 36
hwcap2_SME2 = 1 << 37
hwcap2_SME2P1 = 1 << 38
hwcap2_SME_I16I32 = 1 << 39
hwcap2_SME_BI32I32 = 1 << 40
hwcap2_SME_B16B16 = 1 << 41
hwcap2_SME_F16F16 = 1 << 42
hwcap2_MOPS = 1 << 43
hwcap2_HBC = 1 << 44
hwcap2_SVE_B16B16 = 1 << 45
hwcap2_LRCPC3 = 1 << 46
hwcap2_LSE128 = 1 << 47
hwcap2_FPMR = 1 << 48
hwcap2_LUT = 1 << 49
hwcap2_FAMINMAX = 1 << 50
hwcap2_F8CVT = 1 << 51
hwcap2_F8FMA = 1 << 52
hwcap2_F8DP4 = 1 << 53
hwcap2_F8DP2 = 1 << 54
hwcap2_F8E4M3 = 1 << 55
hwcap2_F8E5M2 = 1 << 56
hwcap2_SME_LUTV2 = 1 << 57
hwcap2_SME_F8F16 = 1 << 58
hwcap2_SME_F8F32 = 1 << 59
hwcap2_SME_SF8FMA = 1 << 60
hwcap2_SME_SF8DP4 = 1 << 61
hwcap2_SME_SF8DP2 = 1 << 62
hwcap2_POE = 1 << 63
)
func detectOS(c *CPUInfo) bool {
// For now assuming no hyperthreading is reasonable.
c.LogicalCores = runtime.NumCPU()
c.PhysicalCores = c.LogicalCores
c.ThreadsPerCore = 1
if hwcap == 0 {
// We did not get values from the runtime.
// Try reading /proc/self/auxv
// From https://github.com/golang/sys
const (
_AT_HWCAP = 16
_AT_HWCAP2 = 26
uintSize = int(32 << (^uint(0) >> 63))
)
buf, err := ioutil.ReadFile("/proc/self/auxv")
if err != nil {
// e.g. on android /proc/self/auxv is not accessible, so silently
// ignore the error and leave Initialized = false. On some
// architectures (e.g. arm64) doinit() implements a fallback
// readout and will set Initialized = true again.
return false
}
bo := binary.LittleEndian
for len(buf) >= 2*(uintSize/8) {
var tag, val uint
switch uintSize {
case 32:
tag = uint(bo.Uint32(buf[0:]))
val = uint(bo.Uint32(buf[4:]))
buf = buf[8:]
case 64:
tag = uint(bo.Uint64(buf[0:]))
val = uint(bo.Uint64(buf[8:]))
buf = buf[16:]
}
switch tag {
case _AT_HWCAP:
hwcap = val
case _AT_HWCAP2:
// Not used
}
}
if hwcap == 0 {
return false
}
}
// HWCap was populated by the runtime from the auxiliary vector.
// Use HWCap information since reading aarch64 system registers
// is not supported in user space on older linux kernels.
c.featureSet.setIf(isSet(hwcap, hwcap_AES), AESARM)
c.featureSet.setIf(isSet(hwcap, hwcap_ASIMD), ASIMD)
c.featureSet.setIf(isSet(hwcap, hwcap_ASIMDDP), ASIMDDP)
c.featureSet.setIf(isSet(hwcap, hwcap_ASIMDHP), ASIMDHP)
c.featureSet.setIf(isSet(hwcap, hwcap_ASIMDRDM), ASIMDRDM)
c.featureSet.setIf(isSet(hwcap, hwcap_CPUID), ARMCPUID)
c.featureSet.setIf(isSet(hwcap, hwcap_CRC32), CRC32)
c.featureSet.setIf(isSet(hwcap, hwcap_DCPOP), DCPOP)
c.featureSet.setIf(isSet(hwcap, hwcap_EVTSTRM), EVTSTRM)
c.featureSet.setIf(isSet(hwcap, hwcap_FCMA), FCMA)
c.featureSet.setIf(isSet(hwcap, hwcap_ASIMDFHM), FHM)
c.featureSet.setIf(isSet(hwcap, hwcap_FP), FP)
c.featureSet.setIf(isSet(hwcap, hwcap_FPHP), FPHP)
c.featureSet.setIf(isSet(hwcap, hwcap_JSCVT), JSCVT)
c.featureSet.setIf(isSet(hwcap, hwcap_LRCPC), LRCPC)
c.featureSet.setIf(isSet(hwcap, hwcap_PMULL), PMULL)
c.featureSet.setIf(isSet(hwcap, hwcap2_RNG), RNDR)
// c.featureSet.setIf(isSet(hwcap, hwcap_), TLB)
// c.featureSet.setIf(isSet(hwcap, hwcap_), TS)
c.featureSet.setIf(isSet(hwcap, hwcap_SHA1), SHA1)
c.featureSet.setIf(isSet(hwcap, hwcap_SHA2), SHA2)
c.featureSet.setIf(isSet(hwcap, hwcap_SHA3), SHA3)
c.featureSet.setIf(isSet(hwcap, hwcap_SHA512), SHA512)
c.featureSet.setIf(isSet(hwcap, hwcap_SM3), SM3)
c.featureSet.setIf(isSet(hwcap, hwcap_SM4), SM4)
c.featureSet.setIf(isSet(hwcap, hwcap_SVE), SVE)
// The Samsung S9+ kernel reports support for atomics, but not all cores
// actually support them, resulting in SIGILL. See issue #28431.
// TODO(elias.naur): Only disable the optimization on bad chipsets on android.
c.featureSet.setIf(isSet(hwcap, hwcap_ATOMICS) && runtime.GOOS != "android", ATOMICS)
return true
}
func isSet(hwc uint, value uint) bool {
return hwc&value != 0
}
+16
View File
@@ -0,0 +1,16 @@
// Copyright (c) 2020 Klaus Post, released under MIT License. See LICENSE file.
//go:build arm64 && !linux && !darwin
// +build arm64,!linux,!darwin
package cpuid
import "runtime"
func detectOS(c *CPUInfo) bool {
c.PhysicalCores = runtime.NumCPU()
// For now assuming 1 thread per core...
c.ThreadsPerCore = 1
c.LogicalCores = c.PhysicalCores
return false
}
+8
View File
@@ -0,0 +1,8 @@
// Copyright (c) 2021 Klaus Post, released under MIT License. See LICENSE file.
//go:build nounsafe
// +build nounsafe
package cpuid
var hwcap uint
+11
View File
@@ -0,0 +1,11 @@
// Copyright (c) 2021 Klaus Post, released under MIT License. See LICENSE file.
//go:build !nounsafe
// +build !nounsafe
package cpuid
import _ "unsafe" // needed for go:linkname
//go:linkname hwcap internal/cpu.HWCap
var hwcap uint
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
set -e
go tool dist list | while IFS=/ read os arch; do
echo "Checking $os/$arch..."
echo " normal"
GOARCH=$arch GOOS=$os go build -o /dev/null .
echo " noasm"
GOARCH=$arch GOOS=$os go build -tags noasm -o /dev/null .
echo " appengine"
GOARCH=$arch GOOS=$os go build -tags appengine -o /dev/null .
echo " noasm,appengine"
GOARCH=$arch GOOS=$os go build -tags 'appengine noasm' -o /dev/null .
done
-64
View File
@@ -1,64 +0,0 @@
# AGENTS.md - gotext Project Guidelines
This document is the **source of truth** for the `gotext` project's architecture, conventions, and development workflows. All contributors — human or AI agent — must adhere to these guidelines.
Agent-specific configuration files (e.g., `GEMINI.md`, `CLAUDE.md`) should reference this document rather than duplicating its content.
## Project Overview
`gotext` is a GNU Gettext implementation for Go (Golang). It provides a thread-safe, hierarchical translation system that supports PO/MO files, plural forms, and context-aware translations.
## Core Architecture
The project follows a hierarchical structure for managing translations:
1. **Global Package API (`gotext.go`)**: Provides convenient, global functions (e.g., `Get`, `GetD`, `GetN`) that rely on a shared `globalConfig`.
2. **Locales (`locale.go`)**: Represent a specific language (e.g., "en_US", "es_ES"). A `Locale` container manages multiple `Domains`.
3. **Domains (`domain.go`)**: The core translation engine for a specific set of messages. It handles:
- Message retrieval (with support for plural forms and `msgctxt`).
- Header parsing (including plural-form rules).
- Thread-safety via `sync.RWMutex`.
4. **Translators (`translator.go`)**: An interface for translation sources.
- **PO (`po.go`)**: Parsers for human-readable Gettext Portable Object files.
- **MO (`mo.go`)**: Parsers for binary Gettext Machine Object files.
5. **Plural Forms (`plurals/`)**: A dedicated package for compiling and evaluating GNU Gettext plural form expressions.
## CLI Tools
- **xgotext (`cli/xgotext/`)**: A tool to scan Go source files, extract translatable strings (matching `Get`, `GetD`, etc.), and generate/update PO files.
## Key Files & Symbols
- `gotext.go`: `Configure`, `Get`, `GetD`, `GetN`.
- `locale.go`: `Locale`, `NewLocale`, `AddDomain`.
- `domain.go`: `Domain`, `Get`, `GetN`, `parseHeaders`.
- `translator.go`: `Translator` interface.
- `plurals/compiler.go`: `Compile` function for plural expressions.
## Development Workflows
### Branching & PRs
- **Feature Branches**: All development MUST happen in dedicated feature branches (e.g., `feature/description-of-change`, `fix/bug-description`, `refactor/topic`).
- **Target Branch**: All Pull Requests MUST be made against the `master` branch.
- **Review Requirement**: No direct commits to `master` are allowed. All changes must be reviewed via a PR.
- **GitHub Integration**: If the `gh` CLI tool is installed and authenticated, it should be used for managing Pull Requests, Issues, and Reviews. Otherwise, these tasks must be performed manually through the GitHub web interface.
### Coding Standards
- **Idiomatic Go**: Follow standard Go conventions (`gofmt`, `go vet`).
- **Thread Safety**: Ensure all shared state (like in `Domain`) is protected by appropriate synchronization primitives.
- **Testing**:
- Every new feature or bug fix must include corresponding tests.
- Use the `fixtures/` directory for sample PO/MO files.
- Run `go test ./...` to ensure no regressions.
### Pre-Submit Checklist
Before submitting a Pull Request, verify:
1. All existing tests pass: `go test ./...`
2. Code passes static analysis: `go vet ./...`
3. Code is properly formatted: `gofmt`
4. New features or bug fixes include corresponding tests.
5. Commit messages are clear and descriptive.
+45 -2
View File
@@ -1,3 +1,46 @@
# CODE OF CONDUCT
# Contributor Covenant Code of Conduct
The canonical text of this project's Code of Conduct lives at [docs/CODE_OF_CONDUCT.md](docs/CODE_OF_CONDUCT.md).
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at leonel.quinteros@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/
+14 -4
View File
@@ -1,9 +1,19 @@
# CONTRIBUTING
Thank you for considering contributing to `gotext`! All kinds of contributions — features, bug fixes, tests, documentation, or discussions — are welcome.
This open source project welcomes everybody that wants to contribute to it by implementing new features, fixing bugs, testing, creating documentation or simply talk about it.
The canonical contribution guidelines live at [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md).
Most contributions will start by creating a new Issue to discuss what is the contribution about and to agree on the steps to move forward.
Before contributing, please also read [AGENTS.md](AGENTS.md): it is the source of truth for project architecture, conventions, and development workflows (branching, PR process, coding standards, and the pre-submit checklist).
## Issues
All issues reports are welcome. Open a new Issue whenever you want to report a bug, request a change or make a proposal.
This should be your start point of contribution.
## Pull Requests
If you have any changes that can be merged, feel free to send a Pull Request.
Usually, you'd want to create a new Issue to discuss about the change you want to merge and why it's needed or what it solves.
Start point: open an [Issue](https://github.com/leonelquinteros/gotext/issues) to discuss your idea, then send a Pull Request against `master`.
-9
View File
@@ -1,9 +0,0 @@
# GEMINI.md - Gemini CLI Instructions for gotext
> **Important**: Read and follow [`AGENTS.md`](./AGENTS.md) first. It is the source of truth for project architecture, conventions, and development workflows. The instructions below are specific to Gemini CLI and supplement — not replace — those guidelines.
## Gemini-Specific Instructions
- Always work in a feature branch. Never commit directly to `master`.
- Before submitting, verify changes with existing and new tests (`go test ./...`).
- Propose a PR to `master` after confirmation, using the `gh` CLI when available.
+206 -99
View File
@@ -9,151 +9,258 @@
[GNU gettext utilities](https://www.gnu.org/software/gettext) for Go.
`gotext` is a native Go implementation of the GNU Gettext utilities. It provides a thread-safe, flexible, and powerful way to handle internationalization (i18n) and localization (l10n) in your Go applications.
---
# Features
**📖 [Read the Full Documentation](https://leonelquinteros.github.io/gotext/)**
- Implements GNU gettext support in native Go.
- Complete support for [PO files](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) including:
- Support for multiline strings and headers.
- Support for variables inside translation strings using Go's [fmt syntax](https://golang.org/pkg/fmt/).
- Support for [pluralization rules](https://www.gnu.org/software/gettext/manual/html_node/Translating-plural-forms.html).
- Support for [message contexts](https://www.gnu.org/software/gettext/manual/html_node/Contexts.html).
- Support for MO files.
- Thread-safe: This package is safe for concurrent use across multiple goroutines.
- It works with UTF-8 encoding as it's the default for Go language.
- Unit tests available.
- Language codes are automatically simplified from the form `en_UK` to `en` if the first isn't available.
- Ready to use inside Go templates.
- Objects are serializable to []byte to store them in cache.
- Support for Go Modules.
---
### Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Getting Started](#getting-started)
- [CLI Tool (xgotext)](#cli-tool-xgotext)
- [Advanced Usage](#advanced-usage)
- [Locale Object](#using-locale-object)
- [Plural Forms](#use-plural-forms-of-translations)
- [Dynamic Variables](#using-dynamic-variables-on-translations)
- [Directory Structure](#locales-directories-structure)
- [Contributing](#contributing)
- [License](#license)
# License
---
[MIT license](LICENSE)
## Features
- Native Go implementation of Gettext (the core library has no external dependencies).
- Full support for **PO and MO files**.
- **Pluralization rules** support via GNU Gettext plural-form expressions.
- **Message context** support (`msgctxt`).
- Thread-safe for concurrent use.
- Works with UTF-8 by default.
- Integrated CLI tool (`xgotext`) for string extraction.
- Serializable objects for caching.
- Plain-string results that plug directly into Go's `text/template` and `html/template` pipelines.
---
# Documentation
## Installation
```bash
Refer to package documentation at (https://pkg.go.dev/github.com/leonelquinteros/gotext)
# Installation
```
go get github.com/leonelquinteros/gotext
```
---
- There are no requirements or dependencies to use this package.
- No need to install GNU gettext utilities (unless specific needs of CLI tools).
- No need for environment variables. Some naming conventions are applied but not needed.
## Getting Started
For a quick start, use the package-level API:
# Usage examples
## Using package for single language/domain settings
For quick/simple translations you can use the package level functions directly.
```go
package main
import (
"fmt"
"github.com/leonelquinteros/gotext"
)
func main() {
// Configure package: locales path, language, and domain
gotext.Configure("/path/to/locales", "en_US", "default")
// Configure package
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
// Simple translation
fmt.Println(gotext.Get("Hello, world!"))
// Translate text from default domain
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
// Translation with variables
fmt.Println(gotext.Get("Hello, %s!", "Gopher"))
// Translate text from a different domain without reconfigure
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
}
```
## Using dynamic variables on translations
All translation strings support dynamic variables to be inserted without translate.
Use the fmt.Printf syntax (from Go's "fmt" package) to specify how to print the non-translated variable inside the translation string.
```go
import (
"fmt"
"github.com/leonelquinteros/gotext"
)
func main() {
// Configure package
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
// Set variables
name := "John"
// Translate text with variables
fmt.Println(gotext.Get("Hi, my name is %s", name))
}
```
## Using Locale object
When having multiple languages/domains/libraries at the same time, you can create Locale objects for each variation
so you can handle each settings on their own.
```go
import (
"fmt"
"github.com/leonelquinteros/gotext"
)
func main() {
// Create Locale with library path and language code
l := gotext.NewLocale("/path/to/locales/root/dir", "es_UY")
// Load domain '/path/to/locales/root/dir/es_UY/default.po'
l.AddDomain("default")
// Translate text from default domain
fmt.Println(l.Get("Translate this"))
// Load different domain
l.AddDomain("translations")
// Translate text from domain
fmt.Println(l.GetD("translations", "Translate this"))
}
```
For more details, see the [Getting Started Guide](docs/GETTING_STARTED.md).
This is also helpful for using inside templates (from the "text/template" package), where you can pass the Locale object to the template.
If you set the Locale object as "Loc" in the template, then the template code would look like:
---
## CLI Tool (xgotext)
`gotext` includes a command-line tool to extract translatable strings from your Go source code.
**Install xgotext:**
```bash
go install github.com/leonelquinteros/gotext/cli/xgotext@latest
```
{{ .Loc.Get "Translate this" }}
```
**Extract strings:**
```bash
xgotext -in . -out locales/templates
```
The command generates `.pot` templates under `locales/templates`; merge those templates into language-specific `.po` files.
See the [xgotext Documentation](docs/xgotext.md) for full usage details.
## Using the Po object to handle .po files and PO-formatted strings
---
## Advanced Usage
### Using Locale object
For managing multiple languages or domains independently:
For when you need to work with PO files and strings,
you can directly use the Po object to parse it and access the translations in there in the same way.
```go
l := gotext.NewLocale("/path/to/locales", "es_UY")
l.AddDomain("default")
fmt.Println(l.Get("Translate this"))
import (
"fmt"
"github.com/leonelquinteros/gotext"
)
func main() {
// Set PO content
str := `
msgid "Translate this"
msgstr "Translated text"
msgid "Another string"
msgstr ""
msgid "One with var: %s"
msgstr "This one sets the var: %s"
`
// Create Po object
po := gotext.NewPo()
po.Parse(str)
fmt.Println(po.Get("Translate this"))
}
```
### Use plural forms of translations
`gotext` handles complex pluralization rules defined in PO headers:
## Use plural forms of translations
PO format supports defining one or more plural forms for the same translation.
Relying on the PO file headers, a Plural-Forms formula can be set on the translation file
as defined in (https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/Plural-forms.html)
```go
// GetN(singular, plural, quantity, args...)
fmt.Println(gotext.GetN("I have one apple.", "I have %d apples.", 5, 5))
```
See the [Plural Forms Guide](docs/PLURALS.md) for more examples.
import (
"fmt"
"github.com/leonelquinteros/gotext"
)
### Using dynamic variables on translations
Supports standard `fmt` package syntax:
```go
name := "John"
fmt.Println(gotext.Get("Hi, my name is %s", name))
func main() {
// Set PO content
str := `
msgid ""
msgstr ""
# Header below
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Translate this"
msgstr "Translated text"
msgid "Another string"
msgstr ""
msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular: %s"
msgstr[1] "This one is the plural: %s"
`
// Create Po object
po := new(gotext.Po)
po.Parse(str)
fmt.Println(po.GetN("One with var: %s", "Several with vars: %s", 54, v))
// "This one is the plural: Variable"
}
```
### More advanced scenarios
- **Embedded translation files** (`embed.FS`) and named `%(name)s` formatting: see the [Advanced Usage Guide](docs/ADVANCED.md).
- **Languages fallback**: pass multiple languages to `Configure` as a colon-separated list (`gotext.Configure("/path/to/locales", "es_UY:es:en_US", "default")`).
- **Serialization/caching** of parsed locales: `Locale.MarshalBinary` / `UnmarshalBinary`.
---
# Locales directories structure
The package will assume a directories structure starting with a base path that will be provided to the package configuration
or to object constructors depending on the use, but either will use the same convention to lookup inside the base path.
Inside the base directory where will be the language directories named using the language and country 2-letter codes (en_US, es_AR, ...).
All package functions can lookup after the simplified version for each language in case the full code isn't present but the more general language code exists.
So if the language set is `en_UK`, but there is no directory named after that code and there is a directory named `en`,
all package functions will be able to resolve this generalization and provide translations for the more general library.
The language codes are assumed to be [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes (2-letter codes).
That said, most functions will work with any coding standard as long the directory name matches the language code set on the configuration.
Then, there can be a `LC_MESSAGES` containing all PO files or the PO files themselves.
A library directory structure can look like:
## Locales directories structure
The package expects a standard Gettext directory structure:
```
/path/to/locales
/en_US
/LC_MESSAGES
default.po
/es_ES
/LC_MESSAGES
default.po
/path/to/locales/en_US
/path/to/locales/en_US/LC_MESSAGES
/path/to/locales/en_US/LC_MESSAGES/default.po
/path/to/locales/en_US/LC_MESSAGES/extras.po
/path/to/locales/en_UK
/path/to/locales/en_UK/LC_MESSAGES
/path/to/locales/en_UK/LC_MESSAGES/default.po
/path/to/locales/en_UK/LC_MESSAGES/extras.po
/path/to/locales/en_AU
/path/to/locales/en_AU/LC_MESSAGES
/path/to/locales/en_AU/LC_MESSAGES/default.po
/path/to/locales/en_AU/LC_MESSAGES/extras.po
/path/to/locales/es
/path/to/locales/es/default.po
/path/to/locales/es/extras.po
/path/to/locales/es_ES
/path/to/locales/es_ES/default.po
/path/to/locales/es_ES/extras.po
/path/to/locales/fr
/path/to/locales/fr/default.po
/path/to/locales/fr/extras.po
```
Translation files can live either inside an `LC_MESSAGES` subdirectory or directly under the language code (`/es_ES/default.po`); both layouts are supported.
It supports automatic language simplification (e.g., falling back from `en_UK` to `en`).
And so on...
---
## Contributing
We welcome contributions of all kinds!
- [Contributing Guidelines](CONTRIBUTING.md)
- [Code of Conduct](CODE_OF_CONDUCT.md)
# Contribute
---
- Please, contribute.
- Use the package on your projects.
- Report issues on Github.
- Send pull requests for bugfixes and improvements.
- Send proposals on Github issues.
## License
`gotext` is released under the [MIT License](LICENSE).
+157 -316
View File
@@ -4,7 +4,7 @@ import (
"bytes"
"encoding/gob"
"fmt"
"maps"
"regexp"
"sort"
"strconv"
"strings"
@@ -64,6 +64,9 @@ func (m HeaderMap) Del(key string) {
// Get value for key from HeaderMap
func (m HeaderMap) Get(key string) string {
if m == nil {
return ""
}
v := m[key]
if len(v) == 0 {
return ""
@@ -78,6 +81,9 @@ func (m HeaderMap) Set(key, value string) {
// Values returns all values for a given key from HeaderMap
func (m HeaderMap) Values(key string) []string {
if m == nil {
return nil
}
return m[key]
}
@@ -94,111 +100,29 @@ func NewDomain() *Domain {
return domain
}
func cloneRefs(refs []string) []string {
if refs == nil {
return nil
}
owned := make([]string, len(refs))
copy(owned, refs)
return owned
}
func cloneHeaderMap(headers HeaderMap) HeaderMap {
if headers == nil {
return nil
}
owned := make(HeaderMap, len(headers))
for key, values := range headers {
owned[key] = cloneRefs(values)
}
return owned
}
func cloneTranslation(trans *Translation) *Translation {
if trans == nil {
return nil
}
owned := NewTranslation()
owned.ID = trans.ID
owned.PluralID = trans.PluralID
owned.dirty = trans.dirty
owned.Refs = cloneRefs(trans.Refs)
maps.Copy(owned.Trs, trans.Trs)
return owned
}
func cloneTranslations(translations map[string]*Translation) map[string]*Translation {
if translations == nil {
return nil
}
owned := make(map[string]*Translation, len(translations))
for id, trans := range translations {
owned[id] = cloneTranslation(trans)
}
return owned
}
func cloneContextTranslations(contexts map[string]map[string]*Translation) map[string]map[string]*Translation {
if contexts == nil {
return nil
}
owned := make(map[string]map[string]*Translation, len(contexts))
for context, translations := range contexts {
owned[context] = cloneTranslations(translations)
}
return owned
}
func (do *Domain) hasTranslation(id string) bool {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
return do.translations[id] != nil
}
func (do *Domain) hasContextTranslation(id, context string) bool {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
return do.contextTranslations[context][id] != nil
}
// SetPluralResolver sets a custom plural resolver function
func (do *Domain) SetPluralResolver(f func(int) int) {
do.pluralMutex.Lock()
defer do.pluralMutex.Unlock()
do.customPluralResolver = f
}
func (do *Domain) pluralForm(n int) int {
// Snapshot plural state before calling an expression or custom resolver.
// do we really need locking here? not sure how this plurals.Expression works, so sticking with it for now
do.pluralMutex.RLock()
pluralforms := do.pluralforms
customPluralResolver := do.customPluralResolver
do.pluralMutex.RUnlock()
if pluralforms != nil {
return pluralforms.Eval(uint32(n))
}
defer do.pluralMutex.RUnlock()
// Failure fallback
if customPluralResolver != nil {
return customPluralResolver(n)
}
if do.pluralforms == nil {
if do.customPluralResolver != nil {
return do.customPluralResolver(n)
}
/* Use the Germanic plural rule. */
if n == 1 {
return 0
/* Use the Germanic plural rule. */
if n == 1 {
return 0
}
return 1
}
return 1
return do.pluralforms.Eval(uint32(n))
}
// parseHeaders retrieves data from previously parsed headers. it's called by both Mo and Po when parsing
@@ -212,16 +136,18 @@ func (do *Domain) parseHeaders() {
languageKey := "Language"
pluralFormsKey := "Plural-Forms"
for line := range strings.SplitSeq(raw, "\n") {
rawLines := strings.Split(raw, "\n")
for _, line := range rawLines {
if len(line) == 0 {
continue
}
key, value, ok := strings.Cut(line, ":")
if !ok {
colonIdx := strings.Index(line, ":")
if colonIdx < 0 {
continue
}
key := line[:colonIdx]
lowerKey := strings.ToLower(key)
if lowerKey == strings.ToLower(languageKey) {
languageKey = key
@@ -229,7 +155,7 @@ func (do *Domain) parseHeaders() {
pluralFormsKey = key
}
value = strings.TrimSpace(value)
value := strings.TrimSpace(line[colonIdx+1:])
do.Headers.Add(key, value)
}
@@ -242,18 +168,22 @@ func (do *Domain) parseHeaders() {
return
}
for i := range strings.SplitSeq(do.PluralForms, ";") {
key, value, ok := strings.Cut(i, "=")
if !ok {
// Split plural form header value
pfs := strings.Split(do.PluralForms, ";")
// Parse values
for _, i := range pfs {
vs := strings.SplitN(i, "=", 2)
if len(vs) != 2 {
continue
}
switch strings.TrimSpace(key) {
switch strings.TrimSpace(vs[0]) {
case "nplurals":
do.nplurals, _ = strconv.Atoi(value)
do.nplurals, _ = strconv.Atoi(vs[1])
case "plural":
do.plural = value
do.plural = vs[1]
if expr, err := plurals.Compile(do.plural); err == nil {
do.pluralforms = expr
@@ -291,19 +221,17 @@ func (do *Domain) DropStaleTranslations() {
// SetRefs set source references for a given translation
func (do *Domain) SetRefs(str string, refs []string) {
ownedRefs := cloneRefs(refs)
do.trMutex.Lock()
do.pluralMutex.Lock()
defer do.trMutex.Unlock()
defer do.pluralMutex.Unlock()
if trans, ok := do.translations[str]; ok && trans != nil {
trans.SetRefs(ownedRefs)
if trans, ok := do.translations[str]; ok {
trans.Refs = refs
} else {
trans := NewTranslation()
trans = NewTranslation()
trans.ID = str
trans.SetRefs(ownedRefs)
trans.SetRefs(refs)
do.translations[str] = trans
}
}
@@ -315,8 +243,8 @@ func (do *Domain) GetRefs(str string) []string {
defer do.trMutex.RUnlock()
if do.translations != nil {
if trans, ok := do.translations[str]; ok && trans != nil {
return cloneRefs(trans.Refs)
if trans, ok := do.translations[str]; ok {
return trans.Refs
}
}
return nil
@@ -340,7 +268,7 @@ func (do *Domain) Set(id, str string) {
}
// Get retrieves the Translation for the given string.
func (do *Domain) Get(str string, vars ...any) string {
func (do *Domain) Get(str string, vars ...interface{}) string {
// Sync read
do.trMutex.RLock()
defer do.trMutex.RUnlock()
@@ -356,8 +284,8 @@ func (do *Domain) Get(str string, vars ...any) string {
}
// Append retrieves the Translation for the given string.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) Append(b []byte, str string, vars ...any) []byte {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) Append(b []byte, str string, vars ...interface{}) []byte {
// Sync read
do.trMutex.RLock()
defer do.trMutex.RUnlock()
@@ -394,48 +322,40 @@ func (do *Domain) SetN(id, plural string, n int, str string) {
}
// GetN retrieves the (N)th plural form of Translation for the given string.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetN(str, plural string, n int, vars ...any) string {
// Get plural form before acquiring the translation lock so a custom
// resolver is never called while a project lock is held.
pluralForm := do.pluralForm(n)
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetN(str, plural string, n int, vars ...interface{}) string {
// Sync read
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.translations != nil {
if _, ok := do.translations[str]; ok {
return FormatString(do.translations[str].GetN(pluralForm), vars...)
return FormatString(do.translations[str].GetN(do.pluralForm(n)), vars...)
}
}
// Parse plural forms to distinguish between plural and singular
if pluralForm == 0 {
if do.pluralForm(n) == 0 {
return FormatString(str, vars...)
}
return FormatString(plural, vars...)
}
// AppendN adds the (N)th plural form of Translation for the given string.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) AppendN(b []byte, str, plural string, n int, vars ...any) []byte {
// Get plural form before acquiring the translation lock so a custom
// resolver is never called while a project lock is held.
pluralForm := do.pluralForm(n)
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
// Sync read
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.translations != nil {
if _, ok := do.translations[str]; ok {
return Appendf(b, do.translations[str].GetN(pluralForm), vars...)
return Appendf(b, do.translations[str].GetN(do.pluralForm(n)), vars...)
}
}
// Parse plural forms to distinguish between plural and singular
if pluralForm == 0 {
if do.pluralForm(n) == 0 {
return Appendf(b, str, vars...)
}
return Appendf(b, plural, vars...)
@@ -468,8 +388,8 @@ func (do *Domain) SetC(id, ctx, str string) {
}
// GetC retrieves the corresponding Translation for a given string in the given context.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetC(str, ctx string, vars ...any) string {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetC(str, ctx string, vars ...interface{}) string {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
@@ -488,8 +408,8 @@ func (do *Domain) GetC(str, ctx string, vars ...any) string {
}
// AppendC retrieves the corresponding Translation for a given string in the given context.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) AppendC(b []byte, str, ctx string, vars ...any) []byte {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
@@ -523,14 +443,12 @@ func (do *Domain) SetNC(id, plural, ctx string, n int, str string) {
} else {
trans = NewTranslation()
trans.ID = id
trans.PluralID = plural
trans.SetN(pluralForm, str)
context[id] = trans
}
} else {
trans := NewTranslation()
trans.ID = id
trans.PluralID = plural
trans.SetN(pluralForm, str)
do.contextTranslations[ctx] = map[string]*Translation{
id: trans,
@@ -539,22 +457,19 @@ func (do *Domain) SetNC(id, plural, ctx string, n int, str string) {
}
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...any) string {
if do.hasContextTranslation(str, ctx) {
// Get plural form before acquiring the translation lock so a custom
// resolver is never called while a project lock is held.
pluralForm := do.pluralForm(n)
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
do.trMutex.RLock()
if translations, ok := do.contextTranslations[ctx]; ok && translations != nil {
if trans, ok := translations[str]; ok && trans != nil {
translated := trans.GetN(pluralForm)
do.trMutex.RUnlock()
return FormatString(translated, vars...)
if do.contextTranslations != nil {
if _, ok := do.contextTranslations[ctx]; ok {
if do.contextTranslations[ctx] != nil {
if _, ok := do.contextTranslations[ctx][str]; ok {
return FormatString(do.contextTranslations[ctx][str].GetN(do.pluralForm(n)), vars...)
}
}
}
do.trMutex.RUnlock()
}
if n == 1 {
@@ -564,22 +479,19 @@ func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...any) stri
}
// AppendNC retrieves the (N)th plural form of Translation for the given string in the given context.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...any) []byte {
if do.hasContextTranslation(str, ctx) {
// Get plural form before acquiring the translation lock so a custom
// resolver is never called while a project lock is held.
pluralForm := do.pluralForm(n)
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
do.trMutex.RLock()
if translations, ok := do.contextTranslations[ctx]; ok && translations != nil {
if trans, ok := translations[str]; ok && trans != nil {
translated := trans.GetN(pluralForm)
do.trMutex.RUnlock()
return Appendf(b, translated, vars...)
if do.contextTranslations != nil {
if _, ok := do.contextTranslations[ctx]; ok {
if do.contextTranslations[ctx] != nil {
if _, ok := do.contextTranslations[ctx][str]; ok {
return Appendf(b, do.contextTranslations[ctx][str].GetN(do.pluralForm(n)), vars...)
}
}
}
do.trMutex.RUnlock()
}
if n == 1 {
@@ -595,14 +507,6 @@ func (do *Domain) IsTranslated(str string) bool {
// IsTranslatedN reports whether a plural string is translated
func (do *Domain) IsTranslatedN(str string, n int) bool {
if !do.hasTranslation(str) {
return false
}
// Get plural form before acquiring the translation lock so a custom
// resolver is never called while a project lock is held.
pluralForm := do.pluralForm(n)
do.trMutex.RLock()
defer do.trMutex.RUnlock()
@@ -610,10 +514,10 @@ func (do *Domain) IsTranslatedN(str string, n int) bool {
return false
}
tr, ok := do.translations[str]
if !ok || tr == nil {
if !ok {
return false
}
return tr.IsTranslatedN(pluralForm)
return tr.IsTranslatedN(do.pluralForm(n))
}
// IsTranslatedC reports whether a context string is translated
@@ -623,14 +527,6 @@ func (do *Domain) IsTranslatedC(str, ctx string) bool {
// IsTranslatedNC reports whether a plural context string is translated
func (do *Domain) IsTranslatedNC(str string, n int, ctx string) bool {
if !do.hasContextTranslation(str, ctx) {
return false
}
// Get plural form before acquiring the translation lock so a custom
// resolver is never called while a project lock is held.
pluralForm := do.pluralForm(n)
do.trMutex.RLock()
defer do.trMutex.RUnlock()
@@ -638,24 +534,36 @@ func (do *Domain) IsTranslatedNC(str string, n int, ctx string) bool {
return false
}
translations, ok := do.contextTranslations[ctx]
if !ok || translations == nil {
if !ok {
return false
}
tr, ok := translations[str]
if !ok || tr == nil {
if !ok {
return false
}
return tr.IsTranslatedN(pluralForm)
return tr.IsTranslatedN(do.pluralForm(n))
}
// GetTranslations returns a copy of every translation in the domain. It does not support contexts.
func (do *Domain) GetTranslations() map[string]*Translation {
all := make(map[string]*Translation, len(do.translations))
do.trMutex.RLock()
defer do.trMutex.RUnlock()
all := make(map[string]*Translation, len(do.translations))
for msgID, trans := range do.translations {
all[msgID] = cloneTranslation(trans)
newTrans := NewTranslation()
newTrans.ID = trans.ID
newTrans.PluralID = trans.PluralID
newTrans.dirty = trans.dirty
if len(trans.Refs) > 0 {
newTrans.Refs = make([]string, len(trans.Refs))
copy(newTrans.Refs, trans.Refs)
}
for k, v := range trans.Trs {
newTrans.Trs[k] = v
}
all[msgID] = newTrans
}
return all
@@ -663,18 +571,32 @@ func (do *Domain) GetTranslations() map[string]*Translation {
// GetCtxTranslations returns a copy of every translation in the domain with context
func (do *Domain) GetCtxTranslations() map[string]map[string]*Translation {
all := make(map[string]map[string]*Translation, len(do.contextTranslations))
do.trMutex.RLock()
defer do.trMutex.RUnlock()
all := make(map[string]map[string]*Translation, len(do.contextTranslations))
for ctx, translations := range do.contextTranslations {
for msgID, trans := range translations {
newTrans := NewTranslation()
newTrans.ID = trans.ID
newTrans.PluralID = trans.PluralID
newTrans.dirty = trans.dirty
if len(trans.Refs) > 0 {
newTrans.Refs = make([]string, len(trans.Refs))
copy(newTrans.Refs, trans.Refs)
}
for k, v := range trans.Trs {
newTrans.Trs[k] = v
}
if all[ctx] == nil {
all[ctx] = make(map[string]*Translation)
}
all[ctx][msgID] = cloneTranslation(trans)
all[ctx][msgID] = newTrans
}
}
return all
@@ -682,11 +604,10 @@ func (do *Domain) GetCtxTranslations() map[string]map[string]*Translation {
// SourceReference is a struct to hold source reference information
type SourceReference struct {
path string
line int
context string
hasContext bool
trans *Translation
path string
line int
context string
trans *Translation
}
func extractPathAndLine(ref string) (string, int) {
@@ -706,11 +627,6 @@ func extractPathAndLine(ref string) (string, int) {
// MarshalText implements encoding.TextMarshaler interface
// Assists round-trip of POT/PO content
func (do *Domain) MarshalText() ([]byte, error) {
do.trMutex.RLock()
do.pluralMutex.RLock()
defer do.trMutex.RUnlock()
defer do.pluralMutex.RUnlock()
var buf bytes.Buffer
if len(do.headerComments) > 0 {
buf.WriteString(strings.Join(do.headerComments, "\n"))
@@ -765,29 +681,31 @@ func (do *Domain) MarshalText() ([]byte, error) {
v := do.Headers[k]
for _, value := range v {
encoded := EscapeSpecialCharacters(k + ": " + value + "\n")
buf.WriteString("\n\"" + encoded + "\"")
buf.WriteString("\n\"" + k + ": " + value + "\\n\"")
}
}
// Just as with headers, output translations in consistent order (to minimise diffs between round-trips), with (first) source reference taking priority, followed by context and finally ID
references := make([]SourceReference, 0)
for name, ctx := range do.contextTranslations {
for _, trans := range ctx {
for id, trans := range ctx {
if id == "" {
continue
}
if len(trans.Refs) > 0 {
path, line := extractPathAndLine(trans.Refs[0])
references = append(references, SourceReference{
path: path,
line: line,
context: name,
hasContext: true,
trans: trans,
path,
line,
name,
trans,
})
} else {
references = append(references, SourceReference{
context: name,
hasContext: true,
trans: trans,
"",
0,
name,
trans,
})
}
}
@@ -801,13 +719,17 @@ func (do *Domain) MarshalText() ([]byte, error) {
if len(trans.Refs) > 0 {
path, line := extractPathAndLine(trans.Refs[0])
references = append(references, SourceReference{
path: path,
line: line,
trans: trans,
path,
line,
"",
trans,
})
} else {
references = append(references, SourceReference{
trans: trans,
"",
0,
"",
trans,
})
}
}
@@ -832,9 +754,6 @@ func (do *Domain) MarshalText() ([]byte, error) {
if references[i].context > references[j].context {
return false
}
if references[i].hasContext != references[j].hasContext {
return !references[i].hasContext
}
return references[i].trans.ID < references[j].trans.ID
})
@@ -846,7 +765,7 @@ func (do *Domain) MarshalText() ([]byte, error) {
buf.WriteByte(byte('\n'))
}
if !ref.hasContext {
if ref.context == "" {
buf.WriteString("\nmsgid \"" + EscapeSpecialCharacters(trans.ID) + "\"")
} else {
buf.WriteString("\nmsgctxt \"" + EscapeSpecialCharacters(ref.context) + "\"\nmsgid \"" + EscapeSpecialCharacters(trans.ID) + "\"")
@@ -855,16 +774,9 @@ func (do *Domain) MarshalText() ([]byte, error) {
if trans.PluralID == "" {
buf.WriteString("\nmsgstr \"" + EscapeSpecialCharacters(trans.Trs[0]) + "\"")
} else {
buf.WriteString("\nmsgid_plural \"" + EscapeSpecialCharacters(trans.PluralID) + "\"")
pluralIndexes := make([]int, 0, len(trans.Trs))
for i := range trans.Trs {
pluralIndexes = append(pluralIndexes, i)
}
sort.Ints(pluralIndexes)
for _, i := range pluralIndexes {
buf.WriteString("\nmsgstr[" + strconv.Itoa(i) + "] \"" + EscapeSpecialCharacters(trans.Trs[i]) + "\"")
buf.WriteString("\nmsgid_plural \"" + trans.PluralID + "\"")
for i, tr := range trans.Trs {
buf.WriteString("\nmsgstr[" + EscapeSpecialCharacters(strconv.Itoa(i)) + "] \"" + tr + "\"")
}
}
}
@@ -874,23 +786,8 @@ func (do *Domain) MarshalText() ([]byte, error) {
// EscapeSpecialCharacters escapes special characters in a string
func EscapeSpecialCharacters(s string) string {
var escaped strings.Builder
escaped.Grow(len(s))
s = regexp.MustCompile(`([^\\])(")`).ReplaceAllString(s, "$1\\\"") // Escape non-escaped double quotation marks
for i := range len(s) {
switch s[i] {
case '\\':
escaped.WriteByte('\\')
escaped.WriteByte('\\')
case '"':
escaped.WriteByte('\\')
escaped.WriteByte('"')
default:
escaped.WriteByte(s[i])
}
}
s = escaped.String()
if strings.Count(s, "\n") == 0 {
return s
}
@@ -924,26 +821,16 @@ func EscapeSpecialCharacters(s string) string {
return strings.Join(data, "\n")
}
func (do *Domain) translatorEncodingSnapshot() *TranslatorEncoding {
do.trMutex.RLock()
do.pluralMutex.RLock()
defer do.trMutex.RUnlock()
defer do.pluralMutex.RUnlock()
return &TranslatorEncoding{
Headers: cloneHeaderMap(do.Headers),
Language: do.Language,
PluralForms: do.PluralForms,
Nplurals: do.nplurals,
Plural: do.plural,
Translations: cloneTranslations(do.translations),
Contexts: cloneContextTranslations(do.contextTranslations),
}
}
// MarshalBinary implements encoding.BinaryMarshaler interface
func (do *Domain) MarshalBinary() ([]byte, error) {
obj := do.translatorEncodingSnapshot()
obj := new(TranslatorEncoding)
obj.Headers = do.Headers
obj.Language = do.Language
obj.PluralForms = do.PluralForms
obj.Nplurals = do.nplurals
obj.Plural = do.plural
obj.Translations = do.translations
obj.Contexts = do.contextTranslations
var buff bytes.Buffer
encoder := gob.NewEncoder(&buff)
@@ -958,68 +845,22 @@ func (do *Domain) UnmarshalBinary(data []byte) error {
obj := new(TranslatorEncoding)
decoder := gob.NewDecoder(buff)
if err := decoder.Decode(obj); err != nil {
err := decoder.Decode(obj)
if err != nil {
return err
}
headers := cloneHeaderMap(obj.Headers)
if headers == nil {
headers = make(HeaderMap)
}
translations := cloneTranslations(obj.Translations)
if translations == nil {
translations = make(map[string]*Translation)
}
contextTranslations := cloneContextTranslations(obj.Contexts)
if contextTranslations == nil {
contextTranslations = make(map[string]map[string]*Translation)
}
for id, trans := range translations {
if trans == nil {
trans = NewTranslation()
trans.ID = id
translations[id] = trans
}
}
for context, translationsForContext := range contextTranslations {
if translationsForContext == nil {
contextTranslations[context] = make(map[string]*Translation)
continue
}
for id, trans := range translationsForContext {
if trans == nil {
trans = NewTranslation()
trans.ID = id
translationsForContext[id] = trans
}
}
}
var pluralforms plurals.Expression
if expr, err := plurals.Compile(obj.Plural); err == nil {
pluralforms = expr
}
do.trMutex.Lock()
do.pluralMutex.Lock()
do.Headers = headers
do.Headers = obj.Headers
do.Language = obj.Language
do.PluralForms = obj.PluralForms
do.nplurals = obj.Nplurals
do.plural = obj.Plural
do.pluralforms = pluralforms
do.translations = translations
do.contextTranslations = contextTranslations
do.pluralTranslations = make(map[string]*Translation)
do.headerComments = make([]string, 0)
do.trBuffer = nil
do.ctxBuffer = ""
do.refBuffer = ""
do.translations = obj.Translations
do.contextTranslations = obj.Contexts
do.pluralMutex.Unlock()
do.trMutex.Unlock()
if expr, err := plurals.Compile(do.plural); err == nil {
do.pluralforms = expr
}
return nil
}
+108 -170
View File
@@ -55,59 +55,51 @@ func init() {
domain: "default",
languages: []string{FallbackLocale},
library: "/usr/local/share/locale",
locales: make([]*Locale, 0),
locales: nil,
}
// Register Translator types for gob encoding
gob.Register(TranslatorEncoding{})
}
// loadLocales ensures package-level locales and their configured domain are loaded.
// loadLocales creates a new Locale object for every language (specified using Configure)
// at package level based on the configuration of global configuration .
// It is called when trying to use Get or GetD methods.
func loadLocales(rebuildCache bool) {
globalConfig.Lock()
if globalConfig.locales == nil || rebuildCache {
locales := make([]*Locale, 0, len(globalConfig.languages))
var locales []*Locale
for _, language := range globalConfig.languages {
locales = append(locales, NewLocale(globalConfig.library, language))
}
globalConfig.locales = locales
}
locales := append([]*Locale(nil), globalConfig.locales...)
domain := globalConfig.domain
globalConfig.Unlock()
for _, locale := range locales {
if locale == nil {
continue
for _, locale := range globalConfig.locales {
if _, ok := locale.Domains[globalConfig.domain]; !ok || rebuildCache {
locale.AddDomain(globalConfig.domain)
}
if rebuildCache || !locale.hasDomain(domain) {
locale.AddDomain(domain)
}
locale.SetDomain(domain)
locale.SetDomain(globalConfig.domain)
}
globalConfig.Unlock()
}
// GetDomain is the domain getter for the package configuration
func GetDomain() string {
var dom string
globalConfig.RLock()
domain := globalConfig.domain
var locale *Locale
for _, candidate := range globalConfig.locales {
if candidate != nil {
locale = candidate
break
}
if globalConfig.locales != nil {
// All locales have the same domain
dom = globalConfig.locales[0].GetDomain()
}
if dom == "" {
dom = globalConfig.domain
}
globalConfig.RUnlock()
if locale != nil {
if localeDomain := locale.GetDomain(); localeDomain != "" {
return localeDomain
}
}
return domain
return dom
}
// SetDomain sets the name for the domain to be used at package level.
@@ -115,8 +107,8 @@ func GetDomain() string {
func SetDomain(dom string) {
globalConfig.Lock()
globalConfig.domain = dom
for _, locale := range globalConfig.locales {
if locale != nil {
if globalConfig.locales != nil {
for _, locale := range globalConfig.locales {
locale.SetDomain(dom)
}
}
@@ -139,10 +131,8 @@ func GetLanguage() string {
// GetLanguages returns all languages that have been supplied.
func GetLanguages() []string {
globalConfig.RLock()
languages := make([]string, len(globalConfig.languages))
copy(languages, globalConfig.languages)
globalConfig.RUnlock()
return languages
defer globalConfig.RUnlock()
return globalConfig.languages
}
// SetLanguage sets the language code (or colon separated language codes) to be used at package level.
@@ -150,7 +140,7 @@ func GetLanguages() []string {
func SetLanguage(lang string) {
globalConfig.Lock()
var languages []string
for language := range strings.SplitSeq(lang, ":") {
for _, language := range strings.Split(lang, ":") {
languages = append(languages, SimplifiedLocale(language))
}
globalConfig.languages = languages
@@ -181,20 +171,17 @@ func SetLibrary(lib string) {
// GetLocales returns the locales that have been set for the package configuration.
func GetLocales() []*Locale {
globalConfig.RLock()
locales := make([]*Locale, len(globalConfig.locales))
copy(locales, globalConfig.locales)
globalConfig.RUnlock()
return locales
defer globalConfig.RUnlock()
return globalConfig.locales
}
// GetStorage is the locale storage getter for the package configuration.
//
// Deprecated: Storage has been renamed to Locale for consistency, use GetLocales instead.
func GetStorage() *Locale {
for _, locale := range GetLocales() {
if locale != nil {
return locale
}
locales := GetLocales()
if len(locales) > 0 {
return locales[0]
}
return nil
}
@@ -203,40 +190,24 @@ func GetStorage() *Locale {
// NewLocale(). This makes it possible to attach custom Domain objects from in-memory po/mo.
// The library, language and domain of the first Locale will set the default global configuration.
func SetLocales(locales []*Locale) {
ownedLocales := make([]*Locale, len(locales))
copy(ownedLocales, locales)
globalConfig.Lock()
globalConfig.locales = ownedLocales
defer globalConfig.Unlock()
languages := make([]string, 0, len(ownedLocales))
var first *Locale
for _, locale := range ownedLocales {
if locale == nil {
continue
}
globalConfig.locales = locales
globalConfig.library = locales[0].path
globalConfig.domain = locales[0].defaultDomain
locale.RLock()
if first == nil {
first = locale
globalConfig.library = locale.path
globalConfig.domain = locale.defaultDomain
}
var languages []string
for _, locale := range locales {
languages = append(languages, locale.lang)
locale.RUnlock()
}
globalConfig.languages = languages
globalConfig.Unlock()
}
// SetStorage allows overriding the global Locale object with one built manually with NewLocale().
//
// Deprecated: Storage has been renamed to Locale for consistency, use SetLocales instead.
func SetStorage(locale *Locale) {
if locale == nil {
SetLocales(nil)
return
}
SetLocales([]*Locale{locale})
}
@@ -248,7 +219,7 @@ func Configure(lib, lang, dom string) {
globalConfig.Lock()
globalConfig.library = lib
var languages []string
for language := range strings.SplitSeq(lang, ":") {
for _, language := range strings.Split(lang, ":") {
languages = append(languages, SimplifiedLocale(language))
}
globalConfig.languages = languages
@@ -259,137 +230,114 @@ func Configure(lib, lang, dom string) {
}
// Get uses the default domain globally set to return the corresponding Translation of a given string.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func Get(str string, vars ...any) string {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func Get(str string, vars ...interface{}) string {
return GetD(GetDomain(), str, vars...)
}
// GetN retrieves the (N)th plural form of Translation for the given string in the default domain.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func GetN(str, plural string, n int, vars ...any) string {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetN(str, plural string, n int, vars ...interface{}) string {
return GetND(GetDomain(), str, plural, n, vars...)
}
// GetD returns the corresponding Translation in the given domain for a given string.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func GetD(dom, str string, vars ...any) string {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetD(dom, str string, vars ...interface{}) string {
// Try to load default package Locales
loadLocales(false)
locales := GetLocales()
var fallback *Locale
for _, locale := range locales {
if locale == nil {
continue
}
fallback = locale
if !locale.hasDomain(dom) {
globalConfig.RLock()
defer globalConfig.RUnlock()
var tr string
for i, locale := range globalConfig.locales {
if _, ok := locale.Domains[dom]; !ok {
locale.AddDomain(dom)
}
if locale.IsTranslatedD(dom, str) {
return locale.GetD(dom, str, vars...)
if !locale.IsTranslatedD(dom, str) && i < (len(globalConfig.locales)-1) {
continue
}
tr = locale.GetD(dom, str, vars...)
break
}
if fallback != nil {
return fallback.GetD(dom, str, vars...)
}
return FormatString(str, vars...)
return tr
}
// GetND retrieves the (N)th plural form of Translation in the given domain for a given string.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func GetND(dom, str, plural string, n int, vars ...any) string {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetND(dom, str, plural string, n int, vars ...interface{}) string {
// Try to load default package Locales
loadLocales(false)
locales := GetLocales()
var fallback *Locale
for _, locale := range locales {
if locale == nil {
continue
}
fallback = locale
if !locale.hasDomain(dom) {
globalConfig.RLock()
defer globalConfig.RUnlock()
var tr string
for i, locale := range globalConfig.locales {
if _, ok := locale.Domains[dom]; !ok {
locale.AddDomain(dom)
}
if locale.IsTranslatedND(dom, str, n) {
return locale.GetND(dom, str, plural, n, vars...)
if !locale.IsTranslatedND(dom, str, n) && i < (len(globalConfig.locales)-1) {
continue
}
tr = locale.GetND(dom, str, plural, n, vars...)
break
}
if fallback != nil {
return fallback.GetND(dom, str, plural, n, vars...)
}
if n == 1 {
return FormatString(str, vars...)
}
return FormatString(plural, vars...)
return tr
}
// GetC uses the default domain globally set to return the corresponding Translation of the given string in the given context.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func GetC(str, ctx string, vars ...any) string {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetC(str, ctx string, vars ...interface{}) string {
return GetDC(GetDomain(), str, ctx, vars...)
}
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the default domain.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func GetNC(str, plural string, n int, ctx string, vars ...any) string {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
return GetNDC(GetDomain(), str, plural, n, ctx, vars...)
}
// GetDC returns the corresponding Translation in the given domain for the given string in the given context.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func GetDC(dom, str, ctx string, vars ...any) string {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetDC(dom, str, ctx string, vars ...interface{}) string {
// Try to load default package Locales
loadLocales(false)
locales := GetLocales()
var fallback *Locale
for _, locale := range locales {
if locale == nil {
globalConfig.RLock()
defer globalConfig.RUnlock()
var tr string
for i, locale := range globalConfig.locales {
if !locale.IsTranslatedDC(dom, str, ctx) && i < (len(globalConfig.locales)-1) {
continue
}
fallback = locale
if !locale.hasDomain(dom) {
locale.AddDomain(dom)
}
if locale.IsTranslatedDC(dom, str, ctx) {
return locale.GetDC(dom, str, ctx, vars...)
}
tr = locale.GetDC(dom, str, ctx, vars...)
break
}
if fallback != nil {
return fallback.GetDC(dom, str, ctx, vars...)
}
return FormatString(str, vars...)
return tr
}
// GetNDC retrieves the (N)th plural form of Translation in the given domain for a given string.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func GetNDC(dom, str, plural string, n int, ctx string, vars ...any) string {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
// Try to load default package Locales
loadLocales(false)
locales := GetLocales()
var fallback *Locale
for _, locale := range locales {
if locale == nil {
// Return Translation
globalConfig.RLock()
defer globalConfig.RUnlock()
var tr string
for i, locale := range globalConfig.locales {
if !locale.IsTranslatedNDC(dom, str, n, ctx) && i < (len(globalConfig.locales)-1) {
continue
}
fallback = locale
if !locale.hasDomain(dom) {
locale.AddDomain(dom)
}
if locale.IsTranslatedNDC(dom, str, n, ctx) {
return locale.GetNDC(dom, str, plural, n, ctx, vars...)
}
tr = locale.GetNDC(dom, str, plural, n, ctx, vars...)
break
}
if fallback != nil {
return fallback.GetNDC(dom, str, plural, n, ctx, vars...)
}
if n == 1 {
return FormatString(str, vars...)
}
return FormatString(plural, vars...)
return tr
}
// IsTranslated reports whether a string is translated in given languages.
@@ -418,23 +366,18 @@ func IsTranslatedND(dom, str string, n int, langs ...string) bool {
}
loadLocales(false)
locales := GetLocales()
globalConfig.RLock()
defer globalConfig.RUnlock()
for _, lang := range langs {
lang = SimplifiedLocale(lang)
for _, supportedLocale := range locales {
if supportedLocale == nil {
for _, supportedLocale := range globalConfig.locales {
if lang != supportedLocale.GetActualLanguage(dom) {
continue
}
if lang != supportedLocale.GetLanguage() && lang != supportedLocale.GetActualLanguage(dom) {
continue
}
if !supportedLocale.hasDomain(dom) {
supportedLocale.AddDomain(dom)
}
if supportedLocale.IsTranslatedND(dom, str, n) {
return true
}
return supportedLocale.IsTranslatedND(dom, str, n)
}
}
return false
@@ -455,7 +398,7 @@ func IsTranslatedNC(str string, n int, ctx string, langs ...string) bool {
// IsTranslatedDC reports whether a domain context string is translated in given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedDC(dom, str, ctx string, langs ...string) bool {
return IsTranslatedNDC(dom, str, 1, ctx, langs...)
return IsTranslatedNDC(dom, str, 0, ctx, langs...)
}
// IsTranslatedNDC reports whether a plural domain context string is translated in any of given languages.
@@ -466,23 +409,18 @@ func IsTranslatedNDC(dom, str string, n int, ctx string, langs ...string) bool {
}
loadLocales(false)
locales := GetLocales()
globalConfig.RLock()
defer globalConfig.RUnlock()
for _, lang := range langs {
lang = SimplifiedLocale(lang)
for _, supportedLocale := range locales {
if supportedLocale == nil {
for _, locale := range globalConfig.locales {
if lang != locale.GetActualLanguage(dom) {
continue
}
if lang != supportedLocale.GetLanguage() && lang != supportedLocale.GetActualLanguage(dom) {
continue
}
if !supportedLocale.hasDomain(dom) {
supportedLocale.AddDomain(dom)
}
if supportedLocale.IsTranslatedNDC(dom, str, n, ctx) {
return true
}
return locale.IsTranslatedNDC(dom, str, n, ctx)
}
}
return false
+27 -31
View File
@@ -29,21 +29,16 @@ func SimplifiedLocale(lang string) string {
}
// FormatString applies text formatting only when needed to parse variables.
func FormatString(str string, vars ...any) string {
func FormatString(str string, vars ...interface{}) string {
if len(vars) > 0 {
return FormatStringWithArgs(str, vars)
return fmt.Sprintf(str, vars...)
}
return str
}
// FormatStringWithArgs applies text formatting as a proxy for fmt.Sprintf.
func FormatStringWithArgs(str string, vars []any) string {
return fmt.Sprintf(str, vars...)
}
// Appendf applies text formatting only when needed to parse variables.
func Appendf(b []byte, str string, vars ...any) []byte {
func Appendf(b []byte, str string, vars ...interface{}) []byte {
if len(vars) > 0 {
return fmt.Appendf(b, str, vars...)
}
@@ -52,49 +47,50 @@ func Appendf(b []byte, str string, vars ...any) []byte {
}
// NPrintf support named format
// NPrintf("%(name)s is Type %(type)s", map[string]any{"name": "Gotext", "type": "struct"})
func NPrintf(format string, params map[string]any) {
// NPrintf("%(name)s is Type %(type)s", map[string]interface{}{"name": "Gotext", "type": "struct"})
func NPrintf(format string, params map[string]interface{}) {
f, p := parseSprintf(format, params)
fmt.Printf(f, p...)
}
// Sprintf support named format
//
// Sprintf("%(name)s is Type %(type)s", map[string]any{"name": "Gotext", "type": "struct"})
func Sprintf(format string, params map[string]any) string {
// Sprintf("%(name)s is Type %(type)s", map[string]interface{}{"name": "Gotext", "type": "struct"})
func Sprintf(format string, params map[string]interface{}) string {
f, p := parseSprintf(format, params)
return fmt.Sprintf(f, p...)
}
func parseSprintf(format string, params map[string]any) (string, []any) {
func parseSprintf(format string, params map[string]interface{}) (string, []interface{}) {
f, n := reformatSprintf(format)
var p []any
if len(n) > 0 {
p = make([]any, len(n))
for i, v := range n {
p[i] = params[v]
}
var p []interface{}
for _, v := range n {
p = append(p, params[v])
}
return f, p
}
func reformatSprintf(f string) (string, []string) {
matches := re.FindAllStringSubmatchIndex(f, -1)
ord := make([]string, 0, len(matches))
pair := make([]int, 1, 1+2*len(matches))
pair[0] = 0
m := re.FindAllStringSubmatch(f, -1)
i := re.FindAllStringSubmatchIndex(f, -1)
for _, match := range matches {
ord = append(ord, f[match[2]:match[3]])
pair = append(pair, match[2]-1, match[3]+1)
ord := []string{}
for _, v := range m {
ord = append(ord, v[1])
}
pair := []int{0}
for _, v := range i {
pair = append(pair, v[2]-1)
pair = append(pair, v[3]+1)
}
pair = append(pair, len(f))
plen := len(pair)
var out strings.Builder
out.Grow(len(f))
for n := 0; n < len(pair); n += 2 {
out.WriteString(f[pair[n]:pair[n+1]])
out := ""
for n := 0; n < plen; n += 2 {
out += f[pair[n]:pair[n+1]]
}
return out.String(), ord
return out, ord
}
+142 -193
View File
@@ -9,7 +9,6 @@ import (
"bytes"
"encoding/gob"
"io/fs"
"maps"
"os"
"path"
"sync"
@@ -23,6 +22,8 @@ multiple languages at the same time by working with this object.
Example:
import (
"encoding/gob"
"bytes"
"fmt"
"github.com/leonelquinteros/gotext"
)
@@ -89,114 +90,95 @@ func NewLocaleFSWithPath(l string, filesystem fs.FS, p string) *Locale {
return loc
}
func (l *Locale) snapshotPathLanguageFS() (string, string, fs.FS) {
if l == nil {
return "", "", nil
func (l *Locale) findExt(dom, ext string) string {
filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
if l.fileExists(filename) {
return filename
}
l.RLock()
p := l.path
lang := l.lang
filesystem := l.fs
l.RUnlock()
return p, lang, filesystem
}
func fileExistsIn(filesystem fs.FS, filename string) bool {
if filesystem != nil {
_, err := fs.Stat(filesystem, filename)
return err == nil
}
_, err := os.Stat(filename)
return err == nil
}
func (l *Locale) findExtWithFS(dom, ext string) (fs.FS, string) {
p, lang, filesystem := l.snapshotPathLanguageFS()
filename := path.Join(p, lang, "LC_MESSAGES", dom+"."+ext)
if fileExistsIn(filesystem, filename) {
return filesystem, filename
}
if len(lang) > 2 {
filename = path.Join(p, lang[:2], "LC_MESSAGES", dom+"."+ext)
if fileExistsIn(filesystem, filename) {
return filesystem, filename
if len(l.lang) > 2 {
filename = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
if l.fileExists(filename) {
return filename
}
}
filename = path.Join(p, lang, dom+"."+ext)
if fileExistsIn(filesystem, filename) {
return filesystem, filename
filename = path.Join(l.path, l.lang, dom+"."+ext)
if l.fileExists(filename) {
return filename
}
if len(lang) > 2 {
filename = path.Join(p, lang[:2], dom+"."+ext)
if fileExistsIn(filesystem, filename) {
return filesystem, filename
if len(l.lang) > 2 {
filename = path.Join(l.path, l.lang[:2], dom+"."+ext)
if l.fileExists(filename) {
return filename
}
}
return filesystem, ""
return ""
}
// GetActualLanguage inspects the filesystem and decides whether to strip
// a CC part of the ll_CC locale string.
func (l *Locale) GetActualLanguage(dom string) string {
p, lang, filesystem := l.snapshotPathLanguageFS()
extensions := []string{"po", "mo"}
extensions := []string{"mo", "po"}
var fp string
for _, ext := range extensions {
// 'll' (or 'll_CC') exists, and it was specified as-is
fp = path.Join(p, lang, "LC_MESSAGES", dom+"."+ext)
if fileExistsIn(filesystem, fp) {
return lang
fp = path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
if l.fileExists(fp) {
return l.lang
}
// 'll' exists, but 'll_CC' was specified
if len(lang) > 2 {
fp = path.Join(p, lang[:2], "LC_MESSAGES", dom+"."+ext)
if fileExistsIn(filesystem, fp) {
return lang[:2]
if len(l.lang) > 2 {
fp = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
if l.fileExists(fp) {
return l.lang[:2]
}
}
// 'll' (or 'll_CC') exists outside of LC_category, and it was specified as-is
fp = path.Join(p, lang, dom+"."+ext)
if fileExistsIn(filesystem, fp) {
return lang
fp = path.Join(l.path, l.lang, dom+"."+ext)
if l.fileExists(fp) {
return l.lang
}
// 'll' exists outside of LC_category, but 'll_CC' was specified
if len(lang) > 2 {
fp = path.Join(p, lang[:2], dom+"."+ext)
if fileExistsIn(filesystem, fp) {
return lang[:2]
if len(l.lang) > 2 {
fp = path.Join(l.path, l.lang[:2], dom+"."+ext)
if l.fileExists(fp) {
return l.lang[:2]
}
}
}
return ""
}
func (l *Locale) fileExists(filename string) bool {
if l.fs != nil {
_, err := fs.Stat(l.fs, filename)
return err == nil
}
_, err := os.Stat(filename)
return err == nil
}
// AddDomain creates a new domain for a given locale object and initializes the Po object.
// If the domain exists, it gets reloaded.
func (l *Locale) AddDomain(dom string) {
if l == nil {
return
}
var poObj Translator
filesystem, file := l.findExtWithFS(dom, "po")
file := l.findExt(dom, "po")
if file != "" {
poObj = NewPoFS(filesystem)
poObj = NewPoFS(l.fs)
// Parse file.
poObj.ParseFile(file)
} else {
filesystem, file = l.findExtWithFS(dom, "mo")
file = l.findExt(dom, "mo")
if file != "" {
poObj = NewMoFS(filesystem)
poObj = NewMoFS(l.fs)
// Parse file.
poObj.ParseFile(file)
} else {
// fallback return if no file found with
return
}
}
@@ -230,34 +212,9 @@ func (l *Locale) AddTranslator(dom string, tr Translator) {
l.Unlock()
}
func (l *Locale) translatorFor(dom string) Translator {
if l == nil {
return nil
}
l.RLock()
translator := l.Domains[dom]
l.RUnlock()
return translator
}
func (l *Locale) hasDomain(dom string) bool {
if l == nil {
return false
}
l.RLock()
_, ok := l.Domains[dom]
l.RUnlock()
return ok
}
// GetDomain is the domain getter for Locale configuration
func (l *Locale) GetDomain() string {
if l == nil {
return ""
}
l.RLock()
dom := l.defaultDomain
l.RUnlock()
@@ -266,10 +223,6 @@ func (l *Locale) GetDomain() string {
// SetDomain sets the name for the domain to be used.
func (l *Locale) SetDomain(dom string) {
if l == nil {
return
}
l.Lock()
l.defaultDomain = dom
l.Unlock()
@@ -277,10 +230,6 @@ func (l *Locale) SetDomain(dom string) {
// GetLanguage is the lang getter for Locale configuration
func (l *Locale) GetLanguage() string {
if l == nil {
return ""
}
l.RLock()
lang := l.lang
l.RUnlock()
@@ -288,37 +237,51 @@ func (l *Locale) GetLanguage() string {
}
// Get uses a domain "default" to return the corresponding Translation of a given string.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) Get(str string, vars ...any) string {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) Get(str string, vars ...interface{}) string {
return l.GetD(l.GetDomain(), str, vars...)
}
// GetN retrieves the (N)th plural form of Translation for the given string in the "default" domain.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetN(str, plural string, n int, vars ...any) string {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetN(str, plural string, n int, vars ...interface{}) string {
return l.GetND(l.GetDomain(), str, plural, n, vars...)
}
// GetD returns the corresponding Translation in the given domain for the given string.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetD(dom, str string, vars ...any) string {
translator := l.translatorFor(dom)
if translator != nil {
return translator.Get(str, vars...)
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetD(dom, str string, vars ...interface{}) string {
// Sync read
l.RLock()
defer l.RUnlock()
if l.Domains != nil {
if _, ok := l.Domains[dom]; ok {
if l.Domains[dom] != nil {
return l.Domains[dom].Get(str, vars...)
}
}
}
return FormatString(str, vars...)
}
// GetND retrieves the (N)th plural form of Translation in the given domain for the given string.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetND(dom, str, plural string, n int, vars ...any) string {
translator := l.translatorFor(dom)
if translator != nil {
return translator.GetN(str, plural, n, vars...)
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) string {
// Sync read
l.RLock()
defer l.RUnlock()
if l.Domains != nil {
if _, ok := l.Domains[dom]; ok {
if l.Domains[dom] != nil {
return l.Domains[dom].GetN(str, plural, n, vars...)
}
}
}
// Use western default rule (n != 1) to handle missing domain default result.
// Use western default rule (plural > 1) to handle missing domain default result.
if n == 1 {
return FormatString(str, vars...)
}
@@ -326,37 +289,51 @@ func (l *Locale) GetND(dom, str, plural string, n int, vars ...any) string {
}
// GetC uses a domain "default" to return the corresponding Translation of the given string in the given context.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetC(str, ctx string, vars ...any) string {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetC(str, ctx string, vars ...interface{}) string {
return l.GetDC(l.GetDomain(), str, ctx, vars...)
}
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the "default" domain.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetNC(str, plural string, n int, ctx string, vars ...any) string {
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
return l.GetNDC(l.GetDomain(), str, plural, n, ctx, vars...)
}
// GetDC returns the corresponding Translation in the given domain for the given string in the given context.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetDC(dom, str, ctx string, vars ...any) string {
translator := l.translatorFor(dom)
if translator != nil {
return translator.GetC(str, ctx, vars...)
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetDC(dom, str, ctx string, vars ...interface{}) string {
// Sync read
l.RLock()
defer l.RUnlock()
if l.Domains != nil {
if _, ok := l.Domains[dom]; ok {
if l.Domains[dom] != nil {
return l.Domains[dom].GetC(str, ctx, vars...)
}
}
}
return FormatString(str, vars...)
}
// GetNDC retrieves the (N)th plural form of Translation in the given domain for the given string in the given context.
// Supports optional parameters (vars... any) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...any) string {
translator := l.translatorFor(dom)
if translator != nil {
return translator.GetNC(str, plural, n, ctx, vars...)
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
// Sync read
l.RLock()
defer l.RUnlock()
if l.Domains != nil {
if _, ok := l.Domains[dom]; ok {
if l.Domains[dom] != nil {
return l.Domains[dom].GetNC(str, plural, n, ctx, vars...)
}
}
}
// Use western default rule (n != 1) to handle missing domain default result.
// Use western default rule (plural > 1) to handle missing domain default result.
if n == 1 {
return FormatString(str, vars...)
}
@@ -366,26 +343,13 @@ func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...any)
// GetTranslations returns a copy of all translations in all domains of this locale. It does not support contexts.
func (l *Locale) GetTranslations() map[string]*Translation {
all := make(map[string]*Translation)
if l == nil {
return all
}
l.RLock()
translators := make([]Translator, 0, len(l.Domains))
defer l.RUnlock()
for _, translator := range l.Domains {
translators = append(translators, translator)
}
l.RUnlock()
for _, translator := range translators {
if translator == nil {
continue
for msgID, trans := range translator.GetDomain().GetTranslations() {
all[msgID] = trans
}
domain := translator.GetDomain()
if domain == nil {
continue
}
maps.Copy(all, domain.GetTranslations())
}
return all
@@ -393,7 +357,7 @@ func (l *Locale) GetTranslations() map[string]*Translation {
// IsTranslated reports whether a string is translated
func (l *Locale) IsTranslated(str string) bool {
return l.IsTranslatedND(l.GetDomain(), str, 1)
return l.IsTranslatedND(l.GetDomain(), str, 0)
}
// IsTranslatedN reports whether a plural string is translated
@@ -403,25 +367,27 @@ func (l *Locale) IsTranslatedN(str string, n int) bool {
// IsTranslatedD reports whether a domain string is translated
func (l *Locale) IsTranslatedD(dom, str string) bool {
return l.IsTranslatedND(dom, str, 1)
return l.IsTranslatedND(dom, str, 0)
}
// IsTranslatedND reports whether a plural domain string is translated
func (l *Locale) IsTranslatedND(dom, str string, n int) bool {
translator := l.translatorFor(dom)
if translator == nil {
l.RLock()
defer l.RUnlock()
if l.Domains == nil {
return false
}
domain := translator.GetDomain()
if domain == nil {
translator, ok := l.Domains[dom]
if !ok {
return false
}
return domain.IsTranslatedN(str, n)
return translator.GetDomain().IsTranslatedN(str, n)
}
// IsTranslatedC reports whether a context string is translated
func (l *Locale) IsTranslatedC(str, ctx string) bool {
return l.IsTranslatedNDC(l.GetDomain(), str, 1, ctx)
return l.IsTranslatedNDC(l.GetDomain(), str, 0, ctx)
}
// IsTranslatedNC reports whether a plural context string is translated
@@ -431,20 +397,22 @@ func (l *Locale) IsTranslatedNC(str string, n int, ctx string) bool {
// IsTranslatedDC reports whether a domain context string is translated
func (l *Locale) IsTranslatedDC(dom, str, ctx string) bool {
return l.IsTranslatedNDC(dom, str, 1, ctx)
return l.IsTranslatedNDC(dom, str, 0, ctx)
}
// IsTranslatedNDC reports whether a plural domain context string is translated
func (l *Locale) IsTranslatedNDC(dom string, str string, n int, ctx string) bool {
translator := l.translatorFor(dom)
if translator == nil {
l.RLock()
defer l.RUnlock()
if l.Domains == nil {
return false
}
domain := translator.GetDomain()
if domain == nil {
translator, ok := l.Domains[dom]
if !ok {
return false
}
return domain.IsTranslatedNC(str, n, ctx)
return translator.GetDomain().IsTranslatedNC(str, n, ctx)
}
// LocaleEncoding is used as intermediary storage to encode Locale objects to Gob.
@@ -457,35 +425,18 @@ type LocaleEncoding struct {
// MarshalBinary implements encoding BinaryMarshaler interface
func (l *Locale) MarshalBinary() ([]byte, error) {
var localePath, lang, defaultDomain string
domains := make(map[string]Translator)
if l != nil {
l.RLock()
localePath = l.path
lang = l.lang
defaultDomain = l.defaultDomain
domains = make(map[string]Translator, len(l.Domains))
for name, translator := range l.Domains {
if translator != nil {
domains[name] = translator
}
}
l.RUnlock()
}
obj := &LocaleEncoding{
Path: localePath,
Lang: lang,
Domains: make(map[string][]byte, len(domains)),
DefaultDomain: defaultDomain,
}
for name, translator := range domains {
data, err := translator.MarshalBinary()
obj := new(LocaleEncoding)
obj.DefaultDomain = l.defaultDomain
obj.Domains = make(map[string][]byte)
for k, v := range l.Domains {
var err error
obj.Domains[k], err = v.MarshalBinary()
if err != nil {
return nil, err
}
obj.Domains[name] = data
}
obj.Lang = l.lang
obj.Path = l.path
var buff bytes.Buffer
encoder := gob.NewEncoder(&buff)
@@ -494,7 +445,7 @@ func (l *Locale) MarshalBinary() ([]byte, error) {
return buff.Bytes(), err
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler interface
// UnmarshalBinary implements encoding BinaryUnmarshaler interface
func (l *Locale) UnmarshalBinary(data []byte) error {
buff := bytes.NewBuffer(data)
obj := new(LocaleEncoding)
@@ -505,7 +456,12 @@ func (l *Locale) UnmarshalBinary(data []byte) error {
return err
}
domains := make(map[string]Translator, len(obj.Domains))
l.defaultDomain = obj.DefaultDomain
l.lang = obj.Lang
l.path = obj.Path
// Decode Domains
l.Domains = make(map[string]Translator)
for k, v := range obj.Domains {
var tr TranslatorEncoding
buff := bytes.NewBuffer(v)
@@ -515,15 +471,8 @@ func (l *Locale) UnmarshalBinary(data []byte) error {
return err
}
domains[k] = tr.GetTranslator()
l.Domains[k] = tr.GetTranslator()
}
l.Lock()
l.defaultDomain = obj.DefaultDomain
l.lang = obj.Lang
l.path = obj.Path
l.Domains = domains
l.Unlock()
return nil
}
-40
View File
@@ -1,40 +0,0 @@
site_name: gotext
site_description: GNU gettext utilities for Go
site_author: Leonel Quinteros
repo_url: https://github.com/leonelquinteros/gotext
edit_uri: edit/master/docs/
theme:
name: material
palette:
primary: indigo
accent: indigo
features:
- navigation.tabs
- navigation.sections
- navigation.top
- search.suggest
- search.highlight
nav:
- Home: index.md
- Getting Started: GETTING_STARTED.md
- User Guides:
- Plural Forms: PLURALS.md
- xgotext CLI: xgotext.md
- Character Encoding: ENCODING.md
- Advanced Usage: ADVANCED.md
- Best Practices: BEST_PRACTICES.md
- API Reference: https://pkg.go.dev/github.com/leonelquinteros/gotext
- Contributing:
- Guidelines: CONTRIBUTING.md
- Code of Conduct: CODE_OF_CONDUCT.md
- License: LICENSE.md
markdown_extensions:
- admonition
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.inlinehilite
- pymdownx.snippets
- pymdownx.superfences
+94 -62
View File
@@ -76,42 +76,42 @@ func (mo *Mo) GetDomain() *Domain {
}
// Get returns the translation for the given string
func (mo *Mo) Get(str string, vars ...any) string {
func (mo *Mo) Get(str string, vars ...interface{}) string {
return mo.domain.Get(str, vars...)
}
// Append a translation string into the domain
func (mo *Mo) Append(b []byte, str string, vars ...any) []byte {
func (mo *Mo) Append(b []byte, str string, vars ...interface{}) []byte {
return mo.domain.Append(b, str, vars...)
}
// GetN returns the translation for the given string and plural form
func (mo *Mo) GetN(str, plural string, n int, vars ...any) string {
func (mo *Mo) GetN(str, plural string, n int, vars ...interface{}) string {
return mo.domain.GetN(str, plural, n, vars...)
}
// AppendN appends a translation string for the given plural form into the domain
func (mo *Mo) AppendN(b []byte, str, plural string, n int, vars ...any) []byte {
func (mo *Mo) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
return mo.domain.AppendN(b, str, plural, n, vars...)
}
// GetC returns the translation for the given string and context
func (mo *Mo) GetC(str, ctx string, vars ...any) string {
func (mo *Mo) GetC(str, ctx string, vars ...interface{}) string {
return mo.domain.GetC(str, ctx, vars...)
}
// AppendC appends a translation string for the given context into the domain
func (mo *Mo) AppendC(b []byte, str, ctx string, vars ...any) []byte {
func (mo *Mo) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
return mo.domain.AppendC(b, str, ctx, vars...)
}
// GetNC returns the translation for the given string, plural form and context
func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...any) string {
func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
return mo.domain.GetNC(str, plural, n, ctx, vars...)
}
// AppendNC appends a translation string for the given plural form and context into the domain
func (mo *Mo) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...any) []byte {
func (mo *Mo) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
return mo.domain.AppendNC(b, str, plural, n, ctx, vars...)
}
@@ -163,18 +163,22 @@ func (mo *Mo) Parse(buf []byte) {
defer mo.domain.trMutex.Unlock()
defer mo.domain.pluralMutex.Unlock()
if len(buf) < 28 {
return
}
r := bytes.NewReader(buf)
var magicNumber uint32
if err := binary.Read(r, binary.LittleEndian, &magicNumber); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
var bo binary.ByteOrder
switch binary.LittleEndian.Uint32(buf[:4]) {
switch magicNumber {
case MoMagicLittleEndian:
bo = binary.LittleEndian
case MoMagicBigEndian:
bo = binary.BigEndian
default:
return
// return fmt.Errorf("gettext: %v", "invalid magic number")
}
var header struct {
@@ -183,58 +187,82 @@ func (mo *Mo) Parse(buf []byte) {
MsgIDCount uint32
MsgIDOffset uint32
MsgStrOffset uint32
HashSize uint32
HashOffset uint32
}
bufLen := uint64(len(buf))
header.MajorVersion = bo.Uint16(buf[4:6])
header.MinorVersion = bo.Uint16(buf[6:8])
header.MsgIDCount = bo.Uint32(buf[8:12])
header.MsgIDOffset = bo.Uint32(buf[12:16])
header.MsgStrOffset = bo.Uint32(buf[16:20])
if header.MajorVersion != 0 && header.MajorVersion != 1 {
if err := binary.Read(r, bo, &header); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
if header.MinorVersion != 0 && header.MinorVersion != 1 {
if v := header.MajorVersion; v != 0 && v != 1 {
return
// return fmt.Errorf("gettext: %v", "invalid version number")
}
if v := header.MinorVersion; v != 0 && v != 1 {
return
// return fmt.Errorf("gettext: %v", "invalid version number")
}
directorySize := uint64(header.MsgIDCount) * 8
if uint64(header.MsgIDOffset)+directorySize > bufLen ||
uint64(header.MsgStrOffset)+directorySize > bufLen {
msgIDStart := make([]uint32, header.MsgIDCount)
msgIDLen := make([]uint32, header.MsgIDCount)
if _, err := r.Seek(int64(header.MsgIDOffset), 0); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
for i := uint32(0); i < header.MsgIDCount; i++ {
msgIDEntry := int(uint64(header.MsgIDOffset) + uint64(i)*8)
msgIDLen := bo.Uint32(buf[msgIDEntry:])
msgIDStart := bo.Uint32(buf[msgIDEntry+4:])
if uint64(msgIDStart) > bufLen ||
uint64(msgIDLen) > bufLen-uint64(msgIDStart) {
for i := 0; i < int(header.MsgIDCount); i++ {
if err := binary.Read(r, bo, &msgIDLen[i]); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
msgStrEntry := int(uint64(header.MsgStrOffset) + uint64(i)*8)
msgStrLen := bo.Uint32(buf[msgStrEntry:])
msgStrStart := bo.Uint32(buf[msgStrEntry+4:])
if uint64(msgStrStart) > bufLen ||
uint64(msgStrLen) > bufLen-uint64(msgStrStart) {
if err := binary.Read(r, bo, &msgIDStart[i]); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
}
for i := uint32(0); i < header.MsgIDCount; i++ {
msgIDEntry := int(uint64(header.MsgIDOffset) + uint64(i)*8)
msgIDLen := bo.Uint32(buf[msgIDEntry:])
msgIDStart := bo.Uint32(buf[msgIDEntry+4:])
msgIDEnd := int(uint64(msgIDStart) + uint64(msgIDLen))
msgStrStart := make([]int32, header.MsgIDCount)
msgStrLen := make([]int32, header.MsgIDCount)
if _, err := r.Seek(int64(header.MsgStrOffset), 0); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
for i := 0; i < int(header.MsgIDCount); i++ {
if err := binary.Read(r, bo, &msgStrLen[i]); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
if err := binary.Read(r, bo, &msgStrStart[i]); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
}
msgStrEntry := int(uint64(header.MsgStrOffset) + uint64(i)*8)
msgStrLen := bo.Uint32(buf[msgStrEntry:])
msgStrStart := bo.Uint32(buf[msgStrEntry+4:])
msgStrEnd := int(uint64(msgStrStart) + uint64(msgStrLen))
for i := 0; i < int(header.MsgIDCount); i++ {
if _, err := r.Seek(int64(msgIDStart[i]), 0); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
msgIDData := make([]byte, msgIDLen[i])
if _, err := r.Read(msgIDData); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
mo.addTranslation(buf[int(msgIDStart):msgIDEnd], buf[int(msgStrStart):msgStrEnd])
if _, err := r.Seek(int64(msgStrStart[i]), 0); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
msgStrData := make([]byte, msgStrLen[i])
if _, err := r.Read(msgStrData); err != nil {
return
// return fmt.Errorf("gettext: %v", err)
}
if len(msgIDData) == 0 {
mo.addTranslation(msgIDData, msgStrData)
} else {
mo.addTranslation(msgIDData, msgStrData)
}
}
// Parse headers
@@ -251,32 +279,36 @@ func (mo *Mo) addTranslation(msgid, msgstr []byte) {
translation := NewTranslation()
var msgctxt []byte
var msgidPlural []byte
hasContext := false
if eotIndex := bytes.IndexByte(msgid, EotSeparator[0]); eotIndex >= 0 {
hasContext = true
msgctxt = msgid[:eotIndex]
msgid = msgid[eotIndex+len(EotSeparator):]
d := bytes.Split(msgid, []byte(EotSeparator))
if len(d) == 1 {
msgid = d[0]
} else {
msgid, msgctxt = d[1], d[0]
}
if nulIndex := bytes.IndexByte(msgid, NulSeparator[0]); nulIndex >= 0 {
msgidPlural = msgid[nulIndex+1:]
msgid = msgid[:nulIndex]
dd := bytes.Split(msgid, []byte(NulSeparator))
if len(dd) > 1 {
msgid = dd[0]
dd = dd[1:]
}
translation.ID = string(msgid)
msgidPlural = bytes.Join(dd, []byte(NulSeparator))
if len(msgidPlural) > 0 {
translation.PluralID = string(msgidPlural)
}
i := 0
for s := range bytes.SplitSeq(msgstr, []byte(NulSeparator)) {
translation.Trs[i] = string(s)
i++
ddd := bytes.Split(msgstr, []byte(NulSeparator))
if len(ddd) > 0 {
for i, s := range ddd {
translation.Trs[i] = string(s)
}
}
if hasContext {
// With context, including an explicitly empty context...
if len(msgctxt) > 0 {
// With context...
if _, ok := mo.domain.contextTranslations[string(msgctxt)]; !ok {
mo.domain.contextTranslations[string(msgctxt)] = make(map[string]*Translation)
}

Some files were not shown because too many files have changed in this diff Show More