Compare commits
37 Commits
9a9519186b
...
b8fc58bd9f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8fc58bd9f | ||
|
|
ba6a8936f4 | ||
|
|
0daf1b1ae4 | ||
|
|
8320e69ccc | ||
|
|
0a82ebcb52 | ||
|
|
715673f4b5 | ||
|
|
2848fa1a69 | ||
|
|
ab66901132 | ||
|
|
0bb33e2ae8 | ||
|
|
6ae03bee7a | ||
|
|
4fab9e7759 | ||
|
|
cfba731eae | ||
|
|
bd1faa7647 | ||
|
|
3def6ed054 | ||
|
|
8b448cc312 | ||
|
|
df53492c98 | ||
|
|
f5ddd27c7e | ||
|
|
23ac917f5b | ||
|
|
485405edac | ||
|
|
45b2b940f6 | ||
|
|
67382a6dbe | ||
|
|
b513356080 | ||
|
|
cbfacd97a6 | ||
|
|
1921f918ee | ||
|
|
9e3ab11076 | ||
|
|
9c9a33515b | ||
|
|
52cab9f145 | ||
|
|
dc8c88b368 | ||
|
|
91c487c959 | ||
|
|
d1929f390f | ||
|
|
2c805ed001 | ||
|
|
d588a72286 | ||
|
|
50ceea79c7 | ||
|
|
d57f04bf53 | ||
|
|
4950fd73f6 | ||
|
|
57f559232c | ||
|
|
2f0aac76a0 |
4
.github/workflows/push.yml
vendored
4
.github/workflows/push.yml
vendored
@@ -35,6 +35,10 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
go install -mod=mod ./cmd/...
|
go install -mod=mod ./cmd/...
|
||||||
|
|
||||||
|
- name: install qemu
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
|
run: sudo apt update && sudo apt-get install qemu-system-x86
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: matrix.os == 'ubuntu-latest'
|
||||||
# TestRelativeParentDir verifies breakglass.authorized_keys
|
# TestRelativeParentDir verifies breakglass.authorized_keys
|
||||||
|
|||||||
78
flake.lock
generated
Normal file
78
flake.lock
generated
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1763049705,
|
||||||
|
"narHash": "sha256-A5LS0AJZ1yDPTa2fHxufZN++n8MCmtgrJDtxFxrH4S8=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "3acb677ea67d4c6218f33de0db0955f116b7588c",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-25.05",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rpi-eeprom-tools": "rpi-eeprom-tools"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rpi-eeprom-tools": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1758789610,
|
||||||
|
"narHash": "sha256-1aGazeN4Ou5GRMCRETie4I0E08mlAQFWp6cGURB1NIM=",
|
||||||
|
"owner": "info-beamer",
|
||||||
|
"repo": "rpi-eeprom-tools",
|
||||||
|
"rev": "9ae4d618a0a51789fbde8e9d667881a321cf6721",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "info-beamer",
|
||||||
|
"repo": "rpi-eeprom-tools",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
66
flake.nix
Normal file
66
flake.nix
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
{
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
rpi-eeprom-tools = {
|
||||||
|
url = "github:info-beamer/rpi-eeprom-tools";
|
||||||
|
flake = false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
rpi-eeprom-tools,
|
||||||
|
}:
|
||||||
|
flake-utils.lib.eachDefaultSystem (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
|
# Python environment with pycryptodome for pi-eeprom-tools
|
||||||
|
pythonEnv = pkgs.python312.withPackages (
|
||||||
|
ps: with ps; [
|
||||||
|
pycryptodome
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
# Helper to create a wrapper for a pi-eeprom-* script
|
||||||
|
mkPiEepromTool =
|
||||||
|
name:
|
||||||
|
pkgs.writeShellApplication {
|
||||||
|
inherit name;
|
||||||
|
runtimeInputs = [
|
||||||
|
pythonEnv
|
||||||
|
];
|
||||||
|
text = ''
|
||||||
|
exec ${pythonEnv.interpreter} ${rpi-eeprom-tools}/${name} "$@"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# All the pi-eeprom tools
|
||||||
|
pi-eeprom-extract = mkPiEepromTool "pi-eeprom-extract";
|
||||||
|
pi-eeprom-ls = mkPiEepromTool "pi-eeprom-ls";
|
||||||
|
pi-eeprom-recompress = mkPiEepromTool "pi-eeprom-recompress";
|
||||||
|
pi-eeprom-update = mkPiEepromTool "pi-eeprom-update";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
go
|
||||||
|
raspberrypi-eeprom
|
||||||
|
pi-eeprom-extract
|
||||||
|
pi-eeprom-ls
|
||||||
|
pi-eeprom-recompress
|
||||||
|
pi-eeprom-update
|
||||||
|
];
|
||||||
|
|
||||||
|
shellHook = ''
|
||||||
|
echo "Nix-based gokrazy/tools dev env (go: $(go version))"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
5
go.mod
5
go.mod
@@ -5,9 +5,10 @@ go 1.24.0
|
|||||||
toolchain go1.24.6
|
toolchain go1.24.6
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/anatol/vmtest v0.0.0-20250627153117-302402d269a6
|
||||||
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0
|
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0
|
||||||
github.com/gokrazy/gokapi v0.0.0-20250222080418-e140e9c461d8
|
github.com/gokrazy/gokapi v0.0.0-20251205165548-0927bab199d4
|
||||||
github.com/gokrazy/internal v0.0.0-20250526201501-559979153369
|
github.com/gokrazy/internal v0.0.0-20251208203110-3c1aa9087c82
|
||||||
github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c
|
github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c
|
||||||
github.com/google/renameio/v2 v2.0.0
|
github.com/google/renameio/v2 v2.0.0
|
||||||
github.com/mattn/go-isatty v0.0.20
|
github.com/mattn/go-isatty v0.0.20
|
||||||
|
|||||||
17
go.sum
17
go.sum
@@ -1,12 +1,16 @@
|
|||||||
|
github.com/anatol/vmtest v0.0.0-20250627153117-302402d269a6 h1:zdaWj/ncXyzpPH3YqACvJXMrJxkkILrnWbjHojHBctc=
|
||||||
|
github.com/anatol/vmtest v0.0.0-20250627153117-302402d269a6/go.mod h1:m5pN88x7ZnEDGXZldwg7RCX+EikR9qz/iSI2GzXq++Y=
|
||||||
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
|
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||||
|
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/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 h1:C7t6eeMaEQVy6e8CarIhscYQlNmw5e3G36y7l7Y21Ao=
|
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0 h1:C7t6eeMaEQVy6e8CarIhscYQlNmw5e3G36y7l7Y21Ao=
|
||||||
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw=
|
github.com/donovanhide/eventsource v0.0.0-20210830082556-c59027999da0/go.mod h1:56wL82FO0bfMU5RvfXoIwSOP2ggqqxT+tAfNEIyxuHw=
|
||||||
github.com/gokrazy/gokapi v0.0.0-20250222080418-e140e9c461d8 h1:BvyzTtbpz1GCGD35Z3G/ZR0nK0j3Fh+dRCCso+w3RKE=
|
github.com/gokrazy/gokapi v0.0.0-20251205165548-0927bab199d4 h1:XFo3EqnHUbmAySp7zqms8ee/tU8bM9k+YzT7L4o5CcQ=
|
||||||
github.com/gokrazy/gokapi v0.0.0-20250222080418-e140e9c461d8/go.mod h1:rVItujrJo0NpYZhFR5dYdzLDqMoMCtjEZkdxoCRDo+o=
|
github.com/gokrazy/gokapi v0.0.0-20251205165548-0927bab199d4/go.mod h1:+StofDb/2cMb7vbA2znaNolgp9SadTYeyRIFtdhH1KQ=
|
||||||
github.com/gokrazy/internal v0.0.0-20250526201501-559979153369 h1:aNni2iPwJbowfHW1SFapKLfY+ZPUIcBfFrJvYPAh3p4=
|
github.com/gokrazy/internal v0.0.0-20251208203110-3c1aa9087c82 h1:4ghNfD9NaZLpFrqQiBF6mPVFeMYXJSky38ubVA4ic2E=
|
||||||
github.com/gokrazy/internal v0.0.0-20250526201501-559979153369/go.mod h1:dQY4EMkD4L5ZjYJ0SPtpgYbV7MIUMCxNIXiOfnZ6jP4=
|
github.com/gokrazy/internal v0.0.0-20251208203110-3c1aa9087c82/go.mod h1:dQY4EMkD4L5ZjYJ0SPtpgYbV7MIUMCxNIXiOfnZ6jP4=
|
||||||
github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c h1:j4/v9FR/cOy6nog5rmXUtauBsOU3mm+rTPn5IENUbmg=
|
github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c h1:j4/v9FR/cOy6nog5rmXUtauBsOU3mm+rTPn5IENUbmg=
|
||||||
github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c/go.mod h1:EtAn+BPibqnAHnYGj3FW5e284xNsiOOMOL2dJiwu7H4=
|
github.com/gokrazy/updater v0.0.0-20250705135802-db129c40879c/go.mod h1:EtAn+BPibqnAHnYGj3FW5e284xNsiOOMOL2dJiwu7H4=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||||
@@ -17,11 +21,15 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250911151450-96dc232fbd79 h1:WZWglxfb13JCTbJyKY1pk0V94spHxJzMAQW29INytRQ=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20250911151450-96dc232fbd79 h1:WZWglxfb13JCTbJyKY1pk0V94spHxJzMAQW29INytRQ=
|
||||||
golang.org/x/crypto/x509roots/fallback v0.0.0-20250911151450-96dc232fbd79/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
|
golang.org/x/crypto/x509roots/fallback v0.0.0-20250911151450-96dc232fbd79/go.mod h1:MEIPiCnxvQEjA4astfaKItNwEVZA5Ki+3+nyGbJ5N18=
|
||||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||||
@@ -34,4 +42,5 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type Context struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c Context) Execute(ctx context.Context) error {
|
func (c Context) Execute(ctx context.Context) error {
|
||||||
root := gok.RootCmd
|
root := gok.RootCmd()
|
||||||
if r := c.Stdin; r != nil {
|
if r := c.Stdin; r != nil {
|
||||||
root.SetIn(r)
|
root.SetIn(r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ func TestGokRun(t *testing.T) {
|
|||||||
|
|
||||||
// Testing the root command because individual cobra commands cannot be
|
// Testing the root command because individual cobra commands cannot be
|
||||||
// executed directly.
|
// executed directly.
|
||||||
root := gok.RootCmd
|
root := gok.RootCmd()
|
||||||
root.SetContext(ctx)
|
root.SetContext(ctx)
|
||||||
logOutputFound := make(chan bool)
|
logOutputFound := make(chan bool)
|
||||||
rd, wr := io.Pipe()
|
rd, wr := io.Pipe()
|
||||||
|
|||||||
@@ -2,41 +2,54 @@ package gokupdate_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"hash"
|
|
||||||
"hash/crc32"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gokrazy/internal/config"
|
"github.com/gokrazy/internal/config"
|
||||||
|
"github.com/gokrazy/internal/tlsflag"
|
||||||
"github.com/gokrazy/tools/gok"
|
"github.com/gokrazy/tools/gok"
|
||||||
"github.com/gokrazy/tools/internal/packer"
|
"github.com/gokrazy/tools/internal/packer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type gokrazyTestInstance struct {
|
type gokrazyTestInstance struct {
|
||||||
|
name string
|
||||||
configDir string
|
configDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (inst *gokrazyTestInstance) writeConfig(t *testing.T, basename, content string) {
|
func (inst *gokrazyTestInstance) configPath() string {
|
||||||
|
return "gokrazy/" + inst.name + "/config.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inst *gokrazyTestInstance) readConfig(t *testing.T) config.Struct {
|
||||||
|
b, err := os.ReadFile(inst.configPath())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var cfg config.Struct
|
||||||
|
if err := json.Unmarshal(b, &cfg); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func (inst *gokrazyTestInstance) writeConfig(t *testing.T, cfg config.Struct) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
fn := filepath.Join(inst.configDir, basename)
|
t.Logf("Writing updated cfg = %+v", cfg)
|
||||||
if err := os.WriteFile(fn, []byte(content), 0600); err != nil {
|
b, err := cfg.FormatForFile()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(inst.configPath(), b, 0644); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeGokrazyInstance(t *testing.T) *gokrazyTestInstance {
|
func writeGokrazyInstance(t *testing.T, name string) *gokrazyTestInstance {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
// Redirect os.UserConfigDir() to a temporary directory under our
|
// Redirect os.UserConfigDir() to a temporary directory under our
|
||||||
@@ -64,6 +77,7 @@ func writeGokrazyInstance(t *testing.T) *gokrazyTestInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &gokrazyTestInstance{
|
return &gokrazyTestInstance{
|
||||||
|
name: name,
|
||||||
configDir: configDir,
|
configDir: configDir,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,85 +87,17 @@ func TestGokUpdate(t *testing.T) {
|
|||||||
// gokrazy/tools repository working copy.
|
// gokrazy/tools repository working copy.
|
||||||
t.Chdir(t.TempDir())
|
t.Chdir(t.TempDir())
|
||||||
|
|
||||||
_ = writeGokrazyInstance(t)
|
|
||||||
|
|
||||||
// TODO: run the gokrazy instance in a VM instead of providing a fake
|
|
||||||
// implementation of the update protocol.
|
|
||||||
mux := http.NewServeMux()
|
|
||||||
mux.HandleFunc("/update/features", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Error(w, "not found", http.StatusNotFound)
|
|
||||||
})
|
|
||||||
mux.HandleFunc("/update/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// accept whatever for now.
|
|
||||||
var hash hash.Hash
|
|
||||||
switch r.Header.Get("X-Gokrazy-Update-Hash") {
|
|
||||||
case "crc32":
|
|
||||||
hash = crc32.NewIEEE()
|
|
||||||
default:
|
|
||||||
hash = sha256.New()
|
|
||||||
}
|
|
||||||
if _, err := io.Copy(hash, r.Body); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Fprintf(w, "%x", hash.Sum(nil))
|
|
||||||
})
|
|
||||||
mux.HandleFunc("/reboot", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// you got it, boss!
|
|
||||||
})
|
|
||||||
mux.HandleFunc("/uploadtemp/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("[HTTP] uploadtemp: %s", r.URL.Path)
|
|
||||||
})
|
|
||||||
mux.HandleFunc("/divert", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("[HTTP] divert: %s to %s",
|
|
||||||
r.FormValue("path"),
|
|
||||||
r.FormValue("diversion"))
|
|
||||||
})
|
|
||||||
mux.HandleFunc("/log", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Printf("[HTTP] log: %s", r.FormValue("path"))
|
|
||||||
w.Header().Set("Content-type", "text/event-stream")
|
|
||||||
if r.FormValue("stream") == "stdout" {
|
|
||||||
const text = "Hello Sun"
|
|
||||||
line := fmt.Sprintf("data: %s\n", text)
|
|
||||||
if _, err := fmt.Fprintln(w, line); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if f, ok := w.(http.Flusher); ok {
|
|
||||||
f.Flush()
|
|
||||||
}
|
|
||||||
select {}
|
|
||||||
})
|
|
||||||
fakeBuildTimestamp := "fake-" + time.Now().Format(time.RFC3339)
|
|
||||||
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if strings.Contains(strings.ToLower(r.Header.Get("Accept")), "application/json") {
|
|
||||||
status := struct {
|
|
||||||
BuildTimestamp string `json:"BuildTimestamp"`
|
|
||||||
}{
|
|
||||||
BuildTimestamp: fakeBuildTimestamp,
|
|
||||||
}
|
|
||||||
b, err := json.Marshal(&status)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write(b)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
http.Error(w, "handler not implemented", http.StatusNotImplemented)
|
|
||||||
})
|
|
||||||
srv := httptest.NewServer(mux)
|
|
||||||
u, err := url.Parse(srv.URL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a new instance
|
// create a new instance
|
||||||
|
const (
|
||||||
|
instanceName = "hello"
|
||||||
|
hostname = "localhost"
|
||||||
|
)
|
||||||
|
ti := writeGokrazyInstance(t, instanceName)
|
||||||
|
|
||||||
c := gok.Context{
|
c := gok.Context{
|
||||||
Args: []string{
|
Args: []string{
|
||||||
"--parent_dir", "gokrazy",
|
"--parent_dir", "gokrazy",
|
||||||
"-i", "hello",
|
"-i", instanceName,
|
||||||
"new",
|
"new",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -160,33 +106,73 @@ func TestGokUpdate(t *testing.T) {
|
|||||||
t.Fatalf("%v: %v", c.Args, err)
|
t.Fatalf("%v: %v", c.Args, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the instance config to speak to the test server
|
// and update the (default) instance config for our test
|
||||||
const configPath = "gokrazy/hello/config.json"
|
{
|
||||||
b, err := os.ReadFile(configPath)
|
cfg := ti.readConfig(t)
|
||||||
|
|
||||||
|
// use generic kernel, enable serial console
|
||||||
|
// TODO: use arm64 kernel when running on arm64
|
||||||
|
kernelPackage := "github.com/gokrazy/kernel.amd64"
|
||||||
|
cfg.KernelPackage = &kernelPackage
|
||||||
|
cfg.FirmwarePackage = &kernelPackage
|
||||||
|
cfg.SerialConsole = "ttyS0,115200"
|
||||||
|
cfg.Environment = []string{"GOOS=linux", "GOARCH=amd64"}
|
||||||
|
|
||||||
|
cfg.Hostname = hostname
|
||||||
|
cfg.Update.Hostname = hostname
|
||||||
|
cfg.Update.HTTPPort = "9080"
|
||||||
|
cfg.Update.HTTPSPort = "9443"
|
||||||
|
t.Logf("Updated cfg.Update = %+v", cfg.Update)
|
||||||
|
|
||||||
|
ti.writeConfig(t, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("booting gokrazy instance in a VM")
|
||||||
|
qemu := Run(t, nil)
|
||||||
|
defer qemu.Kill()
|
||||||
|
// TODO: kill the test if this qemu process dies for any reason
|
||||||
|
// test by setting an aggressive QemuOptions.Timeout
|
||||||
|
|
||||||
|
// wait for this instance to become healthy
|
||||||
|
//
|
||||||
|
// TODO: include the actual build timestamp once gok overwrite returns it.
|
||||||
|
if err := qemu.ConsoleExpect("gokrazy build timestamp "); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Logf("gokrazy VM booted up, waiting for network reachability")
|
||||||
|
// poll for reachability over the network
|
||||||
|
for start := time.Now(); time.Since(start) < 10*time.Second; time.Sleep(1 * time.Second) {
|
||||||
|
ctx, cancel := context.WithTimeout(t.Context(), 1*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
req, err := http.NewRequest("GET", "http://localhost:9080", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("VM not yet reachable: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if resp.StatusCode == http.StatusUnauthorized {
|
||||||
|
t.Logf("gokrazy VM became reachable over the network")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make 'gok update' not change directory?
|
||||||
|
dir, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
var cfg config.Struct
|
|
||||||
if err := json.Unmarshal(b, &cfg); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
cfg.Update.Hostname = "localhost"
|
|
||||||
cfg.Update.HTTPPort = u.Port()
|
|
||||||
t.Logf("Updated cfg.Update = %+v", cfg.Update)
|
|
||||||
b, err = cfg.FormatForFile()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(configPath, b, 0644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify overwrite works (i.e. locates extrafiles)
|
// verify overwrite works (i.e. locates extrafiles)
|
||||||
|
fakeBuildTimestamp := "fake-update-1"
|
||||||
ctx := context.WithValue(context.Background(), packer.BuildTimestampOverride, fakeBuildTimestamp)
|
ctx := context.WithValue(context.Background(), packer.BuildTimestampOverride, fakeBuildTimestamp)
|
||||||
c = gok.Context{
|
c = gok.Context{
|
||||||
Args: []string{
|
Args: []string{
|
||||||
"--parent_dir", "gokrazy",
|
"--parent_dir", "gokrazy",
|
||||||
"-i", "hello",
|
"-i", instanceName,
|
||||||
"update",
|
"update",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -194,4 +180,83 @@ func TestGokUpdate(t *testing.T) {
|
|||||||
if err := c.Execute(ctx); err != nil {
|
if err := c.Execute(ctx); err != nil {
|
||||||
t.Fatalf("%v: %v", c.Args, err)
|
t.Fatalf("%v: %v", c.Args, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := os.Chdir(dir); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// change to use self-signed TLS certificates
|
||||||
|
t.Logf("Setting Update.UseTLS = self-signed")
|
||||||
|
|
||||||
|
{
|
||||||
|
cfg := ti.readConfig(t)
|
||||||
|
|
||||||
|
cfg.Update.UseTLS = "self-signed"
|
||||||
|
t.Logf("Updated cfg.Update = %+v", cfg.Update)
|
||||||
|
|
||||||
|
ti.writeConfig(t, cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeBuildTimestamp = "fake-update-2"
|
||||||
|
ctx = context.WithValue(context.Background(), packer.BuildTimestampOverride, fakeBuildTimestamp)
|
||||||
|
c = gok.Context{
|
||||||
|
Args: []string{
|
||||||
|
"--parent_dir", "gokrazy",
|
||||||
|
"-i", instanceName,
|
||||||
|
"update",
|
||||||
|
"--insecure", // only on first update after enabling self-signed TLS
|
||||||
|
},
|
||||||
|
}
|
||||||
|
t.Logf("running %q", append([]string{"<gok>"}, c.Args...))
|
||||||
|
if err := c.Execute(ctx); err != nil {
|
||||||
|
t.Fatalf("%v: %v", c.Args, err)
|
||||||
|
}
|
||||||
|
if err := os.Chdir(dir); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("first update succeeded, doing another update without --insecure")
|
||||||
|
fakeBuildTimestamp = "fake-update-3"
|
||||||
|
ctx = context.WithValue(context.Background(), packer.BuildTimestampOverride, fakeBuildTimestamp)
|
||||||
|
c = gok.Context{
|
||||||
|
Args: []string{
|
||||||
|
"--parent_dir", "gokrazy",
|
||||||
|
"-i", instanceName,
|
||||||
|
"update",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
t.Logf("running %q", append([]string{"<gok>"}, c.Args...))
|
||||||
|
if err := c.Execute(ctx); err != nil {
|
||||||
|
t.Fatalf("%v: %v", c.Args, err)
|
||||||
|
}
|
||||||
|
if err := os.Chdir(dir); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("second update succeeded, doing another update after deleting the certificates (with --insecure)")
|
||||||
|
certPath, keyPath, err := tlsflag.CertificatePathsFor("self-signed", hostname)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Remove(certPath); err != nil {
|
||||||
|
t.Fatalf("deleting certificate: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.Remove(keyPath); err != nil {
|
||||||
|
t.Fatalf("deleting certificate: %v", err)
|
||||||
|
}
|
||||||
|
fakeBuildTimestamp = "fake-update-4"
|
||||||
|
ctx = context.WithValue(context.Background(), packer.BuildTimestampOverride, fakeBuildTimestamp)
|
||||||
|
c = gok.Context{
|
||||||
|
Args: []string{
|
||||||
|
"--parent_dir", "gokrazy",
|
||||||
|
"-i", instanceName,
|
||||||
|
"update",
|
||||||
|
"--insecure", // because we deleted the certificate files
|
||||||
|
},
|
||||||
|
}
|
||||||
|
t.Logf("running %q", append([]string{"<gok>"}, c.Args...))
|
||||||
|
if err := c.Execute(ctx); err != nil {
|
||||||
|
t.Fatalf("%v: %v", c.Args, err)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
97
integration/gokupdate/integration_test.go
Normal file
97
integration/gokupdate/integration_test.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package gokupdate_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anatol/vmtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func effectiveGOARCH() string {
|
||||||
|
goarch := os.Getenv("GOARCH")
|
||||||
|
if goarch != "" {
|
||||||
|
return goarch
|
||||||
|
}
|
||||||
|
return runtime.GOARCH
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: move to a gokrazy/internal/integrationtest package
|
||||||
|
func Run(t *testing.T, qemuArgs []string) *vmtest.Qemu {
|
||||||
|
tempdir := t.TempDir()
|
||||||
|
diskImage := filepath.Join(tempdir, "gokrazy.img")
|
||||||
|
// diskImage := "/tmp/gokrazy.img" // for debugging
|
||||||
|
|
||||||
|
// TODO: use in-process gok overwrite
|
||||||
|
packer := exec.Command("gok",
|
||||||
|
"overwrite",
|
||||||
|
"--instance=hello",
|
||||||
|
"--parent_dir=gokrazy",
|
||||||
|
"--full="+diskImage,
|
||||||
|
"--target_storage_bytes="+strconv.Itoa(2*1024*1024*1024))
|
||||||
|
packer.Env = append(os.Environ(), "GOARCH=amd64")
|
||||||
|
packer.Stdout = os.Stdout
|
||||||
|
packer.Stderr = os.Stderr
|
||||||
|
log.Printf("%s", packer.Args)
|
||||||
|
if err := packer.Run(); err != nil {
|
||||||
|
t.Fatalf("%s: %v", packer.Args, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chosen to match internal/gok/vmrun.go
|
||||||
|
qemuArgs = append(qemuArgs,
|
||||||
|
//"-enable-kvm",
|
||||||
|
//"-cpu", "host",
|
||||||
|
"-nodefaults",
|
||||||
|
"-m", "1024",
|
||||||
|
// required! system gets stuck without -smp
|
||||||
|
"-smp", strconv.Itoa(max(runtime.NumCPU(), 2)),
|
||||||
|
"-device", "e1000,netdev=net0",
|
||||||
|
"-netdev", "user,id=net0,hostfwd=tcp::9080-:9080,hostfwd=tcp::9022-:22,hostfwd=tcp::9443-:9443",
|
||||||
|
// Use -drive instead of vmtest.QemuOptions.Disks because the latter
|
||||||
|
// results in wiring up the devices using SCSI in a way that the
|
||||||
|
// router7 kernel config does not support.
|
||||||
|
// TODO: update kernel config and switch to Disks:
|
||||||
|
"-boot", "order=d",
|
||||||
|
"-drive", "file="+diskImage+",format=raw",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Do not use hardware acceleration on GitHub Actions,
|
||||||
|
// where there is no nested KVM available (by default).
|
||||||
|
if os.Getenv("GITHUB_ACTIONS") != "true" {
|
||||||
|
goarch := effectiveGOARCH()
|
||||||
|
if goarch == runtime.GOARCH {
|
||||||
|
// Hardware acceleration (in both cases) is only available for the
|
||||||
|
// native architecture, e.g. arm64 for M1 MacBooks.
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
qemuArgs = append(qemuArgs, "-accel", "kvm")
|
||||||
|
case "darwin":
|
||||||
|
qemuArgs = append(qemuArgs, "-accel", "hvf")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := vmtest.QemuOptions{
|
||||||
|
Architecture: vmtest.QEMU_X86_64,
|
||||||
|
OperatingSystem: vmtest.OS_LINUX,
|
||||||
|
Params: qemuArgs,
|
||||||
|
// Disks: []vmtest.QemuDisk{
|
||||||
|
// {
|
||||||
|
// Path: diskImage,
|
||||||
|
// Format: "raw",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
Verbose: testing.Verbose(),
|
||||||
|
Timeout: 30 * time.Minute,
|
||||||
|
}
|
||||||
|
qemu, err := vmtest.NewQemu(&opts)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return qemu
|
||||||
|
}
|
||||||
398
internal/cap/License
Normal file
398
internal/cap/License
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
/* SPDX-License-Identifier: BSD-3-Clause OR GPL-2.0-only */
|
||||||
|
|
||||||
|
Unless otherwise *explicitly* stated, the following text describes the
|
||||||
|
licensed conditions under which the contents of this libcap/cap release
|
||||||
|
may be used and distributed.
|
||||||
|
|
||||||
|
The licensed conditions are one or the other of these two Licenses:
|
||||||
|
|
||||||
|
- BSD 3-clause
|
||||||
|
- GPL v2.0
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------
|
||||||
|
BSD 3-clause:
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms of libcap/cap, with
|
||||||
|
or without modification, are permitted provided that the following
|
||||||
|
conditions are met:
|
||||||
|
|
||||||
|
1. Redistributions of source code must retain any existing copyright
|
||||||
|
notice, and this entire permission notice in its entirety,
|
||||||
|
including the disclaimer of warranties.
|
||||||
|
|
||||||
|
2. Redistributions in binary form must reproduce all prior and current
|
||||||
|
copyright notices, this list of conditions, and the following
|
||||||
|
disclaimer in the documentation and/or other materials provided
|
||||||
|
with the distribution.
|
||||||
|
|
||||||
|
3. The name of any author may not be used to endorse or promote
|
||||||
|
products derived from this software without their specific prior
|
||||||
|
written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
||||||
|
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||||
|
IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
|
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||||
|
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
||||||
|
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
||||||
|
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||||
|
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
|
DAMAGE.
|
||||||
|
|
||||||
|
-------------------------------------------------------------------------
|
||||||
|
GPL v2.0:
|
||||||
|
---------
|
||||||
|
|
||||||
|
ALTERNATIVELY, this product may be distributed under the terms of the
|
||||||
|
GNU General Public License (v2.0 - see below), in which case the
|
||||||
|
provisions of the GNU GPL are required INSTEAD OF the above
|
||||||
|
restrictions. (This clause is necessary due to a potential conflict
|
||||||
|
between the GNU GPL and the restrictions contained in a BSD-style
|
||||||
|
copyright.)
|
||||||
|
|
||||||
|
-------------------------
|
||||||
|
Full text of gpl-2.0.txt:
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 2, June 1991
|
||||||
|
|
||||||
|
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The licenses for most software are designed to take away your
|
||||||
|
freedom to share and change it. By contrast, the GNU General Public
|
||||||
|
License is intended to guarantee your freedom to share and change free
|
||||||
|
software--to make sure the software is free for all its users. This
|
||||||
|
General Public License applies to most of the Free Software
|
||||||
|
Foundation's software and to any other program whose authors commit to
|
||||||
|
using it. (Some other Free Software Foundation software is covered by
|
||||||
|
the GNU Lesser General Public License instead.) You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
this service if you wish), that you receive source code or can get it
|
||||||
|
if you want it, that you can change the software or use pieces of it
|
||||||
|
in new free programs; and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to make restrictions that forbid
|
||||||
|
anyone to deny you these rights or to ask you to surrender the rights.
|
||||||
|
These restrictions translate to certain responsibilities for you if you
|
||||||
|
distribute copies of the software, or if you modify it.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must give the recipients all the rights that
|
||||||
|
you have. You must make sure that they, too, receive or can get the
|
||||||
|
source code. And you must show them these terms so they know their
|
||||||
|
rights.
|
||||||
|
|
||||||
|
We protect your rights with two steps: (1) copyright the software, and
|
||||||
|
(2) offer you this license which gives you legal permission to copy,
|
||||||
|
distribute and/or modify the software.
|
||||||
|
|
||||||
|
Also, for each author's protection and ours, we want to make certain
|
||||||
|
that everyone understands that there is no warranty for this free
|
||||||
|
software. If the software is modified by someone else and passed on, we
|
||||||
|
want its recipients to know that what they have is not the original, so
|
||||||
|
that any problems introduced by others will not reflect on the original
|
||||||
|
authors' reputations.
|
||||||
|
|
||||||
|
Finally, any free program is threatened constantly by software
|
||||||
|
patents. We wish to avoid the danger that redistributors of a free
|
||||||
|
program will individually obtain patent licenses, in effect making the
|
||||||
|
program proprietary. To prevent this, we have made it clear that any
|
||||||
|
patent must be licensed for everyone's free use or not licensed at all.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. This License applies to any program or other work which contains
|
||||||
|
a notice placed by the copyright holder saying it may be distributed
|
||||||
|
under the terms of this General Public License. The "Program", below,
|
||||||
|
refers to any such program or work, and a "work based on the Program"
|
||||||
|
means either the Program or any derivative work under copyright law:
|
||||||
|
that is to say, a work containing the Program or a portion of it,
|
||||||
|
either verbatim or with modifications and/or translated into another
|
||||||
|
language. (Hereinafter, translation is included without limitation in
|
||||||
|
the term "modification".) Each licensee is addressed as "you".
|
||||||
|
|
||||||
|
Activities other than copying, distribution and modification are not
|
||||||
|
covered by this License; they are outside its scope. The act of
|
||||||
|
running the Program is not restricted, and the output from the Program
|
||||||
|
is covered only if its contents constitute a work based on the
|
||||||
|
Program (independent of having been made by running the Program).
|
||||||
|
Whether that is true depends on what the Program does.
|
||||||
|
|
||||||
|
1. You may copy and distribute verbatim copies of the Program's
|
||||||
|
source code as you receive it, in any medium, provided that you
|
||||||
|
conspicuously and appropriately publish on each copy an appropriate
|
||||||
|
copyright notice and disclaimer of warranty; keep intact all the
|
||||||
|
notices that refer to this License and to the absence of any warranty;
|
||||||
|
and give any other recipients of the Program a copy of this License
|
||||||
|
along with the Program.
|
||||||
|
|
||||||
|
You may charge a fee for the physical act of transferring a copy, and
|
||||||
|
you may at your option offer warranty protection in exchange for a fee.
|
||||||
|
|
||||||
|
2. You may modify your copy or copies of the Program or any portion
|
||||||
|
of it, thus forming a work based on the Program, and copy and
|
||||||
|
distribute such modifications or work under the terms of Section 1
|
||||||
|
above, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) You must cause the modified files to carry prominent notices
|
||||||
|
stating that you changed the files and the date of any change.
|
||||||
|
|
||||||
|
b) You must cause any work that you distribute or publish, that in
|
||||||
|
whole or in part contains or is derived from the Program or any
|
||||||
|
part thereof, to be licensed as a whole at no charge to all third
|
||||||
|
parties under the terms of this License.
|
||||||
|
|
||||||
|
c) If the modified program normally reads commands interactively
|
||||||
|
when run, you must cause it, when started running for such
|
||||||
|
interactive use in the most ordinary way, to print or display an
|
||||||
|
announcement including an appropriate copyright notice and a
|
||||||
|
notice that there is no warranty (or else, saying that you provide
|
||||||
|
a warranty) and that users may redistribute the program under
|
||||||
|
these conditions, and telling the user how to view a copy of this
|
||||||
|
License. (Exception: if the Program itself is interactive but
|
||||||
|
does not normally print such an announcement, your work based on
|
||||||
|
the Program is not required to print an announcement.)
|
||||||
|
|
||||||
|
These requirements apply to the modified work as a whole. If
|
||||||
|
identifiable sections of that work are not derived from the Program,
|
||||||
|
and can be reasonably considered independent and separate works in
|
||||||
|
themselves, then this License, and its terms, do not apply to those
|
||||||
|
sections when you distribute them as separate works. But when you
|
||||||
|
distribute the same sections as part of a whole which is a work based
|
||||||
|
on the Program, the distribution of the whole must be on the terms of
|
||||||
|
this License, whose permissions for other licensees extend to the
|
||||||
|
entire whole, and thus to each and every part regardless of who wrote it.
|
||||||
|
|
||||||
|
Thus, it is not the intent of this section to claim rights or contest
|
||||||
|
your rights to work written entirely by you; rather, the intent is to
|
||||||
|
exercise the right to control the distribution of derivative or
|
||||||
|
collective works based on the Program.
|
||||||
|
|
||||||
|
In addition, mere aggregation of another work not based on the Program
|
||||||
|
with the Program (or with a work based on the Program) on a volume of
|
||||||
|
a storage or distribution medium does not bring the other work under
|
||||||
|
the scope of this License.
|
||||||
|
|
||||||
|
3. You may copy and distribute the Program (or a work based on it,
|
||||||
|
under Section 2) in object code or executable form under the terms of
|
||||||
|
Sections 1 and 2 above provided that you also do one of the following:
|
||||||
|
|
||||||
|
a) Accompany it with the complete corresponding machine-readable
|
||||||
|
source code, which must be distributed under the terms of Sections
|
||||||
|
1 and 2 above on a medium customarily used for software interchange; or,
|
||||||
|
|
||||||
|
b) Accompany it with a written offer, valid for at least three
|
||||||
|
years, to give any third party, for a charge no more than your
|
||||||
|
cost of physically performing source distribution, a complete
|
||||||
|
machine-readable copy of the corresponding source code, to be
|
||||||
|
distributed under the terms of Sections 1 and 2 above on a medium
|
||||||
|
customarily used for software interchange; or,
|
||||||
|
|
||||||
|
c) Accompany it with the information you received as to the offer
|
||||||
|
to distribute corresponding source code. (This alternative is
|
||||||
|
allowed only for noncommercial distribution and only if you
|
||||||
|
received the program in object code or executable form with such
|
||||||
|
an offer, in accord with Subsection b above.)
|
||||||
|
|
||||||
|
The source code for a work means the preferred form of the work for
|
||||||
|
making modifications to it. For an executable work, complete source
|
||||||
|
code means all the source code for all modules it contains, plus any
|
||||||
|
associated interface definition files, plus the scripts used to
|
||||||
|
control compilation and installation of the executable. However, as a
|
||||||
|
special exception, the source code distributed need not include
|
||||||
|
anything that is normally distributed (in either source or binary
|
||||||
|
form) with the major components (compiler, kernel, and so on) of the
|
||||||
|
operating system on which the executable runs, unless that component
|
||||||
|
itself accompanies the executable.
|
||||||
|
|
||||||
|
If distribution of executable or object code is made by offering
|
||||||
|
access to copy from a designated place, then offering equivalent
|
||||||
|
access to copy the source code from the same place counts as
|
||||||
|
distribution of the source code, even though third parties are not
|
||||||
|
compelled to copy the source along with the object code.
|
||||||
|
|
||||||
|
4. You may not copy, modify, sublicense, or distribute the Program
|
||||||
|
except as expressly provided under this License. Any attempt
|
||||||
|
otherwise to copy, modify, sublicense or distribute the Program is
|
||||||
|
void, and will automatically terminate your rights under this License.
|
||||||
|
However, parties who have received copies, or rights, from you under
|
||||||
|
this License will not have their licenses terminated so long as such
|
||||||
|
parties remain in full compliance.
|
||||||
|
|
||||||
|
5. You are not required to accept this License, since you have not
|
||||||
|
signed it. However, nothing else grants you permission to modify or
|
||||||
|
distribute the Program or its derivative works. These actions are
|
||||||
|
prohibited by law if you do not accept this License. Therefore, by
|
||||||
|
modifying or distributing the Program (or any work based on the
|
||||||
|
Program), you indicate your acceptance of this License to do so, and
|
||||||
|
all its terms and conditions for copying, distributing or modifying
|
||||||
|
the Program or works based on it.
|
||||||
|
|
||||||
|
6. Each time you redistribute the Program (or any work based on the
|
||||||
|
Program), the recipient automatically receives a license from the
|
||||||
|
original licensor to copy, distribute or modify the Program subject to
|
||||||
|
these terms and conditions. You may not impose any further
|
||||||
|
restrictions on the recipients' exercise of the rights granted herein.
|
||||||
|
You are not responsible for enforcing compliance by third parties to
|
||||||
|
this License.
|
||||||
|
|
||||||
|
7. If, as a consequence of a court judgment or allegation of patent
|
||||||
|
infringement or for any other reason (not limited to patent issues),
|
||||||
|
conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot
|
||||||
|
distribute so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you
|
||||||
|
may not distribute the Program at all. For example, if a patent
|
||||||
|
license would not permit royalty-free redistribution of the Program by
|
||||||
|
all those who receive copies directly or indirectly through you, then
|
||||||
|
the only way you could satisfy both it and this License would be to
|
||||||
|
refrain entirely from distribution of the Program.
|
||||||
|
|
||||||
|
If any portion of this section is held invalid or unenforceable under
|
||||||
|
any particular circumstance, the balance of the section is intended to
|
||||||
|
apply and the section as a whole is intended to apply in other
|
||||||
|
circumstances.
|
||||||
|
|
||||||
|
It is not the purpose of this section to induce you to infringe any
|
||||||
|
patents or other property right claims or to contest validity of any
|
||||||
|
such claims; this section has the sole purpose of protecting the
|
||||||
|
integrity of the free software distribution system, which is
|
||||||
|
implemented by public license practices. Many people have made
|
||||||
|
generous contributions to the wide range of software distributed
|
||||||
|
through that system in reliance on consistent application of that
|
||||||
|
system; it is up to the author/donor to decide if he or she is willing
|
||||||
|
to distribute software through any other system and a licensee cannot
|
||||||
|
impose that choice.
|
||||||
|
|
||||||
|
This section is intended to make thoroughly clear what is believed to
|
||||||
|
be a consequence of the rest of this License.
|
||||||
|
|
||||||
|
8. If the distribution and/or use of the Program is restricted in
|
||||||
|
certain countries either by patents or by copyrighted interfaces, the
|
||||||
|
original copyright holder who places the Program under this License
|
||||||
|
may add an explicit geographical distribution limitation excluding
|
||||||
|
those countries, so that distribution is permitted only in or among
|
||||||
|
countries not thus excluded. In such case, this License incorporates
|
||||||
|
the limitation as if written in the body of this License.
|
||||||
|
|
||||||
|
9. The Free Software Foundation may publish revised and/or new versions
|
||||||
|
of the General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the Program
|
||||||
|
specifies a version number of this License which applies to it and "any
|
||||||
|
later version", you have the option of following the terms and conditions
|
||||||
|
either of that version or of any later version published by the Free
|
||||||
|
Software Foundation. If the Program does not specify a version number of
|
||||||
|
this License, you may choose any version ever published by the Free Software
|
||||||
|
Foundation.
|
||||||
|
|
||||||
|
10. If you wish to incorporate parts of the Program into other free
|
||||||
|
programs whose distribution conditions are different, write to the author
|
||||||
|
to ask for permission. For software which is copyrighted by the Free
|
||||||
|
Software Foundation, write to the Free Software Foundation; we sometimes
|
||||||
|
make exceptions for this. Our decision will be guided by the two goals
|
||||||
|
of preserving the free status of all derivatives of our free software and
|
||||||
|
of promoting the sharing and reuse of software generally.
|
||||||
|
|
||||||
|
NO WARRANTY
|
||||||
|
|
||||||
|
11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
|
||||||
|
FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
|
||||||
|
OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
|
||||||
|
PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
|
||||||
|
OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
|
||||||
|
TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
|
||||||
|
PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
|
||||||
|
REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
|
||||||
|
REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
|
||||||
|
OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
|
||||||
|
TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
|
||||||
|
YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
|
||||||
|
PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
|
||||||
|
POSSIBILITY OF SUCH DAMAGES.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
convey the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation; either version 2 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along
|
||||||
|
with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program is interactive, make it output a short notice like this
|
||||||
|
when it starts in an interactive mode:
|
||||||
|
|
||||||
|
Gnomovision version 69, Copyright (C) year name of author
|
||||||
|
Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, the commands you use may
|
||||||
|
be called something other than `show w' and `show c'; they could even be
|
||||||
|
mouse-clicks or menu items--whatever suits your program.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or your
|
||||||
|
school, if any, to sign a "copyright disclaimer" for the program, if
|
||||||
|
necessary. Here is a sample; alter the names:
|
||||||
|
|
||||||
|
Yoyodyne, Inc., hereby disclaims all copyright interest in the program
|
||||||
|
`Gnomovision' (which makes passes at compilers) written by James Hacker.
|
||||||
|
|
||||||
|
<signature of Ty Coon>, 1 April 1989
|
||||||
|
Ty Coon, President of Vice
|
||||||
|
|
||||||
|
This General Public License does not permit incorporating your program into
|
||||||
|
proprietary programs. If your program is a subroutine library, you may
|
||||||
|
consider it more useful to permit linking proprietary applications with the
|
||||||
|
library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License.
|
||||||
13
internal/cap/README
Normal file
13
internal/cap/README
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
This package is modified from the libcap project to allow
|
||||||
|
cross-platform marshalling to/from the kernel vfs format to write squashfs
|
||||||
|
xattr values and for parsing the text format used with the setcap command
|
||||||
|
|
||||||
|
Package cap is the libcap API for Linux Capabilities written in
|
||||||
|
Go. The official release announcement site for libcap is:
|
||||||
|
|
||||||
|
https://sites.google.com/site/fullycapable/
|
||||||
|
|
||||||
|
Like libcap, the cap package is distributed with a "you choose"
|
||||||
|
License. Specifically: BSD 3-clause, or GPL2. See the License file.
|
||||||
|
|
||||||
|
Andrew G. Morgan <morgan@kernel.org>
|
||||||
124
internal/cap/cap.go
Normal file
124
internal/cap/cap.go
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
// Package cap
|
||||||
|
// Copyright (c) 2019-21 Andrew G. Morgan <morgan@kernel.org>
|
||||||
|
//
|
||||||
|
// The cap and psx packages are licensed with a (you choose) BSD
|
||||||
|
// 3-clause or GPL2. See LICENSE file for details.
|
||||||
|
// [the Fully Capable site]: https://sites.google.com/site/fullycapable/
|
||||||
|
package cap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Value is the type of a single capability (or permission) bit.
|
||||||
|
type Value uint
|
||||||
|
|
||||||
|
// Flag is the type of one of the three Value dimensions held in a
|
||||||
|
// Set. It is also used in the (*IAB).Fill() method for changing the
|
||||||
|
// Bounding and Ambient Vectors.
|
||||||
|
type Flag uint
|
||||||
|
|
||||||
|
// Effective, Permitted, Inheritable are the three Flags of Values
|
||||||
|
// held in a Set.
|
||||||
|
const (
|
||||||
|
Effective Flag = iota
|
||||||
|
Permitted
|
||||||
|
Inheritable
|
||||||
|
)
|
||||||
|
|
||||||
|
// String identifies a Flag value by its conventional "e", "p" or "i"
|
||||||
|
// string abbreviation.
|
||||||
|
func (f Flag) String() string {
|
||||||
|
switch f {
|
||||||
|
case Effective:
|
||||||
|
return "e"
|
||||||
|
case Permitted:
|
||||||
|
return "p"
|
||||||
|
case Inheritable:
|
||||||
|
return "i"
|
||||||
|
default:
|
||||||
|
return "<Error>"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// data holds a 32-bit slice of the compressed bitmaps of capability
|
||||||
|
// sets as understood by the kernel.
|
||||||
|
type data [Inheritable + 1]uint32
|
||||||
|
|
||||||
|
// Set is an opaque capabilities container for a set of system
|
||||||
|
// capbilities. It holds individually addressable capability Value's
|
||||||
|
// for the three capability Flag's. See GetFlag() and SetFlag() for
|
||||||
|
// how to adjust them individually, and Clear() and ClearFlag() for
|
||||||
|
// how to do bulk operations.
|
||||||
|
//
|
||||||
|
// For admin tasks associated with managing namespace specific file
|
||||||
|
// capabilities, Set can also support a namespace-root-UID value which
|
||||||
|
// defaults to zero. See GetNSOwner() and SetNSOwner().
|
||||||
|
type Set struct {
|
||||||
|
// mu protects all other members of a Set.
|
||||||
|
mu sync.RWMutex
|
||||||
|
|
||||||
|
// flat holds Flag Value bitmaps for all capabilities
|
||||||
|
// associated with this Set.
|
||||||
|
flat []data
|
||||||
|
|
||||||
|
// Linux specific
|
||||||
|
nsRoot int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Various known kernel magic values.
|
||||||
|
const (
|
||||||
|
kv1 = 0x19980330 // First iteration of process capabilities (32 bits).
|
||||||
|
kv2 = 0x20071026 // First iteration of process and file capabilities (64 bits) - deprecated.
|
||||||
|
kv3 = 0x20080522 // Most recently supported process and file capabilities (64 bits).
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// startUp protects setting of the following values: magic,
|
||||||
|
// words, maxValues.
|
||||||
|
startUp sync.Once
|
||||||
|
|
||||||
|
// magic holds the preferred magic number for the kernel ABI.
|
||||||
|
magic uint32
|
||||||
|
|
||||||
|
// words holds the number of uint32's associated with each
|
||||||
|
// capability Flag for this session.
|
||||||
|
words int
|
||||||
|
|
||||||
|
// maxValues holds the number of bit values that are named by
|
||||||
|
// the running kernel. This is generally expected to match
|
||||||
|
// ValueCount which is autogenerated at packaging time.
|
||||||
|
maxValues uint
|
||||||
|
)
|
||||||
|
|
||||||
|
type header struct {
|
||||||
|
magic uint32
|
||||||
|
pid int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// defines from uapi/linux/prctl.h
|
||||||
|
const (
|
||||||
|
prCapBSetRead = 23
|
||||||
|
prCapBSetDrop = 24
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewSet returns an empty capability set.
|
||||||
|
func NewSet() *Set {
|
||||||
|
startUp.Do(cInit)
|
||||||
|
return &Set{
|
||||||
|
flat: make([]data, words),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrBadSet indicates a nil pointer was used for a *Set, or the
|
||||||
|
// request of the Set is invalid in some way.
|
||||||
|
var ErrBadSet = errors.New("bad capability set")
|
||||||
|
|
||||||
|
// good confirms that c looks valid.
|
||||||
|
func (c *Set) good() error {
|
||||||
|
if c == nil || len(c.flat) == 0 {
|
||||||
|
return ErrBadSet
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
47
internal/cap/cinit_linux.go
Normal file
47
internal/cap/cinit_linux.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package cap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// cInit performs the lazy identification of the capability vintage of
|
||||||
|
// the running system.
|
||||||
|
func cInit() {
|
||||||
|
h := &header{
|
||||||
|
magic: kv3,
|
||||||
|
}
|
||||||
|
_, _, _ = syscall.RawSyscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(h)), uintptr(0), 0)
|
||||||
|
|
||||||
|
magic = h.magic
|
||||||
|
switch magic {
|
||||||
|
case kv1:
|
||||||
|
words = 1
|
||||||
|
case kv2, kv3:
|
||||||
|
words = 2
|
||||||
|
default:
|
||||||
|
// Fall back to a known good version.
|
||||||
|
magic = kv3
|
||||||
|
words = 2
|
||||||
|
}
|
||||||
|
// Use the bounding set to evaluate which capabilities exist.
|
||||||
|
maxValues = uint(sort.Search(32*words, func(n int) bool {
|
||||||
|
_, err := GetBound(Value(n))
|
||||||
|
return err != nil
|
||||||
|
}))
|
||||||
|
if maxValues == 0 {
|
||||||
|
// Fall back to using the largest value defined at build time.
|
||||||
|
maxValues = NamedCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func GetBound(val Value) (bool, error) {
|
||||||
|
r, _, err := syscall.RawSyscall(syscall.SYS_PRCTL, prCapBSetRead, uintptr(val), 0)
|
||||||
|
if err != 0 {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(r) > 0, nil
|
||||||
|
}
|
||||||
12
internal/cap/cinit_other.go
Normal file
12
internal/cap/cinit_other.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build !linux
|
||||||
|
|
||||||
|
package cap
|
||||||
|
|
||||||
|
// cInit performs the lazy identification of the capability vintage of
|
||||||
|
// the running system.
|
||||||
|
func cInit() {
|
||||||
|
if maxValues == 0 {
|
||||||
|
// Fall back to using the largest value defined at build time.
|
||||||
|
maxValues = NamedCount
|
||||||
|
}
|
||||||
|
}
|
||||||
174
internal/cap/file.go
Normal file
174
internal/cap/file.go
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
package cap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
// uapi/linux/xattr.h defined.
|
||||||
|
var (
|
||||||
|
xattrNameCaps, _ = syscall.BytePtrFromString("security.capability")
|
||||||
|
)
|
||||||
|
|
||||||
|
// uapi/linux/capability.h defined.
|
||||||
|
const (
|
||||||
|
vfsCapRevisionMask = uint32(0xff000000)
|
||||||
|
vfsCapFlagsMask = ^vfsCapRevisionMask
|
||||||
|
vfsCapFlagsEffective = uint32(1)
|
||||||
|
|
||||||
|
vfsCapRevision1 = uint32(0x01000000)
|
||||||
|
vfsCapRevision2 = uint32(0x02000000)
|
||||||
|
vfsCapRevision3 = uint32(0x03000000)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Data types stored in little-endian order.
|
||||||
|
|
||||||
|
type vfsCaps1 struct {
|
||||||
|
MagicEtc uint32
|
||||||
|
Data [1]struct {
|
||||||
|
Permitted, Inheritable uint32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type vfsCaps2 struct {
|
||||||
|
MagicEtc uint32
|
||||||
|
Data [2]struct {
|
||||||
|
Permitted, Inheritable uint32
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type vfsCaps3 struct {
|
||||||
|
MagicEtc uint32
|
||||||
|
Data [2]struct {
|
||||||
|
Permitted, Inheritable uint32
|
||||||
|
}
|
||||||
|
RootID uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrBadSize indicates the loaded file capability has
|
||||||
|
// an invalid number of bytes in it.
|
||||||
|
var ErrBadSize = errors.New("filecap bad size")
|
||||||
|
|
||||||
|
// ErrBadMagic indicates that the kernel preferred magic number for
|
||||||
|
// capability Set values is not supported by this package. This
|
||||||
|
// generally implies you are using an exceptionally old
|
||||||
|
// "../libcap/cap" package. An upgrade is needed, or failing that see
|
||||||
|
// [the Fully Capable site] for the way to report or review a bug.
|
||||||
|
//
|
||||||
|
// [the Fully Capable site]: https://sites.google.com/site/fullycapable/
|
||||||
|
var ErrBadMagic = errors.New("unsupported magic")
|
||||||
|
|
||||||
|
// ErrBadPath indicates a failed attempt to set a file capability on
|
||||||
|
// an irregular (non-executable) file.
|
||||||
|
var ErrBadPath = errors.New("file is not a regular executable")
|
||||||
|
|
||||||
|
// ErrOutOfRange indicates an erroneous value for MinExtFlagSize.
|
||||||
|
var ErrOutOfRange = errors.New("flag length invalid for export")
|
||||||
|
|
||||||
|
// DigestFileCap unpacks a file capability and returns it in a *Set
|
||||||
|
// form.
|
||||||
|
func DigestFileCap(d []byte) (*Set, error) {
|
||||||
|
var (
|
||||||
|
err error
|
||||||
|
raw1 vfsCaps1
|
||||||
|
raw2 vfsCaps2
|
||||||
|
raw3 vfsCaps3
|
||||||
|
)
|
||||||
|
sz := len(d)
|
||||||
|
if sz < binary.Size(raw1) || sz > binary.Size(raw3) {
|
||||||
|
return nil, ErrBadSize
|
||||||
|
}
|
||||||
|
b := bytes.NewReader(d)
|
||||||
|
var magicEtc uint32
|
||||||
|
if err = binary.Read(b, binary.LittleEndian, &magicEtc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
c := NewSet()
|
||||||
|
b.Seek(0, io.SeekStart)
|
||||||
|
switch magicEtc & vfsCapRevisionMask {
|
||||||
|
case vfsCapRevision1:
|
||||||
|
if err = binary.Read(b, binary.LittleEndian, &raw1); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
data := raw1.Data[0]
|
||||||
|
c.flat[0][Permitted] = data.Permitted
|
||||||
|
c.flat[0][Inheritable] = data.Inheritable
|
||||||
|
if raw1.MagicEtc&vfsCapFlagsMask == vfsCapFlagsEffective {
|
||||||
|
c.flat[0][Effective] = data.Inheritable | data.Permitted
|
||||||
|
}
|
||||||
|
case vfsCapRevision2:
|
||||||
|
if err = binary.Read(b, binary.LittleEndian, &raw2); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i, data := range raw2.Data {
|
||||||
|
c.flat[i][Permitted] = data.Permitted
|
||||||
|
c.flat[i][Inheritable] = data.Inheritable
|
||||||
|
if raw2.MagicEtc&vfsCapFlagsMask == vfsCapFlagsEffective {
|
||||||
|
c.flat[i][Effective] = data.Inheritable | data.Permitted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case vfsCapRevision3:
|
||||||
|
if err = binary.Read(b, binary.LittleEndian, &raw3); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for i, data := range raw3.Data {
|
||||||
|
c.flat[i][Permitted] = data.Permitted
|
||||||
|
c.flat[i][Inheritable] = data.Inheritable
|
||||||
|
if raw3.MagicEtc&vfsCapFlagsMask == vfsCapFlagsEffective {
|
||||||
|
c.flat[i][Effective] = data.Inheritable | data.Permitted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.nsRoot = int(raw3.RootID)
|
||||||
|
default:
|
||||||
|
return nil, ErrBadMagic
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackFileCap transforms a system capability into a VFS form. Because
|
||||||
|
// of the way Linux stores capabilities in the file extended
|
||||||
|
// attributes, the process is a little lossy with respect to effective
|
||||||
|
// bits.
|
||||||
|
func (c *Set) PackFileCap() ([]byte, error) {
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
var magic uint32
|
||||||
|
switch words {
|
||||||
|
case 1:
|
||||||
|
if c.nsRoot != 0 {
|
||||||
|
return nil, ErrBadSet // nsRoot not supported for single DWORD caps.
|
||||||
|
}
|
||||||
|
magic = vfsCapRevision1
|
||||||
|
case 2:
|
||||||
|
if c.nsRoot == 0 {
|
||||||
|
magic = vfsCapRevision2
|
||||||
|
break
|
||||||
|
}
|
||||||
|
magic = vfsCapRevision3
|
||||||
|
}
|
||||||
|
if magic == 0 {
|
||||||
|
return nil, ErrBadSize
|
||||||
|
}
|
||||||
|
eff := uint32(0)
|
||||||
|
for _, f := range c.flat {
|
||||||
|
eff |= (f[Permitted] | f[Inheritable]) & f[Effective]
|
||||||
|
}
|
||||||
|
if eff != 0 {
|
||||||
|
magic |= vfsCapFlagsEffective
|
||||||
|
}
|
||||||
|
b := new(bytes.Buffer)
|
||||||
|
binary.Write(b, binary.LittleEndian, magic)
|
||||||
|
for _, f := range c.flat {
|
||||||
|
binary.Write(b, binary.LittleEndian, f[Permitted])
|
||||||
|
binary.Write(b, binary.LittleEndian, f[Inheritable])
|
||||||
|
}
|
||||||
|
if c.nsRoot != 0 {
|
||||||
|
binary.Write(b, binary.LittleEndian, uint32(c.nsRoot))
|
||||||
|
}
|
||||||
|
return b.Bytes(), nil
|
||||||
|
}
|
||||||
134
internal/cap/flags.go
Normal file
134
internal/cap/flags.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package cap
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// GetFlag determines if the requested Value is enabled in the
|
||||||
|
// specified Flag of the capability Set.
|
||||||
|
func (c *Set) GetFlag(vec Flag, val Value) (bool, error) {
|
||||||
|
if err := c.good(); err != nil {
|
||||||
|
// Checked this first, because otherwise we are sure
|
||||||
|
// cInit has been called.
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
offset, mask, err := bitOf(vec, val)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
return c.flat[offset][vec]&mask != 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFlag sets the requested bits to the indicated enable state. This
|
||||||
|
// function does not perform any security checks, so values can be set
|
||||||
|
// out-of-order. Only when the Set is used to SetProc() etc., will the
|
||||||
|
// bits be checked for validity and permission by the kernel. If the
|
||||||
|
// function returns an error, the Set will not be modified.
|
||||||
|
func (c *Set) SetFlag(vec Flag, enable bool, val ...Value) error {
|
||||||
|
if err := c.good(); err != nil {
|
||||||
|
// Checked this first, because otherwise we are sure
|
||||||
|
// cInit has been called.
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
// Make a backup.
|
||||||
|
replace := make([]uint32, words)
|
||||||
|
for i := range replace {
|
||||||
|
replace[i] = c.flat[i][vec]
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
for _, v := range val {
|
||||||
|
offset, mask, err2 := bitOf(vec, v)
|
||||||
|
if err2 != nil {
|
||||||
|
err = err2
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if enable {
|
||||||
|
c.flat[offset][vec] |= mask
|
||||||
|
} else {
|
||||||
|
c.flat[offset][vec] &= ^mask
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Clean up.
|
||||||
|
for i, bits := range replace {
|
||||||
|
c.flat[i][vec] = bits
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear fully clears a capability set.
|
||||||
|
func (c *Set) Clear() error {
|
||||||
|
if err := c.good(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// startUp.Do(cInit) is not called here because c cannot be
|
||||||
|
// initialized except via this package and doing that will
|
||||||
|
// perform that call at least once (sic).
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.flat = make([]data, words)
|
||||||
|
c.nsRoot = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrBadValue indicates a bad capability value was specified.
|
||||||
|
var ErrBadValue = errors.New("bad capability value")
|
||||||
|
|
||||||
|
// bitOf converts from a Value into the offset and mask for a specific
|
||||||
|
// Value bit in the compressed (kernel ABI) representation of a
|
||||||
|
// capabilities. If the requested bit is unsupported, an error is
|
||||||
|
// returned.
|
||||||
|
func bitOf(vec Flag, val Value) (uint, uint32, error) {
|
||||||
|
if vec > Inheritable || val > Value(words*32) {
|
||||||
|
return 0, 0, ErrBadValue
|
||||||
|
}
|
||||||
|
u := uint(val)
|
||||||
|
return u / 32, uint32(1) << (u % 32), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// allMask returns the mask of valid bits in the all mask for index.
|
||||||
|
func allMask(index uint) (mask uint32) {
|
||||||
|
if maxValues == 0 {
|
||||||
|
panic("uninitialized package")
|
||||||
|
}
|
||||||
|
base := 32 * uint(index)
|
||||||
|
if maxValues <= base {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if maxValues >= 32+base {
|
||||||
|
mask = ^mask
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mask = uint32((uint64(1) << (maxValues % 32)) - 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// forceFlag sets 'all' capability values (supported by the kernel) of
|
||||||
|
// a specified Flag to enable.
|
||||||
|
func (c *Set) forceFlag(vec Flag, enable bool) error {
|
||||||
|
if err := c.good(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if vec > Inheritable {
|
||||||
|
return ErrBadSet
|
||||||
|
}
|
||||||
|
m := uint32(0)
|
||||||
|
if enable {
|
||||||
|
m = ^m
|
||||||
|
}
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
for i := range c.flat {
|
||||||
|
c.flat[i][vec] = m & allMask(uint(i))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearFlag clears all the Values associated with the specified Flag.
|
||||||
|
func (c *Set) ClearFlag(vec Flag) error {
|
||||||
|
return c.forceFlag(vec, false)
|
||||||
|
}
|
||||||
440
internal/cap/names.go
Normal file
440
internal/cap/names.go
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
package cap
|
||||||
|
|
||||||
|
/* ** DO NOT EDIT THIS FILE. IT WAS AUTO-GENERATED BY LIBCAP'S GO BUILDER (mknames.go) ** */
|
||||||
|
|
||||||
|
// NamedCount holds the number of capability values, with official
|
||||||
|
// names, known at the time this libcap/cap version was released. The
|
||||||
|
// "../libcap/cap" package is fully able to manipulate higher numbered
|
||||||
|
// capability values by numerical value. However, if you find
|
||||||
|
// cap.NamedCount < cap.MaxBits(), it is probably time to upgrade this
|
||||||
|
// package on your system.
|
||||||
|
//
|
||||||
|
// FWIW the userspace tool '/sbin/capsh' also contains a runtime check
|
||||||
|
// for the condition that libcap is behind the running kernel in this
|
||||||
|
// way.
|
||||||
|
const NamedCount = 41
|
||||||
|
|
||||||
|
// CHOWN etc., are the named capability values of the Linux
|
||||||
|
// kernel. The canonical source for each name is the
|
||||||
|
// "uapi/linux/capabilities.h" file. Some values may not be available
|
||||||
|
// (yet) where the kernel is older. The actual number of capabities
|
||||||
|
// supported by the running kernel can be obtained using the
|
||||||
|
// cap.MaxBits() function.
|
||||||
|
const (
|
||||||
|
// CHOWN allows a process to arbitrarily change the user and
|
||||||
|
// group ownership of a file.
|
||||||
|
CHOWN Value = iota
|
||||||
|
|
||||||
|
// DAC_OVERRIDE allows a process to override of all Discretionary
|
||||||
|
// Access Control (DAC) access, including ACL execute
|
||||||
|
// access. That is read, write or execute files that the
|
||||||
|
// process would otherwise not have access to. This
|
||||||
|
// excludes DAC access covered by cap.LINUX_IMMUTABLE.
|
||||||
|
DAC_OVERRIDE
|
||||||
|
|
||||||
|
// DAC_READ_SEARCH allows a process to override all DAC restrictions
|
||||||
|
// limiting the read and search of files and
|
||||||
|
// directories. This excludes DAC access covered by
|
||||||
|
// cap.LINUX_IMMUTABLE.
|
||||||
|
DAC_READ_SEARCH
|
||||||
|
|
||||||
|
// FOWNER allows a process to perform operations on files, even
|
||||||
|
// where file owner ID should otherwise need be equal to
|
||||||
|
// the UID, except where cap.FSETID is applicable. It
|
||||||
|
// doesn't override MAC and DAC restrictions.
|
||||||
|
//
|
||||||
|
// This capability permits the deletion of a file owned
|
||||||
|
// by another UID in a directory protected by the sticky
|
||||||
|
// (t) bit.
|
||||||
|
FOWNER
|
||||||
|
|
||||||
|
// FSETID allows a process to set the S_ISUID and S_ISUID bits of
|
||||||
|
// the file permissions, even when the process' effective
|
||||||
|
// UID or GID/supplementary GIDs do not match that of the
|
||||||
|
// file.
|
||||||
|
FSETID
|
||||||
|
|
||||||
|
// KILL allows a process to send a kill(2) signal to any other
|
||||||
|
// process - overriding the limitation that there be a
|
||||||
|
// [E]UID match between source and target process.
|
||||||
|
KILL
|
||||||
|
|
||||||
|
// SETGID allows a process to freely manipulate its own GIDs:
|
||||||
|
// - arbitrarily set the GID, EGID, REGID, RESGID values
|
||||||
|
// - arbitrarily set the supplementary GIDs
|
||||||
|
// - allows the forging of GID credentials passed over a
|
||||||
|
// socket
|
||||||
|
SETGID
|
||||||
|
|
||||||
|
// SETUID allows a process to freely manipulate its own UIDs:
|
||||||
|
// - arbitrarily set the UID, EUID, REUID and RESUID
|
||||||
|
// values
|
||||||
|
// - allows the forging of UID credentials passed over a
|
||||||
|
// socket
|
||||||
|
SETUID
|
||||||
|
|
||||||
|
// SETPCAP allows a process to freely manipulate its inheritable
|
||||||
|
// capabilities.
|
||||||
|
//
|
||||||
|
// Linux supports the POSIX.1e Inheritable set, the POXIX.1e (X
|
||||||
|
// vector) known in Linux as the Bounding vector, as well as
|
||||||
|
// the Linux extension Ambient vector.
|
||||||
|
//
|
||||||
|
// This capability permits dropping bits from the Bounding
|
||||||
|
// vector (ie. raising B bits in the libcap IAB
|
||||||
|
// representation). It also permits the process to raise
|
||||||
|
// Ambient vector bits that are both raised in the Permitted
|
||||||
|
// and Inheritable sets of the process. This capability cannot
|
||||||
|
// be used to raise Permitted bits, Effective bits beyond those
|
||||||
|
// already present in the process' permitted set, or
|
||||||
|
// Inheritable bits beyond those present in the Bounding
|
||||||
|
// vector.
|
||||||
|
//
|
||||||
|
// [Historical note: prior to the advent of file capabilities
|
||||||
|
// (2008), this capability was suppressed by default, as its
|
||||||
|
// unsuppressed behavior was not auditable: it could
|
||||||
|
// asynchronously grant its own Permitted capabilities to and
|
||||||
|
// remove capabilities from other processes arbitrarily. The
|
||||||
|
// former leads to undefined behavior, and the latter is better
|
||||||
|
// served by the kill system call.]
|
||||||
|
SETPCAP
|
||||||
|
|
||||||
|
// LINUX_IMMUTABLE allows a process to modify the S_IMMUTABLE and
|
||||||
|
// S_APPEND file attributes.
|
||||||
|
LINUX_IMMUTABLE
|
||||||
|
|
||||||
|
// NET_BIND_SERVICE allows a process to bind to privileged ports:
|
||||||
|
// - TCP/UDP sockets below 1024
|
||||||
|
// - ATM VCIs below 32
|
||||||
|
NET_BIND_SERVICE
|
||||||
|
|
||||||
|
// NET_BROADCAST allows a process to broadcast to the network and to
|
||||||
|
// listen to multicast.
|
||||||
|
NET_BROADCAST
|
||||||
|
|
||||||
|
// NET_ADMIN allows a process to perform network configuration
|
||||||
|
// operations:
|
||||||
|
// - interface configuration
|
||||||
|
// - administration of IP firewall, masquerading and
|
||||||
|
// accounting
|
||||||
|
// - setting debug options on sockets
|
||||||
|
// - modification of routing tables
|
||||||
|
// - setting arbitrary process, and process group
|
||||||
|
// ownership on sockets
|
||||||
|
// - binding to any address for transparent proxying
|
||||||
|
// (this is also allowed via cap.NET_RAW)
|
||||||
|
// - setting TOS (Type of service)
|
||||||
|
// - setting promiscuous mode
|
||||||
|
// - clearing driver statistics
|
||||||
|
// - multicasing
|
||||||
|
// - read/write of device-specific registers
|
||||||
|
// - activation of ATM control sockets
|
||||||
|
NET_ADMIN
|
||||||
|
|
||||||
|
// NET_RAW allows a process to use raw networking:
|
||||||
|
// - RAW sockets
|
||||||
|
// - PACKET sockets
|
||||||
|
// - binding to any address for transparent proxying
|
||||||
|
// (also permitted via cap.NET_ADMIN)
|
||||||
|
NET_RAW
|
||||||
|
|
||||||
|
// IPC_LOCK allows a process to lock shared memory segments for IPC
|
||||||
|
// purposes. Also enables mlock and mlockall system
|
||||||
|
// calls.
|
||||||
|
IPC_LOCK
|
||||||
|
|
||||||
|
// IPC_OWNER allows a process to override IPC ownership checks.
|
||||||
|
IPC_OWNER
|
||||||
|
|
||||||
|
// SYS_MODULE allows a process to initiate the loading and unloading
|
||||||
|
// of kernel modules. This capability can effectively
|
||||||
|
// modify kernel without limit.
|
||||||
|
SYS_MODULE
|
||||||
|
|
||||||
|
// SYS_RAWIO allows a process to perform raw IO:
|
||||||
|
// - permit ioper/iopl access
|
||||||
|
// - permit sending USB messages to any device via
|
||||||
|
// /dev/bus/usb
|
||||||
|
SYS_RAWIO
|
||||||
|
|
||||||
|
// SYS_CHROOT allows a process to perform a chroot syscall to change
|
||||||
|
// the effective root of the process' file system:
|
||||||
|
// redirect to directory "/" to some other location.
|
||||||
|
SYS_CHROOT
|
||||||
|
|
||||||
|
// SYS_PTRACE allows a process to perform a ptrace() of any other
|
||||||
|
// process.
|
||||||
|
SYS_PTRACE
|
||||||
|
|
||||||
|
// SYS_PACCT allows a process to configure process accounting.
|
||||||
|
SYS_PACCT
|
||||||
|
|
||||||
|
// SYS_ADMIN allows a process to perform a somewhat arbitrary
|
||||||
|
// grab-bag of privileged operations. Over time, this
|
||||||
|
// capability should weaken as specific capabilities are
|
||||||
|
// created for subsets of cap.SYS_ADMINs functionality:
|
||||||
|
// - configuration of the secure attention key
|
||||||
|
// - administration of the random device
|
||||||
|
// - examination and configuration of disk quotas
|
||||||
|
// - setting the domainname
|
||||||
|
// - setting the hostname
|
||||||
|
// - calling bdflush()
|
||||||
|
// - mount() and umount(), setting up new SMB connection
|
||||||
|
// - some autofs root ioctls
|
||||||
|
// - nfsservctl
|
||||||
|
// - VM86_REQUEST_IRQ
|
||||||
|
// - to read/write pci config on alpha
|
||||||
|
// - irix_prctl on mips (setstacksize)
|
||||||
|
// - flushing all cache on m68k (sys_cacheflush)
|
||||||
|
// - removing semaphores
|
||||||
|
// - Used instead of cap.CHOWN to "chown" IPC message
|
||||||
|
// queues, semaphores and shared memory
|
||||||
|
// - locking/unlocking of shared memory segment
|
||||||
|
// - turning swap on/off
|
||||||
|
// - forged pids on socket credentials passing
|
||||||
|
// - setting readahead and flushing buffers on block
|
||||||
|
// devices
|
||||||
|
// - setting geometry in floppy driver
|
||||||
|
// - turning DMA on/off in xd driver
|
||||||
|
// - administration of md devices (mostly the above, but
|
||||||
|
// some extra ioctls)
|
||||||
|
// - tuning the ide driver
|
||||||
|
// - access to the nvram device
|
||||||
|
// - administration of apm_bios, serial and bttv (TV)
|
||||||
|
// device
|
||||||
|
// - manufacturer commands in isdn CAPI support driver
|
||||||
|
// - reading non-standardized portions of PCI
|
||||||
|
// configuration space
|
||||||
|
// - DDI debug ioctl on sbpcd driver
|
||||||
|
// - setting up serial ports
|
||||||
|
// - sending raw qic-117 commands
|
||||||
|
// - enabling/disabling tagged queuing on SCSI
|
||||||
|
// controllers and sending arbitrary SCSI commands
|
||||||
|
// - setting encryption key on loopback filesystem
|
||||||
|
// - setting zone reclaim policy
|
||||||
|
SYS_ADMIN
|
||||||
|
|
||||||
|
// SYS_BOOT allows a process to initiate a reboot of the system.
|
||||||
|
SYS_BOOT
|
||||||
|
|
||||||
|
// SYS_NICE allows a process to maipulate the execution priorities
|
||||||
|
// of arbitrary processes:
|
||||||
|
// - those involving different UIDs
|
||||||
|
// - setting their CPU affinity
|
||||||
|
// - alter the FIFO vs. round-robin (realtime)
|
||||||
|
// scheduling for itself and other processes.
|
||||||
|
SYS_NICE
|
||||||
|
|
||||||
|
// SYS_RESOURCE allows a process to adjust resource related parameters
|
||||||
|
// of processes and the system:
|
||||||
|
// - set and override resource limits
|
||||||
|
// - override quota limits
|
||||||
|
// - override the reserved space on ext2 filesystem
|
||||||
|
// (this can also be achieved via cap.FSETID)
|
||||||
|
// - modify the data journaling mode on ext3 filesystem,
|
||||||
|
// which uses journaling resources
|
||||||
|
// - override size restrictions on IPC message queues
|
||||||
|
// - configure more than 64Hz interrupts from the
|
||||||
|
// real-time clock
|
||||||
|
// - override the maximum number of consoles for console
|
||||||
|
// allocation
|
||||||
|
// - override the maximum number of keymaps
|
||||||
|
SYS_RESOURCE
|
||||||
|
|
||||||
|
// SYS_TIME allows a process to perform time manipulation of clocks:
|
||||||
|
// - alter the system clock
|
||||||
|
// - enable irix_stime on MIPS
|
||||||
|
// - set the real-time clock
|
||||||
|
SYS_TIME
|
||||||
|
|
||||||
|
// SYS_TTY_CONFIG allows a process to manipulate tty devices:
|
||||||
|
// - configure tty devices
|
||||||
|
// - perform vhangup() of a tty
|
||||||
|
SYS_TTY_CONFIG
|
||||||
|
|
||||||
|
// MKNOD allows a process to perform privileged operations with
|
||||||
|
// the mknod() system call.
|
||||||
|
MKNOD
|
||||||
|
|
||||||
|
// LEASE allows a process to take leases on files.
|
||||||
|
LEASE
|
||||||
|
|
||||||
|
// AUDIT_WRITE allows a process to write to the audit log via a
|
||||||
|
// unicast netlink socket.
|
||||||
|
AUDIT_WRITE
|
||||||
|
|
||||||
|
// AUDIT_CONTROL allows a process to configure audit logging via a
|
||||||
|
// unicast netlink socket.
|
||||||
|
AUDIT_CONTROL
|
||||||
|
|
||||||
|
// SETFCAP allows a process to set capabilities on files.
|
||||||
|
// Permits a process to uid_map the uid=0 of the
|
||||||
|
// parent user namespace into that of the child
|
||||||
|
// namespace. Also, permits a process to override
|
||||||
|
// securebits locks through user namespace
|
||||||
|
// creation.
|
||||||
|
SETFCAP
|
||||||
|
|
||||||
|
// MAC_OVERRIDE allows a process to override Manditory Access Control
|
||||||
|
// (MAC) access. Not all kernels are configured with a MAC
|
||||||
|
// mechanism, but this is the capability reserved for
|
||||||
|
// overriding them.
|
||||||
|
MAC_OVERRIDE
|
||||||
|
|
||||||
|
// MAC_ADMIN allows a process to configure the Mandatory Access
|
||||||
|
// Control (MAC) policy. Not all kernels are configured
|
||||||
|
// with a MAC enabled, but if they are this capability is
|
||||||
|
// reserved for code to perform administration tasks.
|
||||||
|
MAC_ADMIN
|
||||||
|
|
||||||
|
// SYSLOG allows a process to configure the kernel's syslog
|
||||||
|
// (printk) behavior.
|
||||||
|
SYSLOG
|
||||||
|
|
||||||
|
// WAKE_ALARM allows a process to trigger something that can wake the
|
||||||
|
// system up.
|
||||||
|
WAKE_ALARM
|
||||||
|
|
||||||
|
// BLOCK_SUSPEND allows a process to block system suspends - prevent the
|
||||||
|
// system from entering a lower power state.
|
||||||
|
BLOCK_SUSPEND
|
||||||
|
|
||||||
|
// AUDIT_READ allows a process to read the audit log via a multicast
|
||||||
|
// netlink socket.
|
||||||
|
AUDIT_READ
|
||||||
|
|
||||||
|
// PERFMON allows a process to enable observability of privileged
|
||||||
|
// operations related to performance. The mechanisms
|
||||||
|
// include perf_events, i915_perf and other kernel
|
||||||
|
// subsystems.
|
||||||
|
PERFMON
|
||||||
|
|
||||||
|
// BPF allows a process to manipulate aspects of the kernel
|
||||||
|
// enhanced Berkeley Packet Filter (BPF) system. This is
|
||||||
|
// an execution subsystem of the kernel, that manages BPF
|
||||||
|
// programs. cap.BPF permits a process to:
|
||||||
|
// - create all types of BPF maps
|
||||||
|
// - advanced verifier features:
|
||||||
|
// - indirect variable access
|
||||||
|
// - bounded loops
|
||||||
|
// - BPF to BPF function calls
|
||||||
|
// - scalar precision tracking
|
||||||
|
// - larger complexity limits
|
||||||
|
// - dead code elimination
|
||||||
|
// - potentially other features
|
||||||
|
//
|
||||||
|
// Other capabilities can be used together with cap.BFP to
|
||||||
|
// further manipulate the BPF system:
|
||||||
|
// - cap.PERFMON relaxes the verifier checks as follows:
|
||||||
|
// - BPF programs can use pointer-to-integer
|
||||||
|
// conversions
|
||||||
|
// - speculation attack hardening measures can be
|
||||||
|
// bypassed
|
||||||
|
// - bpf_probe_read to read arbitrary kernel memory is
|
||||||
|
// permitted
|
||||||
|
// - bpf_trace_printk to print the content of kernel
|
||||||
|
// memory
|
||||||
|
// - cap.SYS_ADMIN permits the following:
|
||||||
|
// - use of bpf_probe_write_user
|
||||||
|
// - iteration over the system-wide loaded programs,
|
||||||
|
// maps, links BTFs and convert their IDs to file
|
||||||
|
// descriptors.
|
||||||
|
// - cap.PERFMON is required to load tracing programs.
|
||||||
|
// - cap.NET_ADMIN is required to load networking
|
||||||
|
// programs.
|
||||||
|
BPF
|
||||||
|
|
||||||
|
// CHECKPOINT_RESTORE allows a process to perform checkpoint
|
||||||
|
// and restore operations. Also permits
|
||||||
|
// explicit PID control via clone3() and
|
||||||
|
// also writing to ns_last_pid.
|
||||||
|
CHECKPOINT_RESTORE
|
||||||
|
)
|
||||||
|
|
||||||
|
var names = map[Value]string{
|
||||||
|
CHOWN: "cap_chown",
|
||||||
|
DAC_OVERRIDE: "cap_dac_override",
|
||||||
|
DAC_READ_SEARCH: "cap_dac_read_search",
|
||||||
|
FOWNER: "cap_fowner",
|
||||||
|
FSETID: "cap_fsetid",
|
||||||
|
KILL: "cap_kill",
|
||||||
|
SETGID: "cap_setgid",
|
||||||
|
SETUID: "cap_setuid",
|
||||||
|
SETPCAP: "cap_setpcap",
|
||||||
|
LINUX_IMMUTABLE: "cap_linux_immutable",
|
||||||
|
NET_BIND_SERVICE: "cap_net_bind_service",
|
||||||
|
NET_BROADCAST: "cap_net_broadcast",
|
||||||
|
NET_ADMIN: "cap_net_admin",
|
||||||
|
NET_RAW: "cap_net_raw",
|
||||||
|
IPC_LOCK: "cap_ipc_lock",
|
||||||
|
IPC_OWNER: "cap_ipc_owner",
|
||||||
|
SYS_MODULE: "cap_sys_module",
|
||||||
|
SYS_RAWIO: "cap_sys_rawio",
|
||||||
|
SYS_CHROOT: "cap_sys_chroot",
|
||||||
|
SYS_PTRACE: "cap_sys_ptrace",
|
||||||
|
SYS_PACCT: "cap_sys_pacct",
|
||||||
|
SYS_ADMIN: "cap_sys_admin",
|
||||||
|
SYS_BOOT: "cap_sys_boot",
|
||||||
|
SYS_NICE: "cap_sys_nice",
|
||||||
|
SYS_RESOURCE: "cap_sys_resource",
|
||||||
|
SYS_TIME: "cap_sys_time",
|
||||||
|
SYS_TTY_CONFIG: "cap_sys_tty_config",
|
||||||
|
MKNOD: "cap_mknod",
|
||||||
|
LEASE: "cap_lease",
|
||||||
|
AUDIT_WRITE: "cap_audit_write",
|
||||||
|
AUDIT_CONTROL: "cap_audit_control",
|
||||||
|
SETFCAP: "cap_setfcap",
|
||||||
|
MAC_OVERRIDE: "cap_mac_override",
|
||||||
|
MAC_ADMIN: "cap_mac_admin",
|
||||||
|
SYSLOG: "cap_syslog",
|
||||||
|
WAKE_ALARM: "cap_wake_alarm",
|
||||||
|
BLOCK_SUSPEND: "cap_block_suspend",
|
||||||
|
AUDIT_READ: "cap_audit_read",
|
||||||
|
PERFMON: "cap_perfmon",
|
||||||
|
BPF: "cap_bpf",
|
||||||
|
CHECKPOINT_RESTORE: "cap_checkpoint_restore",
|
||||||
|
}
|
||||||
|
|
||||||
|
var bits = map[string]Value{
|
||||||
|
"cap_chown": CHOWN,
|
||||||
|
"cap_dac_override": DAC_OVERRIDE,
|
||||||
|
"cap_dac_read_search": DAC_READ_SEARCH,
|
||||||
|
"cap_fowner": FOWNER,
|
||||||
|
"cap_fsetid": FSETID,
|
||||||
|
"cap_kill": KILL,
|
||||||
|
"cap_setgid": SETGID,
|
||||||
|
"cap_setuid": SETUID,
|
||||||
|
"cap_setpcap": SETPCAP,
|
||||||
|
"cap_linux_immutable": LINUX_IMMUTABLE,
|
||||||
|
"cap_net_bind_service": NET_BIND_SERVICE,
|
||||||
|
"cap_net_broadcast": NET_BROADCAST,
|
||||||
|
"cap_net_admin": NET_ADMIN,
|
||||||
|
"cap_net_raw": NET_RAW,
|
||||||
|
"cap_ipc_lock": IPC_LOCK,
|
||||||
|
"cap_ipc_owner": IPC_OWNER,
|
||||||
|
"cap_sys_module": SYS_MODULE,
|
||||||
|
"cap_sys_rawio": SYS_RAWIO,
|
||||||
|
"cap_sys_chroot": SYS_CHROOT,
|
||||||
|
"cap_sys_ptrace": SYS_PTRACE,
|
||||||
|
"cap_sys_pacct": SYS_PACCT,
|
||||||
|
"cap_sys_admin": SYS_ADMIN,
|
||||||
|
"cap_sys_boot": SYS_BOOT,
|
||||||
|
"cap_sys_nice": SYS_NICE,
|
||||||
|
"cap_sys_resource": SYS_RESOURCE,
|
||||||
|
"cap_sys_time": SYS_TIME,
|
||||||
|
"cap_sys_tty_config": SYS_TTY_CONFIG,
|
||||||
|
"cap_mknod": MKNOD,
|
||||||
|
"cap_lease": LEASE,
|
||||||
|
"cap_audit_write": AUDIT_WRITE,
|
||||||
|
"cap_audit_control": AUDIT_CONTROL,
|
||||||
|
"cap_setfcap": SETFCAP,
|
||||||
|
"cap_mac_override": MAC_OVERRIDE,
|
||||||
|
"cap_mac_admin": MAC_ADMIN,
|
||||||
|
"cap_syslog": SYSLOG,
|
||||||
|
"cap_wake_alarm": WAKE_ALARM,
|
||||||
|
"cap_block_suspend": BLOCK_SUSPEND,
|
||||||
|
"cap_audit_read": AUDIT_READ,
|
||||||
|
"cap_perfmon": PERFMON,
|
||||||
|
"cap_bpf": BPF,
|
||||||
|
"cap_checkpoint_restore": CHECKPOINT_RESTORE,
|
||||||
|
}
|
||||||
328
internal/cap/text.go
Normal file
328
internal/cap/text.go
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
package cap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// String converts a capability Value into its canonical text
|
||||||
|
// representation.
|
||||||
|
func (v Value) String() string {
|
||||||
|
name, ok := names[v]
|
||||||
|
if ok {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
// Un-named capabilities are referred to numerically (in decimal).
|
||||||
|
return strconv.Itoa(int(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromName converts a named capability Value to its binary
|
||||||
|
// representation.
|
||||||
|
func FromName(name string) (Value, error) {
|
||||||
|
startUp.Do(cInit)
|
||||||
|
v, ok := bits[name]
|
||||||
|
if ok {
|
||||||
|
if v >= Value(words*32) {
|
||||||
|
return 0, ErrBadValue
|
||||||
|
}
|
||||||
|
return v, nil
|
||||||
|
}
|
||||||
|
i, err := strconv.Atoi(name)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if i >= 0 && i < int(words*32) {
|
||||||
|
return Value(i), nil
|
||||||
|
}
|
||||||
|
return 0, ErrBadValue
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
eBin uint = (1 << Effective)
|
||||||
|
pBin = (1 << Permitted)
|
||||||
|
iBin = (1 << Inheritable)
|
||||||
|
)
|
||||||
|
|
||||||
|
var combos = []string{"", "e", "p", "ep", "i", "ei", "ip", "eip"}
|
||||||
|
|
||||||
|
// histo generates a histogram of flag state combinations.
|
||||||
|
// Note: c is locked by or private to the caller.
|
||||||
|
func (c *Set) histo(bins []int, patterns []uint, from, limit Value) uint {
|
||||||
|
for v := from; v < limit; v++ {
|
||||||
|
b := uint(v & 31)
|
||||||
|
u, bit, err := bitOf(0, v)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
x := uint((c.flat[u][Effective]&bit)>>b) * eBin
|
||||||
|
x |= uint((c.flat[u][Permitted]&bit)>>b) * pBin
|
||||||
|
x |= uint((c.flat[u][Inheritable]&bit)>>b) * iBin
|
||||||
|
bins[x]++
|
||||||
|
patterns[uint(v)] = x
|
||||||
|
}
|
||||||
|
// Note, in the loop, we use >= to pick the smallest value for
|
||||||
|
// m with the highest bin value. That is ties break towards
|
||||||
|
// m=0.
|
||||||
|
m := uint(7)
|
||||||
|
for t := m; t > 0; {
|
||||||
|
t--
|
||||||
|
if bins[t] >= bins[m] {
|
||||||
|
m = t
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// String converts a full capability Set into a single short readable
|
||||||
|
// string representation (which may contain spaces). See the
|
||||||
|
// cap.FromText() function for an explanation of its return values.
|
||||||
|
//
|
||||||
|
// Note (*cap.Set).String() may evolve to generate more compact
|
||||||
|
// strings representing the a given Set over time, but it should
|
||||||
|
// maintain compatibility with the libcap:cap_to_text() function for
|
||||||
|
// any given release. Further, it will always be an inverse of
|
||||||
|
// cap.FromText().
|
||||||
|
func (c *Set) String() string {
|
||||||
|
if err := c.good(); err != nil {
|
||||||
|
return "<invalid>"
|
||||||
|
}
|
||||||
|
bins := make([]int, 8)
|
||||||
|
patterns := make([]uint, maxValues)
|
||||||
|
|
||||||
|
c.mu.RLock()
|
||||||
|
defer c.mu.RUnlock()
|
||||||
|
|
||||||
|
// Note, in order to have a *Set pointer, startUp.Do(cInit)
|
||||||
|
// must have been called which sets maxValues.
|
||||||
|
m := c.histo(bins, patterns, 0, Value(maxValues))
|
||||||
|
|
||||||
|
// Background state is the most popular of the named bits.
|
||||||
|
vs := []string{"=" + combos[m]}
|
||||||
|
for i := uint(8); i > 0; {
|
||||||
|
i--
|
||||||
|
if i == m || bins[i] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var list []string
|
||||||
|
for j, p := range patterns {
|
||||||
|
if p != i {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
list = append(list, Value(j).String())
|
||||||
|
}
|
||||||
|
x := strings.Join(list, ",")
|
||||||
|
var y, z string
|
||||||
|
if cf := i & ^m; cf != 0 {
|
||||||
|
op := "+"
|
||||||
|
if len(vs) == 1 && vs[0] == "=" {
|
||||||
|
// Special case "= foo+..." == "foo=...".
|
||||||
|
// Prefer because it
|
||||||
|
vs = nil
|
||||||
|
op = "="
|
||||||
|
}
|
||||||
|
y = op + combos[cf]
|
||||||
|
}
|
||||||
|
if cf := m & ^i; cf != 0 {
|
||||||
|
z = "-" + combos[cf]
|
||||||
|
}
|
||||||
|
vs = append(vs, x+y+z)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The unnamed bits can only add to the above named ones since
|
||||||
|
// unnamed ones are always defaulted to lowered.
|
||||||
|
uBins := make([]int, 8)
|
||||||
|
uPatterns := make([]uint, 32*words)
|
||||||
|
c.histo(uBins, uPatterns, Value(maxValues), 32*Value(words))
|
||||||
|
for i := uint(7); i > 0; i-- {
|
||||||
|
if uBins[i] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var list []string
|
||||||
|
for j, p := range uPatterns {
|
||||||
|
if p != i {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
list = append(list, Value(j).String())
|
||||||
|
}
|
||||||
|
vs = append(vs, strings.Join(list, ",")+"+"+combos[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(vs, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrBadText is returned if the text for a capability set cannot be parsed.
|
||||||
|
var ErrBadText = errors.New("bad text")
|
||||||
|
|
||||||
|
// FromText converts the canonical text representation for a Set into
|
||||||
|
// a freshly allocated Set.
|
||||||
|
//
|
||||||
|
// The format follows the following pattern: a set of space separated
|
||||||
|
// sequences. Each sequence applies over the previous sequence to
|
||||||
|
// build up a Set. The format of a sequence is:
|
||||||
|
//
|
||||||
|
// [comma list of cap_values][[ops][flags]]*
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// "all=ep"
|
||||||
|
// "cap_chown,cap_setuid=ip cap_setuid+e"
|
||||||
|
// "=p cap_setpcap-p+i"
|
||||||
|
//
|
||||||
|
// Here "all" refers to all named capabilities known to the hosting
|
||||||
|
// kernel, and "all" is assumed if no capabilities are listed before
|
||||||
|
// an "=".
|
||||||
|
//
|
||||||
|
// The ops values, "=", "+" and "-" imply "reset and raise", "raise"
|
||||||
|
// and "lower" respectively. The "e", "i" and "p" characters
|
||||||
|
// correspond to the capabilities of the corresponding Flag: "e"
|
||||||
|
// (Effective); "i" (Inheritable); "p" (Permitted).
|
||||||
|
//
|
||||||
|
// This syntax is overspecified and there are many ways of building
|
||||||
|
// the same final Set state. Any sequence that includes a '=' resets
|
||||||
|
// the accumulated state of all Flags ignoring earlier sequences. On
|
||||||
|
// each of the following lines we give three or more examples of ways
|
||||||
|
// to specify a common Set. The last entry on each line is the one
|
||||||
|
// generated by (*cap.Set).String() from that Set.
|
||||||
|
//
|
||||||
|
// "=p all+ei" "all=pie" "=pi all+e" "=eip"
|
||||||
|
//
|
||||||
|
// "cap_setuid=p cap_chown=i" "cap_chown=ip-p" "cap_chown=i"
|
||||||
|
//
|
||||||
|
// "cap_chown=-p" "all=" "cap_setuid=pie-pie" "="
|
||||||
|
//
|
||||||
|
// Note: FromText() is tested at release time to completely match the
|
||||||
|
// import ability of the libcap:cap_from_text() function.
|
||||||
|
func FromText(text string) (*Set, error) {
|
||||||
|
text = strings.ToLower(text)
|
||||||
|
c := NewSet()
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(text))
|
||||||
|
scanner.Split(bufio.ScanWords)
|
||||||
|
chunks := 0
|
||||||
|
for scanner.Scan() {
|
||||||
|
chunks++
|
||||||
|
|
||||||
|
// Parsing for xxx([-+=][eip]+)+
|
||||||
|
t := scanner.Text()
|
||||||
|
i := strings.IndexAny(t, "=+-")
|
||||||
|
if i < 0 {
|
||||||
|
return nil, fmt.Errorf("%w: needs one of [=+-]", ErrBadText)
|
||||||
|
}
|
||||||
|
var vs []Value
|
||||||
|
sep := t[i]
|
||||||
|
if vals := t[:i]; vals == "all" {
|
||||||
|
for v := Value(0); v < Value(maxValues); v++ {
|
||||||
|
vs = append(vs, v)
|
||||||
|
}
|
||||||
|
} else if vals != "" {
|
||||||
|
for name := range strings.SplitSeq(vals, ",") {
|
||||||
|
v, err := FromName(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %w", ErrBadText, err)
|
||||||
|
}
|
||||||
|
vs = append(vs, v)
|
||||||
|
}
|
||||||
|
} else if sep != '=' {
|
||||||
|
if vals == "" {
|
||||||
|
// Only "=" supports ""=="all".
|
||||||
|
return nil, ErrBadText
|
||||||
|
}
|
||||||
|
} else if j := i + 1; j+1 < len(t) {
|
||||||
|
switch t[j] {
|
||||||
|
case '+':
|
||||||
|
sep = 'P'
|
||||||
|
i++
|
||||||
|
case '-':
|
||||||
|
sep = 'M'
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
|
||||||
|
// There are 5 ways to set: =, =+, =-, +, -. We call
|
||||||
|
// the 2nd and 3rd of these 'P' and 'M'.
|
||||||
|
|
||||||
|
for {
|
||||||
|
// read [eip]+ setting flags.
|
||||||
|
var fE, fP, fI bool
|
||||||
|
for ok := true; ok && i < len(t); i++ {
|
||||||
|
switch t[i] {
|
||||||
|
case 'e':
|
||||||
|
fE = true
|
||||||
|
case 'i':
|
||||||
|
fI = true
|
||||||
|
case 'p':
|
||||||
|
fP = true
|
||||||
|
default:
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(fE || fI || fP) {
|
||||||
|
if sep != '=' {
|
||||||
|
return nil, fmt.Errorf("%w: needs one of [eip]", ErrBadText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch sep {
|
||||||
|
case '=', 'P', 'M', '+':
|
||||||
|
if sep != '+' {
|
||||||
|
c.Clear()
|
||||||
|
if sep == 'M' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if keep := len(vs) == 0; keep {
|
||||||
|
if sep != '=' {
|
||||||
|
return nil, fmt.Errorf("%w: expected an '='", ErrBadText)
|
||||||
|
}
|
||||||
|
c.forceFlag(Effective, fE)
|
||||||
|
c.forceFlag(Permitted, fP)
|
||||||
|
c.forceFlag(Inheritable, fI)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// =, + and P for specific values are left.
|
||||||
|
if fE {
|
||||||
|
c.SetFlag(Effective, true, vs...)
|
||||||
|
}
|
||||||
|
if fP {
|
||||||
|
c.SetFlag(Permitted, true, vs...)
|
||||||
|
}
|
||||||
|
if fI {
|
||||||
|
c.SetFlag(Inheritable, true, vs...)
|
||||||
|
}
|
||||||
|
case '-':
|
||||||
|
if fE {
|
||||||
|
c.SetFlag(Effective, false, vs...)
|
||||||
|
}
|
||||||
|
if fP {
|
||||||
|
c.SetFlag(Permitted, false, vs...)
|
||||||
|
}
|
||||||
|
if fI {
|
||||||
|
c.SetFlag(Inheritable, false, vs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if i == len(t) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t[i] {
|
||||||
|
case '+', '-':
|
||||||
|
sep = t[i]
|
||||||
|
i++
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%w: expected '+' or '-'", ErrBadText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if chunks == 0 {
|
||||||
|
return nil, fmt.Errorf("%w: No capabilities found", ErrBadText)
|
||||||
|
}
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
129
internal/eeprom/eeprom.go
Normal file
129
internal/eeprom/eeprom.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
// Package eeprom implements the Raspberry Pi EEPROM update file format.
|
||||||
|
package eeprom
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Other implementations:
|
||||||
|
//
|
||||||
|
// - https://github.com/raspberrypi/rpi-eeprom (Python)
|
||||||
|
// - https://github.com/info-beamer/rpi-eeprom-tools (Python)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MAGIC = 0x55aaf00f
|
||||||
|
MAGIC_MASK = 0xfffff00f
|
||||||
|
FILE_MAGIC = 0x55aaf11f // id for modifiable files
|
||||||
|
FILENAME_LEN = 12
|
||||||
|
FILENAME_PADDING = 4
|
||||||
|
|
||||||
|
chunkHeaderLen = 4 + 4 // magic number + 32 bit offset
|
||||||
|
)
|
||||||
|
|
||||||
|
type Section struct {
|
||||||
|
img []byte
|
||||||
|
Magic uint32
|
||||||
|
Offset int
|
||||||
|
Length int
|
||||||
|
Filename string
|
||||||
|
}
|
||||||
|
|
||||||
|
func FileSection(offset int, name string, contents []byte) *Section {
|
||||||
|
if len(name) > FILENAME_LEN {
|
||||||
|
panic(fmt.Sprintf("BUG: file name %s exceeds max FILENAME_LEN = %d", name, FILENAME_LEN))
|
||||||
|
}
|
||||||
|
|
||||||
|
img := bytes.Repeat([]byte{0}, offset+chunkHeaderLen)
|
||||||
|
img = append(img, []byte(name)...)
|
||||||
|
img = append(img, bytes.Repeat([]byte{0}, FILENAME_PADDING+FILENAME_LEN-len(name))...)
|
||||||
|
img = append(img, contents...)
|
||||||
|
return &Section{
|
||||||
|
img: img,
|
||||||
|
Magic: FILE_MAGIC,
|
||||||
|
Offset: offset,
|
||||||
|
Length: len(contents) + FILENAME_LEN + FILENAME_PADDING,
|
||||||
|
Filename: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Section) WithoutImg() Section {
|
||||||
|
without := *s
|
||||||
|
without.img = nil
|
||||||
|
return without
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Section) RawContent() []byte {
|
||||||
|
offset := s.Offset + chunkHeaderLen
|
||||||
|
length := s.Length
|
||||||
|
return s.img[offset : offset+length]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Section) FileContent() []byte {
|
||||||
|
offset := s.Offset + chunkHeaderLen
|
||||||
|
length := s.Length
|
||||||
|
if s.Magic == FILE_MAGIC {
|
||||||
|
const fileSkip = FILENAME_LEN + FILENAME_PADDING
|
||||||
|
offset += fileSkip
|
||||||
|
length -= fileSkip
|
||||||
|
}
|
||||||
|
return s.img[offset : offset+length]
|
||||||
|
}
|
||||||
|
|
||||||
|
func Analyze(img []byte) ([]*Section, error) {
|
||||||
|
// See https://github.com/raspberrypi/rpi-eeprom/blob/f38dbcb72341a3c3c3e66f1e10d58f8985cb0528/rpi-eeprom-config#L267
|
||||||
|
|
||||||
|
if len(img) != 512*1024 &&
|
||||||
|
len(img) != 2*1024*1024 {
|
||||||
|
return nil, fmt.Errorf("unexpected EEPROM size: got %d, want 512KB or 2MB", len(img))
|
||||||
|
}
|
||||||
|
|
||||||
|
var sections []*Section
|
||||||
|
for offset := 0; offset+chunkHeaderLen < len(img); {
|
||||||
|
magic := binary.BigEndian.Uint32(img[offset : offset+4])
|
||||||
|
length := binary.BigEndian.Uint32(img[offset+4 : offset+8])
|
||||||
|
if magic == 0 || magic == 0xffffffff {
|
||||||
|
break // end of file
|
||||||
|
}
|
||||||
|
if magic&MAGIC_MASK != MAGIC {
|
||||||
|
return nil, fmt.Errorf("EEPROM is corrupted: %x & %x != %x", magic, MAGIC_MASK, MAGIC)
|
||||||
|
}
|
||||||
|
sect := Section{
|
||||||
|
img: img,
|
||||||
|
Magic: magic,
|
||||||
|
Offset: offset,
|
||||||
|
Length: int(length),
|
||||||
|
}
|
||||||
|
if magic == FILE_MAGIC {
|
||||||
|
sect.Filename = string(img[offset+8 : offset+8+FILENAME_LEN])
|
||||||
|
sect.Filename = strings.ReplaceAll(sect.Filename, "\x00", "")
|
||||||
|
}
|
||||||
|
sections = append(sections, §)
|
||||||
|
offset += chunkHeaderLen + int(length)
|
||||||
|
offset = (offset + 7) &^ 7
|
||||||
|
}
|
||||||
|
if len(sections) == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid EEPROM: no sections found")
|
||||||
|
}
|
||||||
|
if sections[len(sections)-1].Filename != "bootconf.txt" {
|
||||||
|
// “by convention bootconf.txt is the last section”, from:
|
||||||
|
// https://github.com/raspberrypi/rpi-eeprom/blob/f38dbcb72341a3c3c3e66f1e10d58f8985cb0528/rpi-eeprom-config#L373
|
||||||
|
return nil, fmt.Errorf("invalid EEPROM: bootconf.txt not the last section")
|
||||||
|
}
|
||||||
|
return sections, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Assemble(sections []*Section) []byte {
|
||||||
|
output := bytes.Repeat([]byte{0xff}, len(sections[0].img))
|
||||||
|
offset := 0
|
||||||
|
for _, sect := range sections {
|
||||||
|
binary.BigEndian.PutUint32(output[offset:], sect.Magic)
|
||||||
|
binary.BigEndian.PutUint32(output[offset+4:], uint32(sect.Length))
|
||||||
|
copy(output[offset+8:], sect.RawContent())
|
||||||
|
offset += chunkHeaderLen + sect.Length
|
||||||
|
offset = (offset + 7) &^ 7
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
110
internal/eeprom/eeprom_test.go
Normal file
110
internal/eeprom/eeprom_test.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package eeprom_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gokrazy/tools/internal/eeprom"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testAgainst(t *testing.T, verify func(eepromPath, needle string), pieeprom, bootUartNeedle string) {
|
||||||
|
verify(pieeprom, bootUartNeedle)
|
||||||
|
|
||||||
|
// Read + Write the EEPROM with our own code,
|
||||||
|
// verify that bootconf.txt can still be displayed.
|
||||||
|
img, err := os.ReadFile(pieeprom)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
sections, err := eeprom.Analyze(img)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
reassembleAndVerify := func(needle string) {
|
||||||
|
reassembled := filepath.Join(t.TempDir(), "reassembled.bin")
|
||||||
|
if err := os.WriteFile(reassembled, eeprom.Assemble(sections), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
verify(reassembled, needle)
|
||||||
|
}
|
||||||
|
reassembleAndVerify(bootUartNeedle)
|
||||||
|
|
||||||
|
bc := sections[len(sections)-1] // guaranteed to be bootconf.txt
|
||||||
|
|
||||||
|
// Re-create the section with the existing contents.
|
||||||
|
sections[len(sections)-1] = eeprom.FileSection(bc.Offset, bc.Filename, bc.FileContent())
|
||||||
|
reassembleAndVerify(bootUartNeedle)
|
||||||
|
|
||||||
|
// Change the bootconf.txt contents.
|
||||||
|
sections[len(sections)-1] = eeprom.FileSection(bc.Offset, bc.Filename, []byte("hello world"))
|
||||||
|
reassembleAndVerify("hello world")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAgainst(t *testing.T) {
|
||||||
|
const fn = "testdata/pieeprom-2025-10-17.bin"
|
||||||
|
f, err := os.Open(fn + ".gz")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
gz, err := gzip.NewReader(f)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
bin, err := io.ReadAll(gz)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
pieeprom := filepath.Join(t.TempDir(), filepath.Base(fn))
|
||||||
|
if err := os.WriteFile(pieeprom, bin, 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Infobeamer", func(t *testing.T) {
|
||||||
|
lsBin, err := exec.LookPath("pi-eeprom-ls")
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("pi-eeprom-ls not available (%v)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify := func(eepromPath, needle string) {
|
||||||
|
ls := exec.Command(lsBin, eepromPath)
|
||||||
|
ls.Stderr = os.Stderr
|
||||||
|
lsOut, err := ls.Output()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(lsOut), needle) {
|
||||||
|
t.Errorf("infobeamer pi-eeprom-ls output did not contain %q: \n%s", needle, string(lsOut))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bootUartNeedle = "-[ bootconf.txt ]------------------\n[all]\nBOOT_UART=1"
|
||||||
|
testAgainst(t, verify, pieeprom, bootUartNeedle)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Pi", func(t *testing.T) {
|
||||||
|
configBin, err := exec.LookPath("rpi-eeprom-config")
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("rpi-eeprom-config not available (%v)", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
verify := func(eepromPath, needle string) {
|
||||||
|
extract := exec.Command(configBin, eepromPath)
|
||||||
|
extract.Dir = t.TempDir()
|
||||||
|
extract.Stderr = os.Stderr
|
||||||
|
extractOut, err := extract.Output()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(string(extractOut), needle) {
|
||||||
|
t.Errorf("rpi-eeprom-config output did not contain %q: \n%s", needle, string(extractOut))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bootUartNeedle = "[all]\nBOOT_UART=1"
|
||||||
|
testAgainst(t, verify, pieeprom, bootUartNeedle)
|
||||||
|
})
|
||||||
|
}
|
||||||
BIN
internal/eeprom/testdata/pieeprom-2025-10-17.bin.gz
vendored
Normal file
BIN
internal/eeprom/testdata/pieeprom-2025-10-17.bin.gz
vendored
Normal file
Binary file not shown.
@@ -18,12 +18,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// addCmd is gok add.
|
// addCmd is gok add.
|
||||||
var addCmd = &cobra.Command{
|
func addCmd() *cobra.Command {
|
||||||
GroupID: "edit",
|
cmd := &cobra.Command{
|
||||||
Use: "add [flags] importpath[@version]",
|
GroupID: "edit",
|
||||||
DisableFlagsInUseLine: true,
|
Use: "add [flags] importpath[@version]",
|
||||||
Short: "Add a Go package to a gokrazy instance",
|
DisableFlagsInUseLine: true,
|
||||||
Long: `Add a Go package to a gokrazy instance.
|
Short: "Add a Go package to a gokrazy instance",
|
||||||
|
Long: `Add a Go package to a gokrazy instance.
|
||||||
|
|
||||||
This command creates the required build directory, runs go get, and adds
|
This command creates the required build directory, runs go get, and adds
|
||||||
the specified package to the gokrazy instance configuration (Packages field).
|
the specified package to the gokrazy instance configuration (Packages field).
|
||||||
@@ -42,26 +43,25 @@ Examples:
|
|||||||
% gok -i scan2drive add /home/michael/projects/scanui/cmd/scanui
|
% gok -i scan2drive add /home/michael/projects/scanui/cmd/scanui
|
||||||
|
|
||||||
`,
|
`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if cmd.Flags().NArg() != 1 {
|
if cmd.Flags().NArg() != 1 {
|
||||||
fmt.Fprint(os.Stderr, `expected Go package name, name@version, or path
|
fmt.Fprint(os.Stderr, `expected Go package name, name@version, or path
|
||||||
|
|
||||||
`)
|
`)
|
||||||
return cmd.Usage()
|
return cmd.Usage()
|
||||||
}
|
}
|
||||||
|
|
||||||
return addImpl.run(cmd.Context(), args[0], cmd.OutOrStdout(), cmd.OutOrStderr())
|
return addImpl.run(cmd.Context(), args[0], cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
instanceflag.RegisterPflags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type addImplConfig struct{}
|
type addImplConfig struct{}
|
||||||
|
|
||||||
var addImpl addImplConfig
|
var addImpl addImplConfig
|
||||||
|
|
||||||
func init() {
|
|
||||||
instanceflag.RegisterPflags(addCmd.Flags())
|
|
||||||
}
|
|
||||||
|
|
||||||
type packageInfo struct {
|
type packageInfo struct {
|
||||||
// Dir is the directory on the local disk containing the package sources,
|
// Dir is the directory on the local disk containing the package sources,
|
||||||
// e.g. /home/michael/projects/stapelberg/localmod/cmd/sup.
|
// e.g. /home/michael/projects/stapelberg/localmod/cmd/sup.
|
||||||
|
|||||||
@@ -13,32 +13,32 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// editCmd is gok edit.
|
// editCmd is gok edit.
|
||||||
var editCmd = &cobra.Command{
|
func editCmd() *cobra.Command {
|
||||||
GroupID: "edit",
|
cmd := &cobra.Command{
|
||||||
Use: "edit",
|
GroupID: "edit",
|
||||||
Short: "Edit a gokrazy instance configuration interactively",
|
Use: "edit",
|
||||||
Long: `Edit a gokrazy instance configuration interactively.
|
Short: "Edit a gokrazy instance configuration interactively",
|
||||||
|
Long: `Edit a gokrazy instance configuration interactively.
|
||||||
`,
|
`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if cmd.Flags().NArg() > 0 {
|
if cmd.Flags().NArg() > 0 {
|
||||||
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
||||||
|
|
||||||
`)
|
`)
|
||||||
return cmd.Usage()
|
return cmd.Usage()
|
||||||
}
|
}
|
||||||
|
|
||||||
return editImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
return editImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
instanceflag.RegisterPflags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type editImplConfig struct{}
|
type editImplConfig struct{}
|
||||||
|
|
||||||
var editImpl editImplConfig
|
var editImpl editImplConfig
|
||||||
|
|
||||||
func init() {
|
|
||||||
instanceflag.RegisterPflags(editCmd.Flags())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *editImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
func (r *editImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||||
parentDir := instanceflag.ParentDir()
|
parentDir := instanceflag.ParentDir()
|
||||||
instance := instanceflag.Instance()
|
instance := instanceflag.Instance()
|
||||||
|
|||||||
@@ -11,17 +11,17 @@ import (
|
|||||||
|
|
||||||
"github.com/gokrazy/internal/config"
|
"github.com/gokrazy/internal/config"
|
||||||
"github.com/gokrazy/internal/instanceflag"
|
"github.com/gokrazy/internal/instanceflag"
|
||||||
"github.com/gokrazy/internal/updateflag"
|
|
||||||
"github.com/gokrazy/tools/packer"
|
"github.com/gokrazy/tools/packer"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// getCmd is gok get.
|
// getCmd is gok get.
|
||||||
var getCmd = &cobra.Command{
|
func getCmd() *cobra.Command {
|
||||||
GroupID: "edit",
|
cmd := &cobra.Command{
|
||||||
Use: "get",
|
GroupID: "edit",
|
||||||
Short: "Update the version of your Go program(s) using `go get`",
|
Use: "get",
|
||||||
Long: "gok get runs `go get` to update the version of the specified Go programs" + `
|
Short: "Update the version of your Go program(s) using `go get`",
|
||||||
|
Long: "gok get runs `go get` to update the version of the specified Go programs" + `
|
||||||
in your gokrazy instance.
|
in your gokrazy instance.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
@@ -34,9 +34,13 @@ Examples:
|
|||||||
# Update only gokrazy system packages
|
# Update only gokrazy system packages
|
||||||
% gok -i scanner get gokrazy
|
% gok -i scanner get gokrazy
|
||||||
`,
|
`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return getImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
return getImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().BoolVarP(&getImpl.updateAll, "update_all", "u", false, "update all installed packages and gokrazy system packages")
|
||||||
|
instanceflag.RegisterPflags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type getImplConfig struct {
|
type getImplConfig struct {
|
||||||
@@ -45,11 +49,6 @@ type getImplConfig struct {
|
|||||||
|
|
||||||
var getImpl getImplConfig
|
var getImpl getImplConfig
|
||||||
|
|
||||||
func init() {
|
|
||||||
getCmd.Flags().BoolVarP(&getImpl.updateAll, "update_all", "u", false, "update all installed packages and gokrazy system packages")
|
|
||||||
instanceflag.RegisterPflags(getCmd.Flags())
|
|
||||||
}
|
|
||||||
|
|
||||||
func getGokrazySystemPackages(cfg *config.Struct) []string {
|
func getGokrazySystemPackages(cfg *config.Struct) []string {
|
||||||
pkgs := append([]string{}, cfg.GokrazyPackagesOrDefault()...)
|
pkgs := append([]string{}, cfg.GokrazyPackagesOrDefault()...)
|
||||||
pkgs = append(pkgs, packer.InitDeps(cfg.InternalCompatibilityFlags.InitPkg)...)
|
pkgs = append(pkgs, packer.InitDeps(cfg.InternalCompatibilityFlags.InitPkg)...)
|
||||||
@@ -78,8 +77,6 @@ func (r *getImplConfig) run(ctx context.Context, args []string, stdout, stderr i
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
updateflag.SetUpdate("yes")
|
|
||||||
|
|
||||||
packages := args
|
packages := args
|
||||||
if r.updateAll {
|
if r.updateAll {
|
||||||
if len(packages) > 0 {
|
if len(packages) > 0 {
|
||||||
|
|||||||
@@ -20,15 +20,20 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// logsCmd is gok logs.
|
// logsCmd is gok logs.
|
||||||
var logsCmd = &cobra.Command{
|
func logsCmd() *cobra.Command {
|
||||||
GroupID: "runtime",
|
cmd := &cobra.Command{
|
||||||
Use: "logs",
|
GroupID: "runtime",
|
||||||
Short: "Stream logs from a running gokrazy service",
|
Use: "logs",
|
||||||
Long: `Display the most recent 100 log lines from stdout and stderr each,
|
Short: "Stream logs from a running gokrazy service",
|
||||||
|
Long: `Display the most recent 100 log lines from stdout and stderr each,
|
||||||
and any new lines the gokrazy service produces (cancel any time with Ctrl-C)`,
|
and any new lines the gokrazy service produces (cancel any time with Ctrl-C)`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return logsImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
return logsImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(&logsImpl.service, "service", "s", "", "gokrazy service to fetch logs for")
|
||||||
|
instanceflag.RegisterPflags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type logsImplConfig struct {
|
type logsImplConfig struct {
|
||||||
@@ -37,11 +42,6 @@ type logsImplConfig struct {
|
|||||||
|
|
||||||
var logsImpl logsImplConfig
|
var logsImpl logsImplConfig
|
||||||
|
|
||||||
func init() {
|
|
||||||
logsCmd.Flags().StringVarP(&logsImpl.service, "service", "s", "", "gokrazy service to fetch logs for")
|
|
||||||
instanceflag.RegisterPflags(logsCmd.Flags())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (l *logsImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
func (l *logsImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||||
cfg, err := config.ApplyInstanceFlag()
|
cfg, err := config.ApplyInstanceFlag()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -53,13 +53,11 @@ func (l *logsImplConfig) run(ctx context.Context, args []string, stdout, stderr
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateflag.SetUpdate("yes")
|
|
||||||
|
|
||||||
if l.service == "" {
|
if l.service == "" {
|
||||||
return fmt.Errorf("the -service flag is empty, but required")
|
return fmt.Errorf("the -service flag is empty, but required")
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient, _, logsUrl, err := httpclient.For(cfg)
|
httpClient, _, logsUrl, err := httpclient.For(updateflag.Value{Update: "yes"}, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,25 +16,30 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// newCmd is gok new.
|
// newCmd is gok new.
|
||||||
var newCmd = &cobra.Command{
|
func newCmd() *cobra.Command {
|
||||||
GroupID: "edit",
|
cmd := &cobra.Command{
|
||||||
Use: "new",
|
GroupID: "edit",
|
||||||
Short: "Create a new gokrazy instance",
|
Use: "new",
|
||||||
Long: `Create a new gokrazy instance.
|
Short: "Create a new gokrazy instance",
|
||||||
|
Long: `Create a new gokrazy instance.
|
||||||
|
|
||||||
If you are unfamiliar with gokrazy, please follow:
|
If you are unfamiliar with gokrazy, please follow:
|
||||||
https://gokrazy.org/quickstart/
|
https://gokrazy.org/quickstart/
|
||||||
`,
|
`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if cmd.Flags().NArg() > 0 {
|
if cmd.Flags().NArg() > 0 {
|
||||||
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
||||||
|
|
||||||
`)
|
`)
|
||||||
return cmd.Usage()
|
return cmd.Usage()
|
||||||
}
|
}
|
||||||
|
|
||||||
return newImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
return newImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
instanceflag.RegisterPflags(cmd.Flags())
|
||||||
|
cmd.Flags().BoolVarP(&newImpl.empty, "empty", "", false, "create an empty gokrazy instance, without the default packages")
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type newImplConfig struct {
|
type newImplConfig struct {
|
||||||
@@ -43,11 +48,6 @@ type newImplConfig struct {
|
|||||||
|
|
||||||
var newImpl newImplConfig
|
var newImpl newImplConfig
|
||||||
|
|
||||||
func init() {
|
|
||||||
instanceflag.RegisterPflags(newCmd.Flags())
|
|
||||||
newCmd.Flags().BoolVarP(&newImpl.empty, "empty", "", false, "create an empty gokrazy instance, without the default packages")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *newImplConfig) createBreakglassAuthorizedKeys(authorizedPath string, matches []string) error {
|
func (r *newImplConfig) createBreakglassAuthorizedKeys(authorizedPath string, matches []string) error {
|
||||||
f, err := os.OpenFile(authorizedPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
f, err := os.OpenFile(authorizedPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// overwriteCmd is gok overwrite.
|
// overwriteCmd is gok overwrite.
|
||||||
var overwriteCmd = &cobra.Command{
|
func overwriteCmd() *cobra.Command {
|
||||||
GroupID: "deploy",
|
cmd := &cobra.Command{
|
||||||
Use: "overwrite",
|
GroupID: "deploy",
|
||||||
Short: "Build and deploy a gokrazy instance to a storage device",
|
Use: "overwrite",
|
||||||
Long: `Build and deploy a gokrazy instance to a storage device.
|
Short: "Build and deploy a gokrazy instance to a storage device",
|
||||||
|
Long: `Build and deploy a gokrazy instance to a storage device.
|
||||||
|
|
||||||
You typically need to use the gok overwrite command only once,
|
You typically need to use the gok overwrite command only once,
|
||||||
when first deploying your gokrazy instance. Afterwards, you can
|
when first deploying your gokrazy instance. Afterwards, you can
|
||||||
@@ -29,16 +30,27 @@ Examples:
|
|||||||
# Overwrite the contents of the SD card sdx with gokrazy instance scan2drive:
|
# Overwrite the contents of the SD card sdx with gokrazy instance scan2drive:
|
||||||
% gok -i scan2drive overwrite --full=/dev/sdx
|
% gok -i scan2drive overwrite --full=/dev/sdx
|
||||||
`,
|
`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if cmd.Flags().NArg() > 0 {
|
if cmd.Flags().NArg() > 0 {
|
||||||
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
||||||
|
|
||||||
`)
|
`)
|
||||||
return cmd.Usage()
|
return cmd.Usage()
|
||||||
}
|
}
|
||||||
|
|
||||||
return overwriteImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
return overwriteImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
instanceflag.RegisterPflags(cmd.Flags())
|
||||||
|
cmd.Flags().StringVarP(&overwriteImpl.full, "full", "", "", "write a full gokrazy device image to the specified device (e.g. /dev/sdx) or path (e.g. /tmp/gokrazy.img)")
|
||||||
|
cmd.Flags().StringVarP(&overwriteImpl.gaf, "gaf", "", "", "write a .gaf (gokrazy archive format) file to the specified path (e.g. /tmp/gokrazy.gaf)")
|
||||||
|
cmd.Flags().StringVarP(&overwriteImpl.boot, "boot", "", "", "write the gokrazy boot file system to the specified partition (e.g. /dev/sdx1) or path (e.g. /tmp/boot.fat)")
|
||||||
|
cmd.Flags().StringVarP(&overwriteImpl.root, "root", "", "", "write the gokrazy root file system to the specified partition (e.g. /dev/sdx2) or path (e.g. /tmp/root.squashfs)")
|
||||||
|
cmd.Flags().StringVarP(&overwriteImpl.mbr, "mbr", "", "", "write the gokrazy master boot record (MBR) to the specified device (e.g. /dev/sdx) or path (e.g. /tmp/mbr.img). only effective if -boot is specified, too")
|
||||||
|
cmd.Flags().StringVarP(&overwriteImpl.sudo, "sudo", "", "", "Whether to elevate privileges using sudo when required (one of auto, always, never, default auto)")
|
||||||
|
cmd.Flags().IntVarP(&overwriteImpl.targetStorageBytes, "target_storage_bytes", "", 0, "Number of bytes which the target storage device (SD card) has. Required for using -full=<file>")
|
||||||
|
cmd.Flags().StringVarP(&overwriteImpl.traceFile, "trace_file", "", "", "If non-empty, write a Go runtime/trace to this file (for performance analysis)")
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type overwriteImplConfig struct {
|
type overwriteImplConfig struct {
|
||||||
@@ -56,18 +68,6 @@ type overwriteImplConfig struct {
|
|||||||
|
|
||||||
var overwriteImpl overwriteImplConfig
|
var overwriteImpl overwriteImplConfig
|
||||||
|
|
||||||
func init() {
|
|
||||||
instanceflag.RegisterPflags(overwriteCmd.Flags())
|
|
||||||
overwriteCmd.Flags().StringVarP(&overwriteImpl.full, "full", "", "", "write a full gokrazy device image to the specified device (e.g. /dev/sdx) or path (e.g. /tmp/gokrazy.img)")
|
|
||||||
overwriteCmd.Flags().StringVarP(&overwriteImpl.gaf, "gaf", "", "", "write a .gaf (gokrazy archive format) file to the specified path (e.g. /tmp/gokrazy.gaf)")
|
|
||||||
overwriteCmd.Flags().StringVarP(&overwriteImpl.boot, "boot", "", "", "write the gokrazy boot file system to the specified partition (e.g. /dev/sdx1) or path (e.g. /tmp/boot.fat)")
|
|
||||||
overwriteCmd.Flags().StringVarP(&overwriteImpl.root, "root", "", "", "write the gokrazy root file system to the specified partition (e.g. /dev/sdx2) or path (e.g. /tmp/root.squashfs)")
|
|
||||||
overwriteCmd.Flags().StringVarP(&overwriteImpl.mbr, "mbr", "", "", "write the gokrazy master boot record (MBR) to the specified device (e.g. /dev/sdx) or path (e.g. /tmp/mbr.img). only effective if -boot is specified, too")
|
|
||||||
overwriteCmd.Flags().StringVarP(&overwriteImpl.sudo, "sudo", "", "", "Whether to elevate privileges using sudo when required (one of auto, always, never, default auto)")
|
|
||||||
overwriteCmd.Flags().IntVarP(&overwriteImpl.targetStorageBytes, "target_storage_bytes", "", 0, "Number of bytes which the target storage device (SD card) has. Required for using -full=<file>")
|
|
||||||
overwriteCmd.Flags().StringVarP(&overwriteImpl.traceFile, "trace_file", "", "", "If non-empty, write a Go runtime/trace to this file (for performance analysis)")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *overwriteImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
func (r *overwriteImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||||
if r.traceFile != "" {
|
if r.traceFile != "" {
|
||||||
out, err := os.Create(r.traceFile)
|
out, err := os.Create(r.traceFile)
|
||||||
@@ -151,7 +151,7 @@ func (r *overwriteImplConfig) run(ctx context.Context, args []string, stdout, st
|
|||||||
Output: &output,
|
Output: &output,
|
||||||
}
|
}
|
||||||
|
|
||||||
pack.Main(ctx, "gokrazy gok")
|
pack.Main(ctx)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,18 +14,22 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// psCmd is gok ps.
|
// psCmd is gok ps.
|
||||||
var psCmd = &cobra.Command{
|
func psCmd() *cobra.Command {
|
||||||
GroupID: "runtime",
|
cmd := &cobra.Command{
|
||||||
Use: "ps",
|
GroupID: "runtime",
|
||||||
Short: "list processes of a running gokrazy instance",
|
Use: "ps",
|
||||||
Long: `gok ps
|
Short: "list processes of a running gokrazy instance",
|
||||||
|
Long: `gok ps
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
% gok -i scan2drive ps
|
% gok -i scan2drive ps
|
||||||
`,
|
`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return psImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
return psImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
instanceflag.RegisterPflags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type psImplConfig struct {
|
type psImplConfig struct {
|
||||||
@@ -33,10 +37,6 @@ type psImplConfig struct {
|
|||||||
|
|
||||||
var psImpl psImplConfig
|
var psImpl psImplConfig
|
||||||
|
|
||||||
func init() {
|
|
||||||
instanceflag.RegisterPflags(psCmd.Flags())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *psImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
func (r *psImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||||
cfg, err := config.ApplyInstanceFlag()
|
cfg, err := config.ApplyInstanceFlag()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// pushCmd is gok push.
|
// pushCmd is gok push.
|
||||||
var pushCmd = &cobra.Command{
|
func pushCmd() *cobra.Command {
|
||||||
GroupID: "server",
|
cmd := &cobra.Command{
|
||||||
Use: "push",
|
GroupID: "server",
|
||||||
Short: "Push a gokrazy image to a remote GUS server",
|
Use: "push",
|
||||||
Long: `gok push pushes a local gaf (gokrazy archive format) file to a remote server.
|
Short: "Push a gokrazy image to a remote GUS server",
|
||||||
|
Long: `gok push pushes a local gaf (gokrazy archive format) file to a remote server.
|
||||||
|
|
||||||
When the --json flag is specified, the server response is printed to stdout.
|
When the --json flag is specified, the server response is printed to stdout.
|
||||||
|
|
||||||
@@ -28,9 +29,15 @@ Examples:
|
|||||||
% gok push --gaf /tmp/gokrazy.gaf --server https://gus.gokrazy.org
|
% gok push --gaf /tmp/gokrazy.gaf --server https://gus.gokrazy.org
|
||||||
|
|
||||||
`,
|
`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return pushImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
return pushImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(&pushImpl.gafPath, "gaf", "", "", "path to the .gaf (gokrazy archive format) file to push to GUS (e.g. /tmp/gokrazy.gaf); build using gok overwrite --gaf")
|
||||||
|
cmd.Flags().StringVarP(&pushImpl.server, "server", "", "", "HTTP(S) URL to the server to push to")
|
||||||
|
cmd.Flags().BoolVarP(&pushImpl.json, "json", "", false, "print server JSON response directly to stdout")
|
||||||
|
instanceflag.RegisterPflags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type pushConfig struct {
|
type pushConfig struct {
|
||||||
@@ -41,13 +48,6 @@ type pushConfig struct {
|
|||||||
|
|
||||||
var pushImpl pushConfig
|
var pushImpl pushConfig
|
||||||
|
|
||||||
func init() {
|
|
||||||
pushCmd.Flags().StringVarP(&pushImpl.gafPath, "gaf", "", "", "path to the .gaf (gokrazy archive format) file to push to GUS (e.g. /tmp/gokrazy.gaf); build using gok overwrite --gaf")
|
|
||||||
pushCmd.Flags().StringVarP(&pushImpl.server, "server", "", "", "HTTP(S) URL to the server to push to")
|
|
||||||
pushCmd.Flags().BoolVarP(&pushImpl.json, "json", "", false, "print server JSON response directly to stdout")
|
|
||||||
instanceflag.RegisterPflags(pushCmd.Flags())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *pushConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
func (r *pushConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||||
// TODO: use an io.Reader that allows us to indicate progress
|
// TODO: use an io.Reader that allows us to indicate progress
|
||||||
body, err := os.Open(r.gafPath)
|
body, err := os.Open(r.gafPath)
|
||||||
|
|||||||
@@ -9,10 +9,11 @@ import (
|
|||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
)
|
)
|
||||||
|
|
||||||
var RootCmd = &cobra.Command{
|
func RootCmd() *cobra.Command {
|
||||||
Use: "gok",
|
rootCmd := &cobra.Command{
|
||||||
Short: "top-level CLI entry point for all things gokrazy",
|
Use: "gok",
|
||||||
Long: `The gok tool is your main entrypoint to gokrazy and allows you to:
|
Short: "top-level CLI entry point for all things gokrazy",
|
||||||
|
Long: `The gok tool is your main entrypoint to gokrazy and allows you to:
|
||||||
|
|
||||||
1. Create new gokrazy instances (gok new),
|
1. Create new gokrazy instances (gok new),
|
||||||
2. Deploy gokrazy instances to storage devices like SD cards (gok overwrite),
|
2. Deploy gokrazy instances to storage devices like SD cards (gok overwrite),
|
||||||
@@ -22,59 +23,58 @@ var RootCmd = &cobra.Command{
|
|||||||
If you are unfamiliar with gokrazy, please follow:
|
If you are unfamiliar with gokrazy, please follow:
|
||||||
https://gokrazy.org/quickstart/
|
https://gokrazy.org/quickstart/
|
||||||
`,
|
`,
|
||||||
SilenceErrors: true,
|
SilenceErrors: true,
|
||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
versionVal, err := cmd.Flags().GetBool("version")
|
versionVal, err := cmd.Flags().GetBool("version")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("BUG: version flag declared as non-bool")
|
return fmt.Errorf("BUG: version flag declared as non-bool")
|
||||||
}
|
}
|
||||||
if versionVal {
|
if versionVal {
|
||||||
fmt.Println(version.Read())
|
fmt.Println(version.Read())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return pflag.ErrHelp
|
return pflag.ErrHelp
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
rootCmd.AddGroup(&cobra.Group{
|
||||||
func init() {
|
|
||||||
RootCmd.AddGroup(&cobra.Group{
|
|
||||||
ID: "edit",
|
ID: "edit",
|
||||||
Title: "Commands to create and edit a gokrazy instance:",
|
Title: "Commands to create and edit a gokrazy instance:",
|
||||||
})
|
})
|
||||||
RootCmd.AddGroup(&cobra.Group{
|
rootCmd.AddGroup(&cobra.Group{
|
||||||
ID: "deploy",
|
ID: "deploy",
|
||||||
Title: "Commands to deploy and update a gokrazy instance:",
|
Title: "Commands to deploy and update a gokrazy instance:",
|
||||||
})
|
})
|
||||||
RootCmd.AddGroup(&cobra.Group{
|
rootCmd.AddGroup(&cobra.Group{
|
||||||
ID: "runtime",
|
ID: "runtime",
|
||||||
Title: "Commands to work with a running gokrazy instance:",
|
Title: "Commands to work with a running gokrazy instance:",
|
||||||
})
|
})
|
||||||
RootCmd.AddGroup(&cobra.Group{
|
rootCmd.AddGroup(&cobra.Group{
|
||||||
ID: "server",
|
ID: "server",
|
||||||
Title: "Commands to work with a remote GUS server:",
|
Title: "Commands to work with a remote GUS server:",
|
||||||
})
|
})
|
||||||
RootCmd.AddGroup(&cobra.Group{
|
rootCmd.AddGroup(&cobra.Group{
|
||||||
ID: "vm",
|
ID: "vm",
|
||||||
Title: "Commands to work with Virtual Machines (VMs):",
|
Title: "Commands to work with Virtual Machines (VMs):",
|
||||||
})
|
})
|
||||||
RootCmd.Flags().Bool("version", false, "print gok version")
|
rootCmd.Flags().Bool("version", false, "print gok version")
|
||||||
// Only defined so that it appears in documentation like --help.
|
// Only defined so that it appears in documentation like --help.
|
||||||
//
|
//
|
||||||
// Cobra only parses local flags on the target command, but they can appear
|
// Cobra only parses local flags on the target command, but they can appear
|
||||||
// at any place in the command line (before or after the verb).
|
// at any place in the command line (before or after the verb).
|
||||||
instanceflag.RegisterPflags(RootCmd.Flags())
|
instanceflag.RegisterPflags(rootCmd.Flags())
|
||||||
RootCmd.AddCommand(runCmd)
|
rootCmd.AddCommand(runCmd())
|
||||||
RootCmd.AddCommand(logsCmd)
|
rootCmd.AddCommand(logsCmd())
|
||||||
RootCmd.AddCommand(updateCmd)
|
rootCmd.AddCommand(updateCmd())
|
||||||
RootCmd.AddCommand(overwriteCmd)
|
rootCmd.AddCommand(overwriteCmd())
|
||||||
RootCmd.AddCommand(versionCmd)
|
rootCmd.AddCommand(versionCmd())
|
||||||
RootCmd.AddCommand(newCmd)
|
rootCmd.AddCommand(newCmd())
|
||||||
RootCmd.AddCommand(editCmd)
|
rootCmd.AddCommand(editCmd())
|
||||||
RootCmd.AddCommand(addCmd)
|
rootCmd.AddCommand(addCmd())
|
||||||
RootCmd.AddCommand(getCmd)
|
rootCmd.AddCommand(getCmd())
|
||||||
RootCmd.AddCommand(sbomCmd)
|
rootCmd.AddCommand(sbomCmd())
|
||||||
RootCmd.AddCommand(pushCmd)
|
rootCmd.AddCommand(pushCmd())
|
||||||
RootCmd.AddCommand(vmCmd)
|
rootCmd.AddCommand(vmCmd())
|
||||||
RootCmd.AddCommand(psCmd)
|
rootCmd.AddCommand(psCmd())
|
||||||
|
return rootCmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// runCmd is gok run.
|
// runCmd is gok run.
|
||||||
var runCmd = &cobra.Command{
|
func runCmd() *cobra.Command {
|
||||||
GroupID: "runtime",
|
cmd := &cobra.Command{
|
||||||
Use: "run",
|
GroupID: "runtime",
|
||||||
Short: "`go install` and run on a running gokrazy instance",
|
Use: "run",
|
||||||
Long: "gok run uses `go install` to build the Go program in the current directory," + `
|
Short: "`go install` and run on a running gokrazy instance",
|
||||||
|
Long: "gok run uses `go install` to build the Go program in the current directory," + `
|
||||||
then it stores the program in RAM of a running gokrazy instance and runs the program.
|
then it stores the program in RAM of a running gokrazy instance and runs the program.
|
||||||
|
|
||||||
This enables a quick feedback loop when working on a program that is running on gokrazy,
|
This enables a quick feedback loop when working on a program that is running on gokrazy,
|
||||||
@@ -42,9 +43,13 @@ Examples:
|
|||||||
# specify extra flags on the command line
|
# specify extra flags on the command line
|
||||||
% gok -i scan2drive run -- -tls_autocert_hosts=scan.example.com
|
% gok -i scan2drive run -- -tls_autocert_hosts=scan.example.com
|
||||||
`,
|
`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return runImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
return runImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().BoolVarP(&runImpl.keep, "keep", "k", false, "keep temporary binary")
|
||||||
|
instanceflag.RegisterPflags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type runImplConfig struct {
|
type runImplConfig struct {
|
||||||
@@ -53,11 +58,6 @@ type runImplConfig struct {
|
|||||||
|
|
||||||
var runImpl runImplConfig
|
var runImpl runImplConfig
|
||||||
|
|
||||||
func init() {
|
|
||||||
runCmd.Flags().BoolVarP(&runImpl.keep, "keep", "k", false, "keep temporary binary")
|
|
||||||
instanceflag.RegisterPflags(runCmd.Flags())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *runImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
func (r *runImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||||
cfg, err := config.ApplyInstanceFlag()
|
cfg, err := config.ApplyInstanceFlag()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -69,8 +69,6 @@ func (r *runImplConfig) run(ctx context.Context, args []string, stdout, stderr i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateflag.SetUpdate("yes")
|
|
||||||
|
|
||||||
var tmp string
|
var tmp string
|
||||||
if r.keep {
|
if r.keep {
|
||||||
tmp = os.TempDir()
|
tmp = os.TempDir()
|
||||||
@@ -122,7 +120,7 @@ func (r *runImplConfig) run(ctx context.Context, args []string, stdout, stderr i
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
httpClient, _, updateBaseUrl, err := httpclient.For(cfg)
|
httpClient, _, updateBaseUrl, err := httpclient.For(updateflag.Value{Update: "yes"}, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,17 +10,17 @@ import (
|
|||||||
|
|
||||||
"github.com/gokrazy/internal/config"
|
"github.com/gokrazy/internal/config"
|
||||||
"github.com/gokrazy/internal/instanceflag"
|
"github.com/gokrazy/internal/instanceflag"
|
||||||
"github.com/gokrazy/internal/updateflag"
|
|
||||||
"github.com/gokrazy/tools/internal/packer"
|
"github.com/gokrazy/tools/internal/packer"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// sbomCmd is gok sbom.
|
// sbomCmd is gok sbom.
|
||||||
var sbomCmd = &cobra.Command{
|
func sbomCmd() *cobra.Command {
|
||||||
GroupID: "deploy",
|
cmd := &cobra.Command{
|
||||||
Use: "sbom",
|
GroupID: "deploy",
|
||||||
Short: "Print the Software Bill Of Materials of a gokrazy instance",
|
Use: "sbom",
|
||||||
Long: `gok sbom generates an SBOM of what gok overwrite or gok update would build
|
Short: "Print the Software Bill Of Materials of a gokrazy instance",
|
||||||
|
Long: `gok sbom generates an SBOM of what gok overwrite or gok update would build
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
# print the hash and SBOM contents in JSON format
|
# print the hash and SBOM contents in JSON format
|
||||||
@@ -30,9 +30,13 @@ Examples:
|
|||||||
% gok -i scanner sbom --format hash
|
% gok -i scanner sbom --format hash
|
||||||
|
|
||||||
`,
|
`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return sbomImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
return sbomImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
cmd.Flags().StringVarP(&sbomImpl.format, "format", "", "json", "output format. one of json or hash")
|
||||||
|
instanceflag.RegisterPflags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type sbomConfig struct {
|
type sbomConfig struct {
|
||||||
@@ -41,11 +45,6 @@ type sbomConfig struct {
|
|||||||
|
|
||||||
var sbomImpl sbomConfig
|
var sbomImpl sbomConfig
|
||||||
|
|
||||||
func init() {
|
|
||||||
sbomCmd.Flags().StringVarP(&sbomImpl.format, "format", "", "json", "output format. one of json or hash")
|
|
||||||
instanceflag.RegisterPflags(sbomCmd.Flags())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *sbomConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
func (r *sbomConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||||
fileCfg, err := config.ApplyInstanceFlag()
|
fileCfg, err := config.ApplyInstanceFlag()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -66,8 +65,6 @@ func (r *sbomConfig) run(ctx context.Context, args []string, stdout, stderr io.W
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
updateflag.SetUpdate("yes")
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
pack := &packer.Pack{
|
pack := &packer.Pack{
|
||||||
// Send all build output to stderr so that stdout
|
// Send all build output to stderr so that stdout
|
||||||
|
|||||||
@@ -13,22 +13,28 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// updateCmd is gok update.
|
// updateCmd is gok update.
|
||||||
var updateCmd = &cobra.Command{
|
func updateCmd() *cobra.Command {
|
||||||
GroupID: "deploy",
|
cmd := &cobra.Command{
|
||||||
Use: "update",
|
GroupID: "deploy",
|
||||||
Short: "Build a gokrazy instance and update over the network",
|
Use: "update",
|
||||||
Long: `Build a gokrazy instance and update over the network.
|
Short: "Build a gokrazy instance and update over the network",
|
||||||
|
Long: `Build a gokrazy instance and update over the network.
|
||||||
`,
|
`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if cmd.Flags().NArg() > 0 {
|
if cmd.Flags().NArg() > 0 {
|
||||||
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
fmt.Fprint(os.Stderr, `positional arguments are not supported
|
||||||
|
|
||||||
`)
|
`)
|
||||||
return cmd.Usage()
|
return cmd.Usage()
|
||||||
}
|
}
|
||||||
|
|
||||||
return updateImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
return updateImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
instanceflag.RegisterPflags(cmd.Flags())
|
||||||
|
cmd.Flags().BoolVarP(&updateImpl.insecure, "insecure", "", false, "Disable TLS stripping detection. Should only be used when first enabling TLS, not permanently.")
|
||||||
|
cmd.Flags().BoolVarP(&updateImpl.testboot, "testboot", "", false, "Trigger a testboot instead of switching to the new root partition directly")
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type updateImplConfig struct {
|
type updateImplConfig struct {
|
||||||
@@ -38,12 +44,6 @@ type updateImplConfig struct {
|
|||||||
|
|
||||||
var updateImpl updateImplConfig
|
var updateImpl updateImplConfig
|
||||||
|
|
||||||
func init() {
|
|
||||||
instanceflag.RegisterPflags(updateCmd.Flags())
|
|
||||||
updateCmd.Flags().BoolVarP(&updateImpl.insecure, "insecure", "", false, "Disable TLS stripping detection. Should only be used when first enabling TLS, not permanently.")
|
|
||||||
updateCmd.Flags().BoolVarP(&updateImpl.testboot, "testboot", "", false, "Trigger a testboot instead of switching to the new root partition directly")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *updateImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
func (r *updateImplConfig) run(ctx context.Context, args []string, stdout, stderr io.Writer) error {
|
||||||
fileCfg, err := config.ApplyInstanceFlag()
|
fileCfg, err := config.ApplyInstanceFlag()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -90,7 +90,7 @@ func (r *updateImplConfig) run(ctx context.Context, args []string, stdout, stder
|
|||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
}
|
}
|
||||||
|
|
||||||
pack.Main(ctx, "gokrazy gok")
|
pack.Main(ctx)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,13 +10,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// versionCmd is gok version.
|
// versionCmd is gok version.
|
||||||
var versionCmd = &cobra.Command{
|
func versionCmd() *cobra.Command {
|
||||||
Use: "version",
|
cmd := &cobra.Command{
|
||||||
Short: "Print gok version",
|
Use: "version",
|
||||||
Long: `Print gok version`,
|
Short: "Print gok version",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
Long: `Print gok version`,
|
||||||
return versionImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
},
|
return versionImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type versionImplConfig struct{}
|
type versionImplConfig struct{}
|
||||||
|
|||||||
@@ -5,11 +5,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// vmCmd is the gok vm subcommand, which (only) has nested commands like run.
|
// vmCmd is the gok vm subcommand, which (only) has nested commands like run.
|
||||||
var vmCmd = &cobra.Command{
|
func vmCmd() *cobra.Command {
|
||||||
GroupID: "vm",
|
cmd := &cobra.Command{
|
||||||
Use: "vm",
|
GroupID: "vm",
|
||||||
Short: "virtual machine",
|
Use: "vm",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
Short: "virtual machine",
|
||||||
return cmd.Usage()
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
},
|
return cmd.Usage()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cmd.AddCommand(vmRunCmd())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,10 +18,11 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
var vmRunCmd = &cobra.Command{
|
func vmRunCmd() *cobra.Command {
|
||||||
Use: "run",
|
cmd := &cobra.Command{
|
||||||
Short: "run a virtual machine (using QEMU)",
|
Use: "run",
|
||||||
Long: `gok run builds a gokrazy instance and runs it using QEMU.
|
Short: "run a virtual machine (using QEMU)",
|
||||||
|
Long: `gok run builds a gokrazy instance and runs it using QEMU.
|
||||||
|
|
||||||
Extra arguments are passed to QEMU as-is.
|
Extra arguments are passed to QEMU as-is.
|
||||||
|
|
||||||
@@ -35,13 +36,20 @@ Examples:
|
|||||||
# Directly specify QEMU USB flags
|
# Directly specify QEMU USB flags
|
||||||
% gok vm run -- -usb -device usb-mouse
|
% gok vm run -- -usb -device usb-mouse
|
||||||
`,
|
`,
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
return vmRunImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
return vmRunImpl.run(cmd.Context(), args, cmd.OutOrStdout(), cmd.OutOrStderr())
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
cmd.Flags().StringVarP(&vmRunImpl.sudo, "sudo", "", "", "Whether to elevate privileges using sudo when required (one of auto, always, never, default auto)")
|
||||||
func init() {
|
const permSize = 512 * 1024 * 1024
|
||||||
vmCmd.AddCommand(vmRunCmd)
|
cmd.Flags().IntVarP(&vmRunImpl.targetStorageBytes, "target_storage_bytes", "", 1258299392+permSize, "Size of the disk image in bytes")
|
||||||
|
cmd.Flags().StringVarP(&vmRunImpl.arch, "arch", "", "", "architecture for which to build and run QEMU. One of 'amd64' or 'arm64'")
|
||||||
|
cmd.Flags().StringVarP(&vmRunImpl.netdev, "netdev", "", "user,id=net0,hostfwd=tcp::8080-:80,hostfwd=tcp::8022-:22", "QEMU -netdev argument")
|
||||||
|
cmd.Flags().BoolVarP(&vmRunImpl.keep, "keep", "", false, "keep ephemeral disk images around instead of deleting them when QEMU exits")
|
||||||
|
cmd.Flags().BoolVarP(&vmRunImpl.dry, "dryrun", "", false, "Whether to actually run QEMU or merely print the command")
|
||||||
|
cmd.Flags().BoolVarP(&vmRunImpl.graphic, "graphic", "", true, "Run QEMU in graphical mode?")
|
||||||
|
instanceflag.RegisterPflags(cmd.Flags())
|
||||||
|
return cmd
|
||||||
}
|
}
|
||||||
|
|
||||||
type vmRunConfig struct {
|
type vmRunConfig struct {
|
||||||
@@ -64,18 +72,6 @@ func (r *vmRunConfig) effectiveGoarch() string {
|
|||||||
|
|
||||||
var vmRunImpl vmRunConfig
|
var vmRunImpl vmRunConfig
|
||||||
|
|
||||||
func init() {
|
|
||||||
vmRunCmd.Flags().StringVarP(&vmRunImpl.sudo, "sudo", "", "", "Whether to elevate privileges using sudo when required (one of auto, always, never, default auto)")
|
|
||||||
const permSize = 512 * 1024 * 1024
|
|
||||||
vmRunCmd.Flags().IntVarP(&vmRunImpl.targetStorageBytes, "target_storage_bytes", "", 1258299392+permSize, "Size of the disk image in bytes")
|
|
||||||
vmRunCmd.Flags().StringVarP(&vmRunImpl.arch, "arch", "", "", "architecture for which to build and run QEMU. One of 'amd64' or 'arm64'")
|
|
||||||
vmRunCmd.Flags().StringVarP(&vmRunImpl.netdev, "netdev", "", "user,id=net0,hostfwd=tcp::8080-:80,hostfwd=tcp::8022-:22", "QEMU -netdev argument")
|
|
||||||
vmRunCmd.Flags().BoolVarP(&vmRunImpl.keep, "keep", "", false, "keep ephemeral disk images around instead of deleting them when QEMU exits")
|
|
||||||
vmRunCmd.Flags().BoolVarP(&vmRunImpl.dry, "dryrun", "", false, "Whether to actually run QEMU or merely print the command")
|
|
||||||
vmRunCmd.Flags().BoolVarP(&vmRunImpl.graphic, "graphic", "", true, "Run QEMU in graphical mode?")
|
|
||||||
instanceflag.RegisterPflags(vmRunCmd.Flags())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *vmRunConfig) buildFullDiskImage(ctx context.Context, dest string, fileCfg *config.Struct) error {
|
func (r *vmRunConfig) buildFullDiskImage(ctx context.Context, dest string, fileCfg *config.Struct) error {
|
||||||
|
|
||||||
if r.arch != "" {
|
if r.arch != "" {
|
||||||
@@ -133,7 +129,7 @@ func (r *vmRunConfig) buildFullDiskImage(ctx context.Context, dest string, fileC
|
|||||||
Output: &output,
|
Output: &output,
|
||||||
}
|
}
|
||||||
|
|
||||||
pack.Main(ctx, "gokrazy gok")
|
pack.Main(ctx)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func generateAndStoreSelfSignedCertificate(cfg *config.Struct, hostConfigPath, c
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getCertificate(cfg *config.Struct) (string, string, error) {
|
func getCertificate(cfg *config.Struct) (string, string, error) {
|
||||||
certPath, keyPath, err := tlsflag.CertificatePathsFor(cfg.Hostname)
|
certPath, keyPath, err := tlsflag.CertificatePathsFor(cfg.Update.UseTLS, cfg.Hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var nycerr *tlsflag.ErrNotYetCreated
|
var nycerr *tlsflag.ErrNotYetCreated
|
||||||
if errors.As(err, &nycerr) {
|
if errors.As(err, &nycerr) {
|
||||||
|
|||||||
56
internal/packer/eeprom.go
Normal file
56
internal/packer/eeprom.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package packer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gokrazy/tools/internal/eeprom"
|
||||||
|
)
|
||||||
|
|
||||||
|
func applyExtraEEPROM(pieeprom string, extraEEPROM []string) ([]byte, error) {
|
||||||
|
upstream, err := os.Open(pieeprom)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
upstreamBin, err := io.ReadAll(upstream)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sections, err := eeprom.Analyze(upstreamBin)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
bc := sections[len(sections)-1] // guaranteed to be bootconf.txt
|
||||||
|
bootconfTxt := bc.FileContent()
|
||||||
|
applied := overwriteBootconf(bootconfTxt, extraEEPROM)
|
||||||
|
sections[len(sections)-1] = eeprom.FileSection(bc.Offset, bc.Filename, applied)
|
||||||
|
return eeprom.Assemble(sections), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func overwriteBootconf(bootconfTxt []byte, extraEEPROM []string) []byte {
|
||||||
|
bootconfLines := strings.Split(strings.TrimSpace(string(bootconfTxt)), "\n")
|
||||||
|
// For each property, there will be an entry from property to full line
|
||||||
|
extraByProp := make(map[string]string)
|
||||||
|
for _, line := range extraEEPROM {
|
||||||
|
prop, _, ok := strings.Cut(line, "=")
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
extraByProp[prop] = line
|
||||||
|
}
|
||||||
|
var out strings.Builder
|
||||||
|
for _, line := range bootconfLines {
|
||||||
|
prop, _, ok := strings.Cut(line, "=")
|
||||||
|
if !ok {
|
||||||
|
out.WriteString(line + "\n")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if extra, ok := extraByProp[prop]; ok {
|
||||||
|
out.WriteString(extra + "\n")
|
||||||
|
} else {
|
||||||
|
out.WriteString(line + "\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []byte(out.String())
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
398
internal/packer/packerbuild.go
Normal file
398
internal/packer/packerbuild.go
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
package packer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime/trace"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gokrazy/internal/updateflag"
|
||||||
|
"github.com/gokrazy/tools/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (pack *Pack) logicBuild(bindir string) error {
|
||||||
|
log := pack.Env.Logger()
|
||||||
|
|
||||||
|
cfg := pack.Cfg // for convenience
|
||||||
|
args := cfg.Packages
|
||||||
|
log.Printf("Building %d Go packages:", len(args))
|
||||||
|
log.Printf("")
|
||||||
|
for _, pkg := range args {
|
||||||
|
log.Printf(" %s", pkg)
|
||||||
|
for _, configFile := range packageConfigFiles[pkg] {
|
||||||
|
log.Printf(" will %s",
|
||||||
|
configFile.kind)
|
||||||
|
if configFile.path != "" {
|
||||||
|
log.Printf(" from %s",
|
||||||
|
configFile.path)
|
||||||
|
}
|
||||||
|
if !configFile.lastModified.IsZero() {
|
||||||
|
log.Printf(" last modified: %s (%s ago)",
|
||||||
|
configFile.lastModified.Format(time.RFC3339),
|
||||||
|
time.Since(configFile.lastModified).Round(1*time.Second))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("")
|
||||||
|
}
|
||||||
|
|
||||||
|
pkgs := append([]string{}, cfg.GokrazyPackagesOrDefault()...)
|
||||||
|
pkgs = append(pkgs, cfg.Packages...)
|
||||||
|
pkgs = append(pkgs, packer.InitDeps(cfg.InternalCompatibilityFlags.InitPkg)...)
|
||||||
|
noBuildPkgs := []string{
|
||||||
|
cfg.KernelPackageOrDefault(),
|
||||||
|
}
|
||||||
|
if fw := cfg.FirmwarePackageOrDefault(); fw != "" {
|
||||||
|
noBuildPkgs = append(noBuildPkgs, fw)
|
||||||
|
}
|
||||||
|
if e := cfg.EEPROMPackageOrDefault(); e != "" {
|
||||||
|
noBuildPkgs = append(noBuildPkgs, e)
|
||||||
|
}
|
||||||
|
setUmask()
|
||||||
|
buildEnv := &packer.BuildEnv{
|
||||||
|
BuildDir: packer.BuildDirOrMigrate,
|
||||||
|
}
|
||||||
|
var buildErr error
|
||||||
|
trace.WithRegion(context.Background(), "build", func() {
|
||||||
|
buildErr = buildEnv.Build(bindir, pkgs, pack.packageBuildFlags, pack.packageBuildTags, pack.packageBuildEnv, noBuildPkgs, pack.basenames)
|
||||||
|
})
|
||||||
|
if buildErr != nil {
|
||||||
|
return buildErr
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("")
|
||||||
|
|
||||||
|
var err error
|
||||||
|
trace.WithRegion(context.Background(), "validate", func() {
|
||||||
|
err = pack.validateTargetArchMatchesKernel()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
root *FileInfo
|
||||||
|
foundBins []foundBin
|
||||||
|
)
|
||||||
|
trace.WithRegion(context.Background(), "findbins", func() {
|
||||||
|
root, foundBins, err = findBins(cfg, buildEnv, bindir, pack.basenames)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pack.root = root
|
||||||
|
|
||||||
|
packageConfigFiles = make(map[string][]packageConfigFile)
|
||||||
|
|
||||||
|
var extraFiles map[string][]*FileInfo
|
||||||
|
trace.WithRegion(context.Background(), "findextrafiles", func() {
|
||||||
|
extraFiles, err = FindExtraFiles(cfg)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, packageExtraFiles := range extraFiles {
|
||||||
|
for _, ef := range packageExtraFiles {
|
||||||
|
for _, de := range ef.Dirents {
|
||||||
|
if de.Filename != "perm" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fmt.Errorf("invalid ExtraFilePaths or ExtraFileContents: cannot write extra files to user-controlled /perm partition")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(packageConfigFiles) > 0 {
|
||||||
|
log.Printf("Including extra files for Go packages:")
|
||||||
|
log.Printf("")
|
||||||
|
for _, pkg := range args {
|
||||||
|
if len(packageConfigFiles[pkg]) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
log.Printf(" %s", pkg)
|
||||||
|
for _, configFile := range packageConfigFiles[pkg] {
|
||||||
|
log.Printf(" will %s",
|
||||||
|
configFile.kind)
|
||||||
|
log.Printf(" from %s",
|
||||||
|
configFile.path)
|
||||||
|
log.Printf(" last modified: %s (%s ago)",
|
||||||
|
configFile.lastModified.Format(time.RFC3339),
|
||||||
|
time.Since(configFile.lastModified).Round(1*time.Second))
|
||||||
|
}
|
||||||
|
log.Printf("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.InternalCompatibilityFlags.InitPkg == "" {
|
||||||
|
gokrazyInit := &gokrazyInit{
|
||||||
|
root: root,
|
||||||
|
flagFileContents: pack.flagFileContents,
|
||||||
|
envFileContents: pack.envFileContents,
|
||||||
|
buildTimestamp: pack.buildTimestamp,
|
||||||
|
dontStart: pack.dontStart,
|
||||||
|
waitForClock: pack.waitForClock,
|
||||||
|
basenames: pack.basenames,
|
||||||
|
}
|
||||||
|
if cfg.InternalCompatibilityFlags.OverwriteInit != "" {
|
||||||
|
return gokrazyInit.dump(cfg.InternalCompatibilityFlags.OverwriteInit)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tmpdir string
|
||||||
|
trace.WithRegion(context.Background(), "buildinit", func() {
|
||||||
|
tmpdir, err = gokrazyInit.build()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pack.initTmp = tmpdir
|
||||||
|
|
||||||
|
initPath := filepath.Join(tmpdir, "init")
|
||||||
|
|
||||||
|
fileIsELFOrFatal(initPath)
|
||||||
|
|
||||||
|
gokrazy := root.mustFindDirent("gokrazy")
|
||||||
|
gokrazy.Dirents = append(gokrazy.Dirents, &FileInfo{
|
||||||
|
Filename: "init",
|
||||||
|
FromHost: initPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultPassword, updateHostname := updateflag.Value{
|
||||||
|
Update: cfg.InternalCompatibilityFlags.Update,
|
||||||
|
}.GetUpdateTarget(cfg.Hostname)
|
||||||
|
update, err := cfg.Update.WithFallbackToHostSpecific(cfg.Hostname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if update.HTTPPort == "" {
|
||||||
|
update.HTTPPort = "80"
|
||||||
|
}
|
||||||
|
|
||||||
|
if update.HTTPSPort == "" {
|
||||||
|
update.HTTPSPort = "443"
|
||||||
|
}
|
||||||
|
|
||||||
|
if update.Hostname == "" {
|
||||||
|
update.Hostname = updateHostname
|
||||||
|
}
|
||||||
|
|
||||||
|
if update.HTTPPassword == "" && !update.NoPassword {
|
||||||
|
pw, err := ensurePasswordFileExists(updateHostname, defaultPassword)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
update.HTTPPassword = pw
|
||||||
|
}
|
||||||
|
|
||||||
|
pack.update = update
|
||||||
|
|
||||||
|
for _, dir := range []string{"bin", "dev", "etc", "proc", "sys", "tmp", "perm", "lib", "run", "mnt"} {
|
||||||
|
root.Dirents = append(root.Dirents, &FileInfo{
|
||||||
|
Filename: dir,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
root.Dirents = append(root.Dirents, &FileInfo{
|
||||||
|
Filename: "var",
|
||||||
|
SymlinkDest: "/perm/var",
|
||||||
|
})
|
||||||
|
|
||||||
|
mnt := root.mustFindDirent("mnt")
|
||||||
|
for _, md := range cfg.MountDevices {
|
||||||
|
if !strings.HasPrefix(md.Target, "/mnt/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rest := strings.TrimPrefix(md.Target, "/mnt/")
|
||||||
|
rest = strings.TrimSuffix(rest, "/")
|
||||||
|
if strings.Contains(rest, "/") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
mnt.Dirents = append(mnt.Dirents, &FileInfo{
|
||||||
|
Filename: rest,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// include lib/modules from kernelPackage dir, if present
|
||||||
|
kernelDir, err := packer.PackageDir(cfg.KernelPackageOrDefault())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pack.kernelDir = kernelDir
|
||||||
|
modulesDir := filepath.Join(kernelDir, "lib", "modules")
|
||||||
|
if _, err := os.Stat(modulesDir); err == nil {
|
||||||
|
log.Printf("Including loadable kernel modules from:")
|
||||||
|
log.Printf(" %s", modulesDir)
|
||||||
|
modules := &FileInfo{
|
||||||
|
Filename: "modules",
|
||||||
|
}
|
||||||
|
trace.WithRegion(context.Background(), "kernelmod", func() {
|
||||||
|
_, err = addToFileInfo(modules, modulesDir)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lib := root.mustFindDirent("lib")
|
||||||
|
lib.Dirents = append(lib.Dirents, modules)
|
||||||
|
}
|
||||||
|
|
||||||
|
etc := root.mustFindDirent("etc")
|
||||||
|
tmpdir, err := os.MkdirTemp("", "gokrazy")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(tmpdir)
|
||||||
|
hostLocaltime, err := hostLocaltime(tmpdir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if hostLocaltime != "" {
|
||||||
|
etc.Dirents = append(etc.Dirents, &FileInfo{
|
||||||
|
Filename: "localtime",
|
||||||
|
FromHost: hostLocaltime,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
etc.Dirents = append(etc.Dirents, &FileInfo{
|
||||||
|
Filename: "resolv.conf",
|
||||||
|
SymlinkDest: "/tmp/resolv.conf",
|
||||||
|
})
|
||||||
|
etc.Dirents = append(etc.Dirents, &FileInfo{
|
||||||
|
Filename: "hosts",
|
||||||
|
FromLiteral: `127.0.0.1 localhost
|
||||||
|
::1 localhost
|
||||||
|
`,
|
||||||
|
})
|
||||||
|
etc.Dirents = append(etc.Dirents, &FileInfo{
|
||||||
|
Filename: "hostname",
|
||||||
|
FromLiteral: cfg.Hostname,
|
||||||
|
})
|
||||||
|
|
||||||
|
ssl := &FileInfo{Filename: "ssl"}
|
||||||
|
ssl.Dirents = append(ssl.Dirents, &FileInfo{
|
||||||
|
Filename: "ca-bundle.pem",
|
||||||
|
FromLiteral: pack.systemCertsPEM,
|
||||||
|
})
|
||||||
|
|
||||||
|
pack.schema = "http"
|
||||||
|
if update.CertPEM == "" || update.KeyPEM == "" {
|
||||||
|
deployCertFile, deployKeyFile, err := getCertificate(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if deployCertFile != "" {
|
||||||
|
b, err := os.ReadFile(deployCertFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
update.CertPEM = strings.TrimSpace(string(b))
|
||||||
|
|
||||||
|
b, err = os.ReadFile(deployKeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
update.KeyPEM = strings.TrimSpace(string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if update.CertPEM != "" && update.KeyPEM != "" {
|
||||||
|
// User requested TLS
|
||||||
|
pack.schema = "https"
|
||||||
|
|
||||||
|
ssl.Dirents = append(ssl.Dirents, &FileInfo{
|
||||||
|
Filename: "gokrazy-web.pem",
|
||||||
|
FromLiteral: update.CertPEM,
|
||||||
|
})
|
||||||
|
ssl.Dirents = append(ssl.Dirents, &FileInfo{
|
||||||
|
Filename: "gokrazy-web.key.pem",
|
||||||
|
FromLiteral: update.KeyPEM,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
etc.Dirents = append(etc.Dirents, ssl)
|
||||||
|
|
||||||
|
if !update.NoPassword {
|
||||||
|
etc.Dirents = append(etc.Dirents, &FileInfo{
|
||||||
|
Filename: "gokr-pw.txt",
|
||||||
|
Mode: 0400,
|
||||||
|
FromLiteral: update.HTTPPassword,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
etc.Dirents = append(etc.Dirents, &FileInfo{
|
||||||
|
Filename: "http-port.txt",
|
||||||
|
FromLiteral: update.HTTPPort,
|
||||||
|
})
|
||||||
|
|
||||||
|
etc.Dirents = append(etc.Dirents, &FileInfo{
|
||||||
|
Filename: "https-port.txt",
|
||||||
|
FromLiteral: update.HTTPSPort,
|
||||||
|
})
|
||||||
|
|
||||||
|
// GenerateSBOM() must be provided with a cfg
|
||||||
|
// that hasn't been modified by gok at runtime,
|
||||||
|
// as the SBOM should reflect what’s going into gokrazy,
|
||||||
|
// not its internal implementation details
|
||||||
|
// (i.e. cfg.InternalCompatibilityFlags untouched).
|
||||||
|
var sbom []byte
|
||||||
|
var sbomWithHash SBOMWithHash
|
||||||
|
trace.WithRegion(context.Background(), "sbom", func() {
|
||||||
|
sbom, sbomWithHash, err = generateSBOM(pack.FileCfg, foundBins)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pack.sbom = sbom
|
||||||
|
pack.sbomWithHash = sbomWithHash
|
||||||
|
|
||||||
|
etcGokrazy := &FileInfo{Filename: "gokrazy"}
|
||||||
|
etcGokrazy.Dirents = append(etcGokrazy.Dirents, &FileInfo{
|
||||||
|
Filename: "sbom.json",
|
||||||
|
FromLiteral: string(sbom),
|
||||||
|
})
|
||||||
|
mountdevices, err := json.Marshal(cfg.MountDevices)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
etcGokrazy.Dirents = append(etcGokrazy.Dirents, &FileInfo{
|
||||||
|
Filename: "mountdevices.json",
|
||||||
|
FromLiteral: string(mountdevices),
|
||||||
|
})
|
||||||
|
etc.Dirents = append(etc.Dirents, etcGokrazy)
|
||||||
|
|
||||||
|
empty := &FileInfo{Filename: ""}
|
||||||
|
if paths := getDuplication(root, empty); len(paths) > 0 {
|
||||||
|
return fmt.Errorf("root file system contains duplicate files: your config contains multiple packages that install %s", paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
for pkg1, fs := range extraFiles {
|
||||||
|
for _, fs1 := range fs {
|
||||||
|
// check against root fs
|
||||||
|
if paths := getDuplication(root, fs1); len(paths) > 0 {
|
||||||
|
return fmt.Errorf("extra files of package %s collides with root file system: %v", pkg1, paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check against other packages
|
||||||
|
for pkg2, fs := range extraFiles {
|
||||||
|
for _, fs2 := range fs {
|
||||||
|
if pkg1 == pkg2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if paths := getDuplication(fs1, fs2); len(paths) > 0 {
|
||||||
|
return fmt.Errorf("extra files of package %s collides with package %s: %v", pkg1, pkg2, paths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add extra files to rootfs
|
||||||
|
if err := root.combine(fs1); err != nil {
|
||||||
|
return fmt.Errorf("failed to add extra files from package %s: %v", pkg1, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
591
internal/packer/packerprepare.go
Normal file
591
internal/packer/packerprepare.go
Normal file
@@ -0,0 +1,591 @@
|
|||||||
|
package packer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gokrazy/internal/config"
|
||||||
|
"github.com/gokrazy/internal/deviceconfig"
|
||||||
|
"github.com/gokrazy/tools/internal/cap"
|
||||||
|
"github.com/gokrazy/tools/internal/version"
|
||||||
|
"github.com/gokrazy/tools/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type filePathAndModTime struct {
|
||||||
|
path string
|
||||||
|
modTime time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func findPackageFiles(fileType string) ([]filePathAndModTime, error) {
|
||||||
|
var packageFilePaths []filePathAndModTime
|
||||||
|
err := filepath.Walk(fileType, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info != nil && !info.Mode().IsRegular() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(path, fmt.Sprintf("/%s.txt", fileType)) {
|
||||||
|
packageFilePaths = append(packageFilePaths, filePathAndModTime{
|
||||||
|
path: path,
|
||||||
|
modTime: info.ModTime(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil // no fileType directory found
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return packageFilePaths, nil
|
||||||
|
}
|
||||||
|
func (pack *Pack) logicPrepare(ctx context.Context) error {
|
||||||
|
log := pack.Env.Logger()
|
||||||
|
cfg := pack.Cfg
|
||||||
|
|
||||||
|
if cfg.InternalCompatibilityFlags.Update != "" &&
|
||||||
|
cfg.InternalCompatibilityFlags.Overwrite != "" {
|
||||||
|
return fmt.Errorf("both -update and -overwrite are specified; use either one, not both")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check early on if the destination is a device that is mounted
|
||||||
|
// so that the user does not get the impression that everything
|
||||||
|
// is fine and about to complete after a lengthy build phase.
|
||||||
|
// See also https://github.com/gokrazy/gokrazy/discussions/308
|
||||||
|
switch {
|
||||||
|
case cfg.InternalCompatibilityFlags.Overwrite != "" ||
|
||||||
|
(pack.Output != nil && pack.Output.Type == OutputTypeFull && pack.Output.Path != ""):
|
||||||
|
|
||||||
|
target := cfg.InternalCompatibilityFlags.Overwrite
|
||||||
|
st, err := os.Stat(target)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil && st.Mode()&os.ModeDevice == os.ModeDevice {
|
||||||
|
if err := verifyNotMounted(target); err != nil {
|
||||||
|
return fmt.Errorf("cannot overwrite %s: %v (perhaps automatically?)\n please unmount all partitions and retry", target, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var mbrOnlyWithoutGpt bool
|
||||||
|
pack.firstPartitionOffsetSectors = deviceconfig.DefaultBootPartitionStartLBA
|
||||||
|
if cfg.DeviceType != "" {
|
||||||
|
if devcfg, ok := deviceconfig.GetDeviceConfigBySlug(cfg.DeviceType); ok {
|
||||||
|
pack.rootDeviceFiles = devcfg.RootDeviceFiles
|
||||||
|
mbrOnlyWithoutGpt = devcfg.MBROnlyWithoutGPT
|
||||||
|
if devcfg.BootPartitionStartLBA != 0 {
|
||||||
|
pack.firstPartitionOffsetSectors = devcfg.BootPartitionStartLBA
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("unknown device slug %q", cfg.DeviceType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pack.Pack = packer.NewPackForHost(pack.firstPartitionOffsetSectors, cfg.Hostname)
|
||||||
|
|
||||||
|
newInstallation := cfg.InternalCompatibilityFlags.Update == ""
|
||||||
|
useGPT := newInstallation && !mbrOnlyWithoutGpt
|
||||||
|
|
||||||
|
pack.Pack.UsePartuuid = newInstallation
|
||||||
|
pack.Pack.UseGPTPartuuid = useGPT
|
||||||
|
pack.Pack.UseGPT = useGPT
|
||||||
|
|
||||||
|
if os.Getenv("GOKR_PACKER_FD") != "" { // partitioning child process
|
||||||
|
if _, err := pack.SudoPartition(cfg.InternalCompatibilityFlags.Overwrite); err != nil {
|
||||||
|
log.Printf("%s", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%s %s on GOARCH=%s GOOS=%s",
|
||||||
|
programName,
|
||||||
|
version.ReadBrief(),
|
||||||
|
runtime.GOARCH,
|
||||||
|
runtime.GOOS)
|
||||||
|
log.Printf("")
|
||||||
|
|
||||||
|
if cfg.InternalCompatibilityFlags.Update != "" {
|
||||||
|
// TODO: fix update URL:
|
||||||
|
log.Printf("Updating gokrazy installation on http://%s", cfg.Hostname)
|
||||||
|
log.Printf("")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Build target: %s", strings.Join(filterGoEnv(packer.Env()), " "))
|
||||||
|
|
||||||
|
pack.buildTimestamp = time.Now().Format(time.RFC3339)
|
||||||
|
if ts, ok := ctx.Value(BuildTimestampOverride).(string); ok {
|
||||||
|
pack.buildTimestamp = ts
|
||||||
|
}
|
||||||
|
log.Printf("Build timestamp: %s", pack.buildTimestamp)
|
||||||
|
|
||||||
|
systemCertsPEM, err := pack.findSystemCertsPEM()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pack.systemCertsPEM = systemCertsPEM
|
||||||
|
|
||||||
|
packageBuildFlags, err := pack.findBuildFlagsFiles(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pack.packageBuildFlags = packageBuildFlags
|
||||||
|
|
||||||
|
packageBuildTags, err := pack.findBuildTagsFiles(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pack.packageBuildTags = packageBuildTags
|
||||||
|
|
||||||
|
packageBuildEnv, err := findBuildEnv(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pack.packageBuildEnv = packageBuildEnv
|
||||||
|
|
||||||
|
flagFileContents, err := pack.findFlagFiles(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pack.flagFileContents = flagFileContents
|
||||||
|
|
||||||
|
envFileContents, err := pack.findEnvFiles(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pack.envFileContents = envFileContents
|
||||||
|
|
||||||
|
dontStart, err := pack.findDontStart(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pack.dontStart = dontStart
|
||||||
|
|
||||||
|
waitForClock, err := pack.findWaitForClock(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pack.waitForClock = waitForClock
|
||||||
|
|
||||||
|
basenames, err := findBasenames(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pack.basenames = basenames
|
||||||
|
|
||||||
|
capabilities, err := findCapabilities(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pack.xattrs = capabilities
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pack *Pack) findFlagFiles(cfg *config.Struct) (map[string][]string, error) {
|
||||||
|
log := pack.Env.Logger()
|
||||||
|
|
||||||
|
if len(cfg.PackageConfig) > 0 {
|
||||||
|
contents := make(map[string][]string)
|
||||||
|
for pkg, packageConfig := range cfg.PackageConfig {
|
||||||
|
if len(packageConfig.CommandLineFlags) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contents[pkg] = packageConfig.CommandLineFlags
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "be started with command-line flags",
|
||||||
|
path: cfg.Meta.Path,
|
||||||
|
lastModified: cfg.Meta.LastModified,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
flagFilePaths, err := findPackageFiles("flags")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(flagFilePaths) == 0 {
|
||||||
|
return nil, nil // no flags.txt files found
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPackages := buildPackageMapFromFlags(cfg)
|
||||||
|
|
||||||
|
contents := make(map[string][]string)
|
||||||
|
for _, p := range flagFilePaths {
|
||||||
|
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "flags/"), "/flags.txt")
|
||||||
|
if !buildPackages[pkg] {
|
||||||
|
log.Printf("WARNING: flag file %s does not match any specified package (%s)", pkg, cfg.Packages)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "be started with command-line flags",
|
||||||
|
path: p.path,
|
||||||
|
lastModified: p.modTime,
|
||||||
|
})
|
||||||
|
|
||||||
|
b, err := os.ReadFile(p.path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(b)), "\n")
|
||||||
|
contents[pkg] = lines
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pack *Pack) findBuildFlagsFiles(cfg *config.Struct) (map[string][]string, error) {
|
||||||
|
log := pack.Env.Logger()
|
||||||
|
|
||||||
|
if len(cfg.PackageConfig) > 0 {
|
||||||
|
contents := make(map[string][]string)
|
||||||
|
for pkg, packageConfig := range cfg.PackageConfig {
|
||||||
|
if len(packageConfig.GoBuildFlags) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contents[pkg] = packageConfig.GoBuildFlags
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "be compiled with build flags",
|
||||||
|
path: cfg.Meta.Path,
|
||||||
|
lastModified: cfg.Meta.LastModified,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFlagsFilePaths, err := findPackageFiles("buildflags")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buildFlagsFilePaths) == 0 {
|
||||||
|
return nil, nil // no flags.txt files found
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPackages := buildPackageMapFromFlags(cfg)
|
||||||
|
|
||||||
|
contents := make(map[string][]string)
|
||||||
|
for _, p := range buildFlagsFilePaths {
|
||||||
|
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "buildflags/"), "/buildflags.txt")
|
||||||
|
if !buildPackages[pkg] {
|
||||||
|
log.Printf("WARNING: buildflags file %s does not match any specified package (%s)", pkg, cfg.Packages)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "be compiled with build flags",
|
||||||
|
path: p.path,
|
||||||
|
lastModified: p.modTime,
|
||||||
|
})
|
||||||
|
|
||||||
|
b, err := os.ReadFile(p.path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buildFlags []string
|
||||||
|
sc := bufio.NewScanner(strings.NewReader(string(b)))
|
||||||
|
for sc.Scan() {
|
||||||
|
if flag := sc.Text(); flag != "" {
|
||||||
|
buildFlags = append(buildFlags, flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sc.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// use full package path opposed to flags
|
||||||
|
contents[pkg] = buildFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findBuildEnv(cfg *config.Struct) (map[string][]string, error) {
|
||||||
|
contents := make(map[string][]string)
|
||||||
|
for pkg, packageConfig := range cfg.PackageConfig {
|
||||||
|
if len(packageConfig.GoBuildEnvironment) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contents[pkg] = packageConfig.GoBuildEnvironment
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "be compiled with build environment variables",
|
||||||
|
path: cfg.Meta.Path,
|
||||||
|
lastModified: cfg.Meta.LastModified,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pack *Pack) findBuildTagsFiles(cfg *config.Struct) (map[string][]string, error) {
|
||||||
|
log := pack.Env.Logger()
|
||||||
|
|
||||||
|
if len(cfg.PackageConfig) > 0 {
|
||||||
|
contents := make(map[string][]string)
|
||||||
|
for pkg, packageConfig := range cfg.PackageConfig {
|
||||||
|
if len(packageConfig.GoBuildTags) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contents[pkg] = packageConfig.GoBuildTags
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "be compiled with build tags",
|
||||||
|
path: cfg.Meta.Path,
|
||||||
|
lastModified: cfg.Meta.LastModified,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTagsFiles, err := findPackageFiles("buildtags")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buildTagsFiles) == 0 {
|
||||||
|
return nil, nil // no flags.txt files found
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPackages := buildPackageMapFromFlags(cfg)
|
||||||
|
|
||||||
|
contents := make(map[string][]string)
|
||||||
|
for _, p := range buildTagsFiles {
|
||||||
|
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "buildtags/"), "/buildtags.txt")
|
||||||
|
if !buildPackages[pkg] {
|
||||||
|
log.Printf("WARNING: buildtags file %s does not match any specified package (%s)", pkg, cfg.Packages)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "be compiled with build tags",
|
||||||
|
path: p.path,
|
||||||
|
lastModified: p.modTime,
|
||||||
|
})
|
||||||
|
|
||||||
|
b, err := os.ReadFile(p.path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var buildTags []string
|
||||||
|
sc := bufio.NewScanner(strings.NewReader(string(b)))
|
||||||
|
for sc.Scan() {
|
||||||
|
if flag := sc.Text(); flag != "" {
|
||||||
|
buildTags = append(buildTags, flag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := sc.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// use full package path opposed to flags
|
||||||
|
contents[pkg] = buildTags
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pack *Pack) findEnvFiles(cfg *config.Struct) (map[string][]string, error) {
|
||||||
|
log := pack.Env.Logger()
|
||||||
|
|
||||||
|
if len(cfg.PackageConfig) > 0 {
|
||||||
|
contents := make(map[string][]string)
|
||||||
|
for pkg, packageConfig := range cfg.PackageConfig {
|
||||||
|
if len(packageConfig.Environment) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contents[pkg] = packageConfig.Environment
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "be started with environment variables",
|
||||||
|
path: cfg.Meta.Path,
|
||||||
|
lastModified: cfg.Meta.LastModified,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buildFlagsFilePaths, err := findPackageFiles("env")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(buildFlagsFilePaths) == 0 {
|
||||||
|
return nil, nil // no flags.txt files found
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPackages := buildPackageMapFromFlags(cfg)
|
||||||
|
|
||||||
|
contents := make(map[string][]string)
|
||||||
|
for _, p := range buildFlagsFilePaths {
|
||||||
|
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "env/"), "/env.txt")
|
||||||
|
if !buildPackages[pkg] {
|
||||||
|
log.Printf("WARNING: environment variable file %s does not match any specified package (%s)", pkg, cfg.Packages)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "be started with environment variables",
|
||||||
|
path: p.path,
|
||||||
|
lastModified: p.modTime,
|
||||||
|
})
|
||||||
|
|
||||||
|
b, err := os.ReadFile(p.path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.TrimSpace(string(b)), "\n")
|
||||||
|
contents[pkg] = lines
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pack *Pack) findDontStart(cfg *config.Struct) (map[string]bool, error) {
|
||||||
|
log := pack.Env.Logger()
|
||||||
|
|
||||||
|
if len(cfg.PackageConfig) > 0 {
|
||||||
|
contents := make(map[string]bool)
|
||||||
|
for pkg, packageConfig := range cfg.PackageConfig {
|
||||||
|
if !packageConfig.DontStart {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contents[pkg] = packageConfig.DontStart
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "not be started at boot",
|
||||||
|
path: cfg.Meta.Path,
|
||||||
|
lastModified: cfg.Meta.LastModified,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dontStartPaths, err := findPackageFiles("dontstart")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(dontStartPaths) == 0 {
|
||||||
|
return nil, nil // no dontstart.txt files found
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPackages := buildPackageMapFromFlags(cfg)
|
||||||
|
|
||||||
|
contents := make(map[string]bool)
|
||||||
|
for _, p := range dontStartPaths {
|
||||||
|
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "dontstart/"), "/dontstart.txt")
|
||||||
|
if !buildPackages[pkg] {
|
||||||
|
log.Printf("WARNING: dontstart.txt file %s does not match any specified package (%s)", pkg, cfg.Packages)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "not be started at boot",
|
||||||
|
path: p.path,
|
||||||
|
lastModified: p.modTime,
|
||||||
|
})
|
||||||
|
|
||||||
|
contents[pkg] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pack *Pack) findWaitForClock(cfg *config.Struct) (map[string]bool, error) {
|
||||||
|
log := pack.Env.Logger()
|
||||||
|
|
||||||
|
if len(cfg.PackageConfig) > 0 {
|
||||||
|
contents := make(map[string]bool)
|
||||||
|
for pkg, packageConfig := range cfg.PackageConfig {
|
||||||
|
if !packageConfig.WaitForClock {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contents[pkg] = packageConfig.WaitForClock
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "wait for clock synchronization before start",
|
||||||
|
path: cfg.Meta.Path,
|
||||||
|
lastModified: cfg.Meta.LastModified,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForClockPaths, err := findPackageFiles("waitforclock")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(waitForClockPaths) == 0 {
|
||||||
|
return nil, nil // no waitforclock.txt files found
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPackages := buildPackageMapFromFlags(cfg)
|
||||||
|
|
||||||
|
contents := make(map[string]bool)
|
||||||
|
for _, p := range waitForClockPaths {
|
||||||
|
pkg := strings.TrimSuffix(strings.TrimPrefix(p.path, "waitforclock/"), "/waitforclock.txt")
|
||||||
|
if !buildPackages[pkg] {
|
||||||
|
log.Printf("WARNING: waitforclock.txt file %s does not match any specified package (%s)", pkg, cfg.Packages)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "wait for clock synchronization before start",
|
||||||
|
path: p.path,
|
||||||
|
lastModified: p.modTime,
|
||||||
|
})
|
||||||
|
|
||||||
|
contents[pkg] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findBasenames(cfg *config.Struct) (map[string]string, error) {
|
||||||
|
contents := make(map[string]string)
|
||||||
|
for pkg, packageConfig := range cfg.PackageConfig {
|
||||||
|
if packageConfig.Basename == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
contents[pkg] = packageConfig.Basename
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "be installed with the basename set to " + packageConfig.Basename,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findCapabilities(cfg *config.Struct) (map[string]map[string][]byte, error) {
|
||||||
|
contents := make(map[string]map[string][]byte)
|
||||||
|
for pkg, packageConfig := range cfg.PackageConfig {
|
||||||
|
if packageConfig.Capabilities == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
set, err := cap.FromText(packageConfig.Capabilities)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to parse capabilities: %s: %w", packageConfig.Capabilities, err)
|
||||||
|
}
|
||||||
|
xattrValue, err := set.PackFileCap()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to pack capabilities: %s: %w", packageConfig.Capabilities, err) // This should basically never happen
|
||||||
|
}
|
||||||
|
set, err = cap.DigestFileCap(xattrValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error checking packed capabilities: %s: %w", packageConfig.Capabilities, err) // This should also never happen
|
||||||
|
}
|
||||||
|
contents[pkg] = map[string][]byte{
|
||||||
|
"security.capability": xattrValue,
|
||||||
|
}
|
||||||
|
packageConfigFiles[pkg] = append(packageConfigFiles[pkg], packageConfigFile{
|
||||||
|
kind: "be installed with file capabilities " + set.String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return contents, nil
|
||||||
|
}
|
||||||
253
internal/packer/packerupdate.go
Normal file
253
internal/packer/packerupdate.go
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
package packer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gokrazy/internal/httpclient"
|
||||||
|
"github.com/gokrazy/internal/humanize"
|
||||||
|
"github.com/gokrazy/internal/progress"
|
||||||
|
"github.com/gokrazy/internal/updateflag"
|
||||||
|
"github.com/gokrazy/updater"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (pack *Pack) logicUpdate(ctx context.Context, isDev bool, bootSize int64, rootSize int64, tmpMBR, tmpBoot, tmpRoot *os.File, updateBaseUrl *url.URL, target *updater.Target, updateHttpClient *http.Client) error {
|
||||||
|
log := pack.Env.Logger()
|
||||||
|
cfg := pack.Cfg // for convenience
|
||||||
|
update := pack.update // for convenience
|
||||||
|
|
||||||
|
var rootReader, bootReader, mbrReader io.Reader
|
||||||
|
// Determine where to read the boot, root and MBR images from.
|
||||||
|
switch {
|
||||||
|
case cfg.InternalCompatibilityFlags.Overwrite != "":
|
||||||
|
if isDev {
|
||||||
|
bootFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite + "1")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bootReader = bootFile
|
||||||
|
rootFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite + "2")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rootReader = rootFile
|
||||||
|
} else {
|
||||||
|
bootFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := bootFile.Seek(pack.firstPartitionOffsetSectors*512, io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bootReader = &io.LimitedReader{
|
||||||
|
R: bootFile,
|
||||||
|
N: bootSize,
|
||||||
|
}
|
||||||
|
|
||||||
|
rootFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := rootFile.Seek(pack.firstPartitionOffsetSectors*512+100*MB, io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rootReader = &io.LimitedReader{
|
||||||
|
R: rootFile,
|
||||||
|
N: rootSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mbrFile, err := os.Open(cfg.InternalCompatibilityFlags.Overwrite)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mbrReader = &io.LimitedReader{
|
||||||
|
R: mbrFile,
|
||||||
|
N: 446,
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if cfg.InternalCompatibilityFlags.OverwriteBoot != "" {
|
||||||
|
bootFile, err := os.Open(cfg.InternalCompatibilityFlags.OverwriteBoot)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bootReader = bootFile
|
||||||
|
if cfg.InternalCompatibilityFlags.OverwriteMBR != "" {
|
||||||
|
mbrFile, err := os.Open(cfg.InternalCompatibilityFlags.OverwriteMBR)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mbrReader = mbrFile
|
||||||
|
} else {
|
||||||
|
if _, err := tmpMBR.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mbrReader = tmpMBR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.InternalCompatibilityFlags.OverwriteRoot != "" {
|
||||||
|
rootFile, err := os.Open(cfg.InternalCompatibilityFlags.OverwriteRoot)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rootReader = rootFile
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.InternalCompatibilityFlags.OverwriteBoot == "" && cfg.InternalCompatibilityFlags.OverwriteRoot == "" {
|
||||||
|
if _, err := tmpBoot.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
bootReader = tmpBoot
|
||||||
|
|
||||||
|
if _, err := tmpMBR.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mbrReader = tmpMBR
|
||||||
|
|
||||||
|
if _, err := tmpRoot.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
rootReader = tmpRoot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBaseUrl.Path = "/"
|
||||||
|
log.Printf("Updating %s", updateBaseUrl.String())
|
||||||
|
|
||||||
|
progctx, canc := context.WithCancel(context.Background())
|
||||||
|
defer canc()
|
||||||
|
prog := &progress.Reporter{}
|
||||||
|
go prog.Report(progctx)
|
||||||
|
|
||||||
|
// Start with the root file system because writing to the non-active
|
||||||
|
// partition cannot break the currently running system.
|
||||||
|
if err := pack.updateWithProgress(prog, rootReader, target, "root file system", "root"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, rootDeviceFile := range pack.rootDeviceFiles {
|
||||||
|
f, err := os.Open(filepath.Join(pack.kernelDir, rootDeviceFile.Name))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pack.updateWithProgress(
|
||||||
|
prog, f, target, fmt.Sprintf("root device file %s", rootDeviceFile.Name),
|
||||||
|
filepath.Join("device-specific", rootDeviceFile.Name),
|
||||||
|
); err != nil {
|
||||||
|
if errors.Is(err, updater.ErrUpdateHandlerNotImplemented) {
|
||||||
|
log.Printf("target does not support updating device file %s yet, ignoring", rootDeviceFile.Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pack.updateWithProgress(prog, bootReader, target, "boot file system", "boot"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := target.StreamTo(ctx, "mbr", mbrReader); err != nil {
|
||||||
|
if err == updater.ErrUpdateHandlerNotImplemented {
|
||||||
|
log.Printf("target does not support updating MBR yet, ignoring")
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("updating MBR: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.InternalCompatibilityFlags.Testboot {
|
||||||
|
if err := target.Testboot(ctx); err != nil {
|
||||||
|
return fmt.Errorf("enable testboot of non-active partition: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err := target.Switch(ctx); err != nil {
|
||||||
|
return fmt.Errorf("switching to non-active partition: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop progress reporting to not mess up the following logs output.
|
||||||
|
canc()
|
||||||
|
|
||||||
|
log.Printf("Triggering reboot")
|
||||||
|
if err := target.Reboot(ctx); err != nil {
|
||||||
|
if errors.Is(err, syscall.ECONNRESET) {
|
||||||
|
log.Printf("ignoring reboot error: %v", err)
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("reboot: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const polltimeout = 5 * time.Minute
|
||||||
|
log.Printf("Updated, waiting %v for the device to become reachable (cancel with Ctrl-C any time)", polltimeout)
|
||||||
|
|
||||||
|
if update.CertPEM != "" && update.KeyPEM != "" {
|
||||||
|
// Use an HTTPS client (post-update),
|
||||||
|
// even when the --insecure flag was specified.
|
||||||
|
pack.schema = "https"
|
||||||
|
var err error
|
||||||
|
updateBaseUrl, err = updateflag.Value{
|
||||||
|
Update: "yes",
|
||||||
|
}.BaseURL(update.HTTPPort, update.HTTPSPort, pack.schema, update.Hostname, update.HTTPPassword)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updateHttpClient, _, err = httpclient.GetTLSHttpClientByTLSFlag(update.UseTLS, false /* insecure */, updateBaseUrl)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting http client by tls flag: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pollctx, canc := context.WithTimeout(context.Background(), polltimeout)
|
||||||
|
defer canc()
|
||||||
|
for {
|
||||||
|
if err := pollctx.Err(); err != nil {
|
||||||
|
return fmt.Errorf("device did not become healthy after update (%v)", err)
|
||||||
|
}
|
||||||
|
if err := pollUpdated1(pollctx, updateHttpClient, updateBaseUrl.String(), pack.buildTimestamp); err != nil {
|
||||||
|
log.Printf("device not yet reachable: %v", err)
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Device ready to use!")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pack *Pack) updateWithProgress(prog *progress.Reporter, reader io.Reader, target *updater.Target, logStr string, stream string) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
log := pack.Env.Logger()
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
prog.SetStatus(fmt.Sprintf("update %s", logStr))
|
||||||
|
prog.SetTotal(0)
|
||||||
|
|
||||||
|
if stater, ok := reader.(interface{ Stat() (os.FileInfo, error) }); ok {
|
||||||
|
if st, err := stater.Stat(); err == nil {
|
||||||
|
prog.SetTotal(uint64(st.Size()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := target.StreamTo(ctx, stream, io.TeeReader(reader, &progress.Writer{})); err != nil {
|
||||||
|
return fmt.Errorf("updating %s: %w", logStr, err)
|
||||||
|
}
|
||||||
|
duration := time.Since(start)
|
||||||
|
transferred := progress.Reset()
|
||||||
|
log.Printf("\rTransferred %s (%s) at %.2f MiB/s (total: %v)",
|
||||||
|
logStr,
|
||||||
|
humanize.Bytes(transferred),
|
||||||
|
float64(transferred)/duration.Seconds()/1024/1024,
|
||||||
|
duration.Round(time.Second))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
433
internal/packer/packerwrite.go
Normal file
433
internal/packer/packerwrite.go
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
package packer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime/trace"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gokrazy/internal/config"
|
||||||
|
"github.com/gokrazy/internal/deviceconfig"
|
||||||
|
"github.com/gokrazy/internal/httpclient"
|
||||||
|
"github.com/gokrazy/internal/updateflag"
|
||||||
|
"github.com/gokrazy/tools/packer"
|
||||||
|
"github.com/gokrazy/updater"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (pack *Pack) logicWrite(dnsCheck chan error) error {
|
||||||
|
ctx := context.Background()
|
||||||
|
log := pack.Env.Logger()
|
||||||
|
|
||||||
|
var (
|
||||||
|
updateHttpClient *http.Client
|
||||||
|
updateBaseUrl *url.URL
|
||||||
|
target *updater.Target
|
||||||
|
)
|
||||||
|
|
||||||
|
newInstallation := pack.Cfg.InternalCompatibilityFlags.Update == ""
|
||||||
|
insecure := pack.Cfg.InternalCompatibilityFlags.Insecure
|
||||||
|
if !newInstallation {
|
||||||
|
update := pack.update // for convenience
|
||||||
|
var err error
|
||||||
|
updateBaseUrl, err = updateflag.Value{
|
||||||
|
Update: pack.Cfg.InternalCompatibilityFlags.Update,
|
||||||
|
}.BaseURL(update.HTTPPort, update.HTTPSPort, pack.schema, update.Hostname, update.HTTPPassword)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHttpClient, _, err = httpclient.GetTLSHttpClientByTLSFlag(update.UseTLS, insecure, updateBaseUrl)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting http client by tls flag: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBaseUrl.Path = "/"
|
||||||
|
|
||||||
|
target, err = updater.NewTarget(ctx, updateBaseUrl.String(), updateHttpClient)
|
||||||
|
if err != nil {
|
||||||
|
if !insecure {
|
||||||
|
return fmt.Errorf("checking target partuuid support: %v", err)
|
||||||
|
}
|
||||||
|
log.Printf("Falling back to HTTP because of the --insecure flag")
|
||||||
|
updateBaseUrl, err = updateflag.Value{
|
||||||
|
Update: pack.Cfg.InternalCompatibilityFlags.Update,
|
||||||
|
}.BaseURL(update.HTTPPort, update.HTTPSPort, "http", update.Hostname, update.HTTPPassword)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHttpClient, _, err = httpclient.GetTLSHttpClientByTLSFlag(update.UseTLS, insecure, updateBaseUrl)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("getting http client by tls flag: %v", err)
|
||||||
|
}
|
||||||
|
target, err = updater.NewTarget(ctx, updateBaseUrl.String(), updateHttpClient)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("checking target partuuid support: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pack.UsePartuuid = target.Supports("partuuid")
|
||||||
|
pack.UseGPTPartuuid = target.Supports("gpt")
|
||||||
|
pack.UseGPT = target.Supports("gpt")
|
||||||
|
pack.ExistingEEPROM = target.InstalledEEPROM()
|
||||||
|
}
|
||||||
|
log.Printf("")
|
||||||
|
log.Printf("Feature summary:")
|
||||||
|
log.Printf(" use GPT: %v", pack.UseGPT)
|
||||||
|
log.Printf(" use PARTUUID: %v", pack.UsePartuuid)
|
||||||
|
log.Printf(" use GPT PARTUUID: %v", pack.UseGPTPartuuid)
|
||||||
|
|
||||||
|
cfg := pack.Cfg // for convenience
|
||||||
|
root := pack.root // for convenience
|
||||||
|
// Determine where to write the boot and root images to.
|
||||||
|
var (
|
||||||
|
isDev bool
|
||||||
|
tmpBoot, tmpRoot, tmpMBR *os.File
|
||||||
|
bootSize, rootSize int64
|
||||||
|
)
|
||||||
|
switch {
|
||||||
|
case cfg.InternalCompatibilityFlags.Overwrite != "" ||
|
||||||
|
(pack.Output != nil && pack.Output.Type == OutputTypeFull && pack.Output.Path != ""):
|
||||||
|
|
||||||
|
st, err := os.Stat(cfg.InternalCompatibilityFlags.Overwrite)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
isDev = err == nil && st.Mode()&os.ModeDevice == os.ModeDevice
|
||||||
|
|
||||||
|
if isDev {
|
||||||
|
if err := pack.overwriteDevice(cfg.InternalCompatibilityFlags.Overwrite, root, pack.rootDeviceFiles); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("To boot gokrazy, plug the SD card into a supported device (see https://gokrazy.org/platforms/)")
|
||||||
|
log.Printf("")
|
||||||
|
} else {
|
||||||
|
lower := 1200*MB + int(pack.firstPartitionOffsetSectors)
|
||||||
|
|
||||||
|
if cfg.InternalCompatibilityFlags.TargetStorageBytes == 0 {
|
||||||
|
return fmt.Errorf("--target_storage_bytes is required (e.g. --target_storage_bytes=%d) when using overwrite with a file", lower)
|
||||||
|
}
|
||||||
|
if cfg.InternalCompatibilityFlags.TargetStorageBytes%512 != 0 {
|
||||||
|
return fmt.Errorf("--target_storage_bytes must be a multiple of 512 (sector size), use e.g. %d", lower)
|
||||||
|
}
|
||||||
|
if cfg.InternalCompatibilityFlags.TargetStorageBytes < lower {
|
||||||
|
return fmt.Errorf("--target_storage_bytes must be at least %d (for boot + 2 root file systems + 100 MB /perm)", lower)
|
||||||
|
}
|
||||||
|
|
||||||
|
bootSize, rootSize, err = pack.overwriteFile(root, pack.rootDeviceFiles, pack.firstPartitionOffsetSectors)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("To boot gokrazy, copy %s to an SD card and plug it into a supported device (see https://gokrazy.org/platforms/)", cfg.InternalCompatibilityFlags.Overwrite)
|
||||||
|
log.Printf("")
|
||||||
|
}
|
||||||
|
|
||||||
|
case pack.Output != nil && pack.Output.Type == OutputTypeGaf && pack.Output.Path != "":
|
||||||
|
if err := pack.overwriteGaf(root, pack.sbom); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
if cfg.InternalCompatibilityFlags.OverwriteBoot != "" {
|
||||||
|
mbrfn := cfg.InternalCompatibilityFlags.OverwriteMBR
|
||||||
|
if cfg.InternalCompatibilityFlags.OverwriteMBR == "" {
|
||||||
|
var err error
|
||||||
|
tmpMBR, err = os.CreateTemp("", "gokrazy")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpMBR.Name())
|
||||||
|
mbrfn = tmpMBR.Name()
|
||||||
|
}
|
||||||
|
if err := pack.writeBootFile(cfg.InternalCompatibilityFlags.OverwriteBoot, mbrfn); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.InternalCompatibilityFlags.OverwriteRoot != "" {
|
||||||
|
var rootErr error
|
||||||
|
trace.WithRegion(context.Background(), "writeroot", func() {
|
||||||
|
rootErr = pack.writeRootFile(cfg.InternalCompatibilityFlags.OverwriteRoot, root)
|
||||||
|
})
|
||||||
|
if rootErr != nil {
|
||||||
|
return rootErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.InternalCompatibilityFlags.OverwriteBoot == "" && cfg.InternalCompatibilityFlags.OverwriteRoot == "" {
|
||||||
|
var err error
|
||||||
|
tmpMBR, err = os.CreateTemp("", "gokrazy")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpMBR.Name())
|
||||||
|
|
||||||
|
tmpBoot, err = os.CreateTemp("", "gokrazy")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpBoot.Name())
|
||||||
|
|
||||||
|
if err := pack.writeBoot(tmpBoot, tmpMBR.Name()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpRoot, err = os.CreateTemp("", "gokrazy")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpRoot.Name())
|
||||||
|
|
||||||
|
if err := pack.writeRoot(tmpRoot, root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("")
|
||||||
|
log.Printf("Build complete!")
|
||||||
|
|
||||||
|
if err := pack.printHowToInteract(cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := <-dnsCheck; err != nil {
|
||||||
|
log.Printf("WARNING: if the above URL does not work, perhaps name resolution (DNS) is broken")
|
||||||
|
log.Printf("in your local network? Resolving your hostname failed: %v", err)
|
||||||
|
log.Printf("Did you maybe configure a DNS server other than your router?")
|
||||||
|
log.Printf("")
|
||||||
|
}
|
||||||
|
|
||||||
|
if newInstallation {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return pack.logicUpdate(ctx, isDev, bootSize, rootSize, tmpMBR, tmpBoot, tmpRoot, updateBaseUrl, target, updateHttpClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pack) overwriteDevice(dev string, root *FileInfo, rootDeviceFiles []deviceconfig.RootFile) error {
|
||||||
|
log := p.Env.Logger()
|
||||||
|
|
||||||
|
if err := verifyNotMounted(dev); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
parttable := "GPT + Hybrid MBR"
|
||||||
|
if !p.UseGPT {
|
||||||
|
parttable = "no GPT, only MBR"
|
||||||
|
}
|
||||||
|
log.Printf("partitioning %s (%s)", dev, parttable)
|
||||||
|
|
||||||
|
f, err := p.partition(p.Cfg.InternalCompatibilityFlags.Overwrite)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if _, err := f.Seek(p.FirstPartitionOffsetSectors*512, io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.writeBoot(f, ""); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.writeMBR(p.FirstPartitionOffsetSectors, &offsetReadSeeker{f, p.FirstPartitionOffsetSectors * 512}, f, p.Partuuid); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := f.Seek((p.FirstPartitionOffsetSectors+(100*MB/512))*512, io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := os.CreateTemp("", "gokr-packer")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(tmp.Name())
|
||||||
|
defer tmp.Close()
|
||||||
|
|
||||||
|
if err := p.writeRoot(tmp, root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tmp.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := io.Copy(f, tmp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.writeRootDeviceFiles(f, rootDeviceFiles); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("If your applications need to store persistent data, unplug and re-plug the SD card, then create a file system using e.g.:")
|
||||||
|
log.Printf("")
|
||||||
|
partition := partitionPath(dev, "4")
|
||||||
|
if p.ModifyCmdlineRoot() {
|
||||||
|
partition = fmt.Sprintf("/dev/disk/by-partuuid/%s", p.PermUUID())
|
||||||
|
} else {
|
||||||
|
if target, err := filepath.EvalSymlinks(dev); err == nil {
|
||||||
|
partition = partitionPath(target, "4")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("\tmkfs.ext4 %s", partition)
|
||||||
|
log.Printf("")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func partitionPath(base, num string) string {
|
||||||
|
if strings.HasPrefix(base, "/dev/mmcblk") ||
|
||||||
|
strings.HasPrefix(base, "/dev/loop") {
|
||||||
|
return base + "p" + num
|
||||||
|
} else if strings.HasPrefix(base, "/dev/disk") ||
|
||||||
|
strings.HasPrefix(base, "/dev/rdisk") {
|
||||||
|
return base + "s" + num
|
||||||
|
}
|
||||||
|
return base + num
|
||||||
|
}
|
||||||
|
|
||||||
|
type offsetReadSeeker struct {
|
||||||
|
io.ReadSeeker
|
||||||
|
offset int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ors *offsetReadSeeker) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
if whence == io.SeekStart {
|
||||||
|
// github.com/gokrazy/internal/fat.Reader only uses io.SeekStart
|
||||||
|
return ors.ReadSeeker.Seek(offset+ors.offset, io.SeekStart)
|
||||||
|
}
|
||||||
|
return ors.ReadSeeker.Seek(offset, whence)
|
||||||
|
}
|
||||||
|
|
||||||
|
type countingWriter int64
|
||||||
|
|
||||||
|
func (cw *countingWriter) Write(p []byte) (n int, err error) {
|
||||||
|
*cw += countingWriter(len(p))
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Pack) overwriteFile(root *FileInfo, rootDeviceFiles []deviceconfig.RootFile, firstPartitionOffsetSectors int64) (bootSize int64, rootSize int64, err error) {
|
||||||
|
log := p.Env.Logger()
|
||||||
|
|
||||||
|
f, err := os.Create(p.Cfg.InternalCompatibilityFlags.Overwrite)
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.Truncate(int64(p.Cfg.InternalCompatibilityFlags.TargetStorageBytes)); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.Partition(f, uint64(p.Cfg.InternalCompatibilityFlags.TargetStorageBytes)); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := f.Seek(p.FirstPartitionOffsetSectors*512, io.SeekStart); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
var bs countingWriter
|
||||||
|
if err := p.writeBoot(io.MultiWriter(f, &bs), ""); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.writeMBR(p.FirstPartitionOffsetSectors, &offsetReadSeeker{f, p.FirstPartitionOffsetSectors * 512}, f, p.Partuuid); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := f.Seek(p.FirstPartitionOffsetSectors*512+100*MB, io.SeekStart); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := os.CreateTemp("", "gokr-packer")
|
||||||
|
if err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
defer os.Remove(tmp.Name())
|
||||||
|
defer tmp.Close()
|
||||||
|
|
||||||
|
if err := p.writeRoot(tmp, root); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
if _, err := tmp.Seek(0, io.SeekStart); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rs countingWriter
|
||||||
|
if _, err := io.Copy(io.MultiWriter(f, &rs), tmp); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.writeRootDeviceFiles(f, rootDeviceFiles); err != nil {
|
||||||
|
return 0, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("If your applications need to store persistent data, create a file system using e.g.:")
|
||||||
|
log.Printf("\t/sbin/mkfs.ext4 -F -E offset=%v %s %v", p.FirstPartitionOffsetSectors*512+1100*MB, p.Cfg.InternalCompatibilityFlags.Overwrite, packer.PermSizeInKB(firstPartitionOffsetSectors, uint64(p.Cfg.InternalCompatibilityFlags.TargetStorageBytes)))
|
||||||
|
log.Printf("")
|
||||||
|
|
||||||
|
return int64(bs), int64(rs), f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pack *Pack) printHowToInteract(cfg *config.Struct) error {
|
||||||
|
log := pack.Env.Logger()
|
||||||
|
update := pack.update // for convenience
|
||||||
|
|
||||||
|
updateFlag := pack.Cfg.InternalCompatibilityFlags.Update
|
||||||
|
if updateFlag == "" {
|
||||||
|
updateFlag = "yes"
|
||||||
|
}
|
||||||
|
updateBaseUrl, err := updateflag.Value{
|
||||||
|
Update: updateFlag,
|
||||||
|
}.BaseURL(update.HTTPPort, update.HTTPSPort, pack.schema, update.Hostname, update.HTTPPassword)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("")
|
||||||
|
log.Printf("To interact with the device, gokrazy provides a web interface reachable at:")
|
||||||
|
log.Printf("")
|
||||||
|
log.Printf("\t%s", updateBaseUrl.String())
|
||||||
|
log.Printf("")
|
||||||
|
log.Printf("In addition, the following Linux consoles are set up:")
|
||||||
|
log.Printf("")
|
||||||
|
if cfg.SerialConsoleOrDefault() != "disabled" {
|
||||||
|
log.Printf("\t1. foreground Linux console on the serial port (115200n8, pin 6, 8, 10 for GND, TX, RX), accepting input")
|
||||||
|
log.Printf("\t2. secondary Linux framebuffer console on HDMI; shows Linux kernel message but no init system messages")
|
||||||
|
} else {
|
||||||
|
log.Printf("\t1. foreground Linux framebuffer console on HDMI")
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.SerialConsoleOrDefault() != "disabled" {
|
||||||
|
log.Printf("")
|
||||||
|
log.Printf("Use -serial_console=disabled to make gokrazy not touch the serial port, and instead make the framebuffer console on HDMI the foreground console")
|
||||||
|
}
|
||||||
|
log.Printf("")
|
||||||
|
if pack.schema == "https" {
|
||||||
|
certObj, err := getCertificateFromString(update.CertPEM)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error loading certificate: %v", err)
|
||||||
|
} else {
|
||||||
|
log.Printf("")
|
||||||
|
log.Printf("The TLS Certificate of the gokrazy web interface is located under")
|
||||||
|
log.Printf("\t%s", cfg.Meta.Path)
|
||||||
|
log.Printf("The fingerprint of the Certificate is")
|
||||||
|
log.Printf("\t%x", getCertificateFingerprintSHA1(certObj))
|
||||||
|
log.Printf("The certificate is valid until")
|
||||||
|
log.Printf("\t%s", certObj.NotAfter.String())
|
||||||
|
log.Printf("Please verify the certificate, before adding an exception to your browser!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
82
internal/packer/validatekernel.go
Normal file
82
internal/packer/validatekernel.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package packer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gokrazy/tools/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// kernelGoarch returns the GOARCH value that corresponds to the provided
|
||||||
|
// vmlinuz header. It returns one of "arm", "arm64", "386", "amd64" or the empty
|
||||||
|
// string if not detected.
|
||||||
|
func kernelGoarch(hdr []byte) string {
|
||||||
|
// Some constants from the file(1) command's magic.
|
||||||
|
const (
|
||||||
|
// 32-bit arm: https://github.com/file/file/blob/65be1904/magic/Magdir/linux#L238-L241
|
||||||
|
arm32Magic = 0x016f2818
|
||||||
|
arm32MagicOffset = 0x24
|
||||||
|
// 64-bit arm: https://github.com/file/file/blob/65be1904/magic/Magdir/linux#L253-L254
|
||||||
|
arm64Magic = 0x644d5241
|
||||||
|
arm64MagicOffset = 0x38
|
||||||
|
// x86: https://github.com/file/file/blob/65be1904/magic/Magdir/linux#L137-L152
|
||||||
|
x86Magic = 0xaa55
|
||||||
|
x86MagicOffset = 0x1fe
|
||||||
|
x86XloadflagsOffset = 0x236
|
||||||
|
)
|
||||||
|
if len(hdr) >= arm64MagicOffset+4 && binary.LittleEndian.Uint32(hdr[arm64MagicOffset:]) == arm64Magic {
|
||||||
|
return "arm64"
|
||||||
|
}
|
||||||
|
if len(hdr) >= arm32MagicOffset+4 && binary.LittleEndian.Uint32(hdr[arm32MagicOffset:]) == arm32Magic {
|
||||||
|
return "arm"
|
||||||
|
}
|
||||||
|
if len(hdr) >= x86XloadflagsOffset+2 && binary.LittleEndian.Uint16(hdr[x86MagicOffset:]) == x86Magic {
|
||||||
|
// XLF0 in arch/x86/boot/header.S
|
||||||
|
if hdr[x86XloadflagsOffset]&1 != 0 {
|
||||||
|
return "amd64"
|
||||||
|
} else {
|
||||||
|
return "386"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateTargetArchMatchesKernel validates that the packer.TargetArch
|
||||||
|
// corresponds to the kernel's architecture.
|
||||||
|
//
|
||||||
|
// See https://github.com/gokrazy/gokrazy/issues/191 for background. Maybe the
|
||||||
|
// TargetArch will become automatic in the future but for now this is a safety
|
||||||
|
// net to prevent people from bricking their appliances with the wrong userspace
|
||||||
|
// architecture.
|
||||||
|
func (pack *Pack) validateTargetArchMatchesKernel() error {
|
||||||
|
cfg := pack.Cfg
|
||||||
|
kernelDir, err := packer.PackageDir(cfg.KernelPackageOrDefault())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
kernelPath := filepath.Join(kernelDir, "vmlinuz")
|
||||||
|
k, err := os.Open(kernelPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer k.Close()
|
||||||
|
hdr := make([]byte, 1<<10) // plenty
|
||||||
|
if _, err := io.ReadFull(k, hdr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
kernelArch := kernelGoarch(hdr)
|
||||||
|
if kernelArch == "" {
|
||||||
|
return fmt.Errorf("kernel %v architecture in %s not detected", cfg.KernelPackageOrDefault(), kernelPath)
|
||||||
|
}
|
||||||
|
targetArch := packer.TargetArch()
|
||||||
|
if kernelArch != targetArch {
|
||||||
|
return fmt.Errorf("target architecture %q (GOARCH) doesn't match the %s kernel type %q",
|
||||||
|
targetArch,
|
||||||
|
cfg.KernelPackageOrDefault(),
|
||||||
|
kernelArch)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ func copyFile(fw *fat.Writer, dest string, src fs.File, srcName string) error {
|
|||||||
return src.Close()
|
return src.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func copyFileSquash(d *squashfs.Directory, dest, src string) error {
|
func copyFileSquash(d *squashfs.Directory, dest, src string, xattrs map[string][]byte) error {
|
||||||
f, err := os.Open(src)
|
f, err := os.Open(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -58,7 +58,7 @@ func copyFileSquash(d *squashfs.Directory, dest, src string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
w, err := d.File(filepath.Base(dest), st.ModTime(), st.Mode()&os.ModePerm)
|
w, err := d.File(filepath.Base(dest), st.ModTime(), st.Mode()&os.ModePerm, xattrs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -202,6 +202,18 @@ func (p *Pack) copyGlobsToBoot(fw *fat.Writer, srcDir string, globs []string) er
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Pack) writeBootFile(bootfilename, mbrfilename string) error {
|
||||||
|
f, err := os.Create(bootfilename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if err := p.writeBoot(f, mbrfilename); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Pack) writeBoot(f io.Writer, mbrfilename string) error {
|
func (p *Pack) writeBoot(f io.Writer, mbrfilename string) error {
|
||||||
log := p.Env.Logger()
|
log := p.Env.Logger()
|
||||||
log.Printf("")
|
log.Printf("")
|
||||||
@@ -254,39 +266,25 @@ func (p *Pack) writeBoot(f io.Writer, mbrfilename string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EEPROM update procedure. See also:
|
// matches must be non-empty
|
||||||
// https://news.ycombinator.com/item?id=21674550
|
bestMatch := func(matches []string) string {
|
||||||
writeEepromUpdateFile := func(globPattern, target string) (sig string, _ error) {
|
|
||||||
matches, err := filepath.Glob(globPattern)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if len(matches) == 0 {
|
|
||||||
return "", fmt.Errorf("invalid -eeprom_package: no files matching %s", filepath.Base(globPattern))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select the EEPROM file that sorts last.
|
// Select the EEPROM file that sorts last.
|
||||||
// This corresponds to most recent for the pieeprom-*.bin files,
|
// This corresponds to most recent for the pieeprom-*.bin files,
|
||||||
// which contain the date in yyyy-mm-dd format.
|
// which contain the date in yyyy-mm-dd format.
|
||||||
sort.Sort(sort.Reverse(sort.StringSlice(matches)))
|
sort.Sort(sort.Reverse(sort.StringSlice(matches)))
|
||||||
|
return matches[0]
|
||||||
f, err := os.Open(matches[0])
|
}
|
||||||
if err != nil {
|
// EEPROM update procedure. See also:
|
||||||
return "", err
|
// https://news.ycombinator.com/item?id=21674550
|
||||||
}
|
writeEepromUpdate := func(target string, modTime time.Time, r io.Reader) (sig string, _ error) {
|
||||||
defer f.Close()
|
|
||||||
st, err := f.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
// Copy the EEPROM file into the image and calculate its SHA256 hash
|
// Copy the EEPROM file into the image and calculate its SHA256 hash
|
||||||
// while doing so:
|
// while doing so:
|
||||||
w, err := fw.File(target, st.ModTime())
|
w, err := fw.File(target, modTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
h := sha256.New()
|
h := sha256.New()
|
||||||
if _, err := io.Copy(w, io.TeeReader(f, h)); err != nil {
|
if _, err := io.Copy(w, io.TeeReader(r, h)); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,33 +299,81 @@ func (p *Pack) writeBoot(f io.Writer, mbrfilename string) error {
|
|||||||
sigFn := target
|
sigFn := target
|
||||||
ext := filepath.Ext(sigFn)
|
ext := filepath.Ext(sigFn)
|
||||||
if ext == "" {
|
if ext == "" {
|
||||||
return "", fmt.Errorf("BUG: cannot derive signature file name from matches[0]=%q", matches[0])
|
return "", fmt.Errorf("BUG: cannot derive signature file name from target=%q", target)
|
||||||
}
|
}
|
||||||
sigFn = strings.TrimSuffix(sigFn, ext) + ".sig"
|
sigFn = strings.TrimSuffix(sigFn, ext) + ".sig"
|
||||||
w, err = fw.File(sigFn, st.ModTime())
|
w, err = fw.File(sigFn, modTime)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
_, err = fmt.Fprintf(w, "%x\n", h.Sum(nil))
|
_, err = fmt.Fprintf(w, "%x\n", h.Sum(nil))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
_, err = fmt.Fprintf(w, "ts: %d\n", modTime.Unix())
|
||||||
return fmt.Sprintf("%x", h.Sum(nil)), err
|
return fmt.Sprintf("%x", h.Sum(nil)), err
|
||||||
}
|
}
|
||||||
|
writeEepromUpdateFile := func(matches []string, target string) (sig string, _ error) {
|
||||||
|
f, err := os.Open(bestMatch(matches))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
st, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return writeEepromUpdate(target, st.ModTime(), f)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pieSig string
|
||||||
if eepromDir != "" {
|
if eepromDir != "" {
|
||||||
|
log.Printf("EEPROM directory: %s", eepromDir)
|
||||||
|
log.Printf("(gokrazy config mtime: %v)", p.Cfg.Meta.LastModified)
|
||||||
log.Printf("EEPROM update summary:")
|
log.Printf("EEPROM update summary:")
|
||||||
pieSig, err := writeEepromUpdateFile(filepath.Join(eepromDir, "pieeprom-*.bin"), "/pieeprom.upd")
|
eepromGlob := filepath.Join(eepromDir, "pieeprom-*.bin")
|
||||||
|
eepromMatches, err := filepath.Glob(eepromGlob)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
vlSig, err := writeEepromUpdateFile(filepath.Join(eepromDir, "vl805-*.bin"), "/vl805.bin")
|
if len(eepromMatches) == 0 {
|
||||||
|
return fmt.Errorf("invalid -eeprom_package: no files matching %s", filepath.Base(eepromGlob))
|
||||||
|
}
|
||||||
|
if ee := p.Cfg.BootloaderExtraEEPROM; len(ee) > 0 {
|
||||||
|
updated, err := applyExtraEEPROM(bestMatch(eepromMatches), ee)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pieSig, err = writeEepromUpdate("/pieeprom.upd", p.Cfg.Meta.LastModified, bytes.NewReader(updated))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pieSig, err = writeEepromUpdateFile(eepromMatches, "/pieeprom.upd")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vl805Glob := filepath.Join(eepromDir, "vl805-*.bin")
|
||||||
|
vl805Matches, err := filepath.Glob(vl805Glob)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
var vlSig string
|
||||||
|
if len(vl805Matches) > 0 {
|
||||||
|
vlSig, err = writeEepromUpdateFile(vl805Matches, "/vl805.bin")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
targetFilename := "/recovery.bin"
|
targetFilename := "/recovery.bin"
|
||||||
if pieSig == p.ExistingEEPROM.PieepromSHA256 &&
|
if pieSig == p.ExistingEEPROM.PieepromSHA256 &&
|
||||||
vlSig == p.ExistingEEPROM.VL805SHA256 {
|
vlSig == p.ExistingEEPROM.VL805SHA256 {
|
||||||
log.Printf(" installing recovery.bin as RECOVERY.000 (EEPROM already up-to-date)")
|
log.Printf(" installing recovery.bin as RECOVERY.000 (EEPROM already up-to-date)")
|
||||||
targetFilename = "/RECOVERY.000"
|
targetFilename = "/RECOVERY.000"
|
||||||
}
|
}
|
||||||
if _, err := writeEepromUpdateFile(filepath.Join(eepromDir, "recovery.bin"), targetFilename); err != nil {
|
if _, err := writeEepromUpdateFile([]string{filepath.Join(eepromDir, "recovery.bin")}, targetFilename); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,6 +443,7 @@ type FileInfo struct {
|
|||||||
FromHost string
|
FromHost string
|
||||||
FromLiteral string
|
FromLiteral string
|
||||||
SymlinkDest string
|
SymlinkDest string
|
||||||
|
Xattrs map[string][]byte
|
||||||
|
|
||||||
Dirents []*FileInfo
|
Dirents []*FileInfo
|
||||||
}
|
}
|
||||||
@@ -459,12 +506,79 @@ func (fi *FileInfo) mustFindDirent(path string) *FileInfo {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func addToFileInfo(parent *FileInfo, path string) (time.Time, error) {
|
||||||
|
entries, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return time.Time{}, nil
|
||||||
|
}
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var latestTime time.Time
|
||||||
|
for _, entry := range entries {
|
||||||
|
filename := entry.Name()
|
||||||
|
// get existing file info
|
||||||
|
var fi *FileInfo
|
||||||
|
for _, ent := range parent.Dirents {
|
||||||
|
if ent.Filename == filename {
|
||||||
|
fi = ent
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := entry.Info()
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
if info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
info, err = os.Stat(filepath.Join(path, filename))
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if latestTime.Before(info.ModTime()) {
|
||||||
|
latestTime = info.ModTime()
|
||||||
|
}
|
||||||
|
|
||||||
|
// or create if not exist
|
||||||
|
if fi == nil {
|
||||||
|
fi = &FileInfo{
|
||||||
|
Filename: filename,
|
||||||
|
Mode: info.Mode(),
|
||||||
|
}
|
||||||
|
parent.Dirents = append(parent.Dirents, fi)
|
||||||
|
} else {
|
||||||
|
// file overwrite is not supported -> return error
|
||||||
|
if !info.IsDir() || fi.FromHost != "" || fi.FromLiteral != "" {
|
||||||
|
return time.Time{}, fmt.Errorf("file already exists in filesystem: %s", filepath.Join(path, filename))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add content
|
||||||
|
if info.IsDir() {
|
||||||
|
modTime, err := addToFileInfo(fi, filepath.Join(path, filename))
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, err
|
||||||
|
}
|
||||||
|
if latestTime.Before(modTime) {
|
||||||
|
latestTime = modTime
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fi.FromHost = filepath.Join(path, filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return latestTime, nil
|
||||||
|
}
|
||||||
|
|
||||||
type foundBin struct {
|
type foundBin struct {
|
||||||
gokrazyPath string
|
gokrazyPath string
|
||||||
hostPath string
|
hostPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func findBins(cfg *config.Struct, buildEnv *packer.BuildEnv, bindir string, basenames map[string]string) (*FileInfo, []foundBin, error) {
|
func findBins(cfg *config.Struct, buildEnv *packer.BuildEnv, bindir string, basenames map[string]string, xattrs map[string]map[string][]byte) (*FileInfo, []foundBin, error) {
|
||||||
var found []foundBin
|
var found []foundBin
|
||||||
result := FileInfo{Filename: ""}
|
result := FileInfo{Filename: ""}
|
||||||
|
|
||||||
@@ -519,15 +633,20 @@ func findBins(cfg *config.Struct, buildEnv *packer.BuildEnv, bindir string, base
|
|||||||
}
|
}
|
||||||
user := FileInfo{Filename: "user"}
|
user := FileInfo{Filename: "user"}
|
||||||
for _, pkg := range mainPkgs {
|
for _, pkg := range mainPkgs {
|
||||||
|
xattr := make(map[string][]byte)
|
||||||
basename := pkg.Basename()
|
basename := pkg.Basename()
|
||||||
if basenameOverride, ok := basenames[pkg.ImportPath]; ok {
|
if basenameOverride, ok := basenames[pkg.ImportPath]; ok {
|
||||||
basename = basenameOverride
|
basename = basenameOverride
|
||||||
}
|
}
|
||||||
|
if xattrsOverride, ok := xattrs[pkg.ImportPath]; ok {
|
||||||
|
xattr = xattrsOverride
|
||||||
|
}
|
||||||
binPath := filepath.Join(bindir, basename)
|
binPath := filepath.Join(bindir, basename)
|
||||||
fileIsELFOrFatal(binPath)
|
fileIsELFOrFatal(binPath)
|
||||||
user.Dirents = append(user.Dirents, &FileInfo{
|
user.Dirents = append(user.Dirents, &FileInfo{
|
||||||
Filename: basename,
|
Filename: basename,
|
||||||
FromHost: binPath,
|
FromHost: binPath,
|
||||||
|
Xattrs: xattr,
|
||||||
})
|
})
|
||||||
found = append(found, foundBin{
|
found = append(found, foundBin{
|
||||||
gokrazyPath: "/user/" + basename,
|
gokrazyPath: "/user/" + basename,
|
||||||
@@ -540,14 +659,14 @@ func findBins(cfg *config.Struct, buildEnv *packer.BuildEnv, bindir string, base
|
|||||||
|
|
||||||
func writeFileInfo(dir *squashfs.Directory, fi *FileInfo) error {
|
func writeFileInfo(dir *squashfs.Directory, fi *FileInfo) error {
|
||||||
if fi.FromHost != "" { // copy a regular file
|
if fi.FromHost != "" { // copy a regular file
|
||||||
return copyFileSquash(dir, fi.Filename, fi.FromHost)
|
return copyFileSquash(dir, fi.Filename, fi.FromHost, fi.Xattrs)
|
||||||
}
|
}
|
||||||
if fi.FromLiteral != "" { // write a regular file
|
if fi.FromLiteral != "" { // write a regular file
|
||||||
mode := fi.Mode
|
mode := fi.Mode
|
||||||
if mode == 0 {
|
if mode == 0 {
|
||||||
mode = 0444
|
mode = 0444
|
||||||
}
|
}
|
||||||
w, err := dir.File(fi.Filename, time.Now(), mode)
|
w, err := dir.File(fi.Filename, time.Now(), mode, fi.Xattrs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -578,6 +697,18 @@ func writeFileInfo(dir *squashfs.Directory, fi *FileInfo) error {
|
|||||||
return d.Flush()
|
return d.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Pack) writeRootFile(filename string, root *FileInfo) error {
|
||||||
|
f, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
if err := p.writeRoot(f, root); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Pack) writeRoot(f io.WriteSeeker, root *FileInfo) error {
|
func (p *Pack) writeRoot(f io.WriteSeeker, root *FileInfo) error {
|
||||||
log := p.Env.Logger()
|
log := p.Env.Logger()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user