Add licensing and distribution metadata

This commit is contained in:
ms2k 2026-08-20 23:01:10 +09:00
commit 5fb0c9c57b
11 changed files with 9231 additions and 63 deletions

10
.dockerignore Normal file
View file

@ -0,0 +1,10 @@
.git
dist
dist-newstyle
docs/journal
scratchpad
tests/kind/.runtime
web/dist
web/node_modules
web/playwright-report
web/test-results

View file

@ -18,7 +18,7 @@ RUN apt-get update \
WORKDIR /src WORKDIR /src
COPY nuthatch.cabal cabal.project ./ COPY nuthatch.cabal cabal.project cabal.project.freeze LICENSE ./
RUN cabal update \ RUN cabal update \
&& cabal build --only-dependencies all && cabal build --only-dependencies all
@ -57,6 +57,9 @@ RUN apt-get update \
WORKDIR /app WORKDIR /app
COPY sql ./sql COPY sql ./sql
COPY --from=frontend /web/dist ./web/dist COPY --from=frontend /web/dist ./web/dist
COPY LICENSE THIRD_PARTY_NOTICES.txt /usr/share/doc/nuthatch/
RUN dpkg-query --show --showformat='${binary:Package}\t${Version}\n' \
| sort > /usr/share/doc/nuthatch/debian-packages.txt
USER 10001:10001 USER 10001:10001
FROM runtime-base AS test-runtime FROM runtime-base AS test-runtime

26
LICENSE Normal file
View file

@ -0,0 +1,26 @@
Copyright 2026 ms2k
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 COPYRIGHT HOLDER OR CONTRIBUTORS 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.

View file

@ -1,71 +1,76 @@
.PHONY: test check-dependencies check-fixture-lib check-kind-matrix check-storage-foundation check-auth check-app check-app-boundaries check-streaming check-idempotency check-recovery check-model-correspondence check-protocol check-product check-browser check-runtime check-v1 check-specs check-web-contract generate-web-contract app-up app-down test-frontend check-frontend build-frontend .PHONY: test check-dependencies check-fixture-lib check-kind-matrix check-storage-foundation check-auth check-app check-app-boundaries check-streaming check-idempotency check-recovery check-model-correspondence check-protocol check-product check-browser check-runtime check-v1 check-specs check-web-contract generate-web-contract third-party-notices app-up app-down test-frontend check-frontend build-frontend
test: test:
./tests/run-host-tests.sh bash ./tests/run-host-tests.sh
check-dependencies: check-dependencies:
./tests/kind/run.sh bash ./tests/kind/run.sh
check-fixture-lib: check-fixture-lib:
./tests/kind/lib-test.sh bash ./tests/kind/lib-test.sh
check-kind-matrix: check-kind-matrix:
./tests/kind/matrix-run.sh bash ./tests/kind/matrix-run.sh
check-storage-foundation: check-storage-foundation:
./tests/kind/storage-foundation-run.sh bash ./tests/kind/storage-foundation-run.sh
check-auth: check-auth:
./tests/kind/auth-run.sh bash ./tests/kind/auth-run.sh
check-app: check-app:
./tests/kind/auth-run.sh bash ./tests/kind/auth-run.sh
check-app-boundaries: check-app-boundaries:
./tests/application-boundaries.sh bash ./tests/application-boundaries.sh
check-streaming: check-streaming:
./tests/kind/streaming-run.sh bash ./tests/kind/streaming-run.sh
check-idempotency: check-idempotency:
./tests/kind/idempotency-run.sh bash ./tests/kind/idempotency-run.sh
check-recovery: check-recovery:
./tests/kind/recovery-run.sh bash ./tests/kind/recovery-run.sh
check-model-correspondence: check-model-correspondence:
./tests/model-correspondence/run.sh bash ./tests/model-correspondence/run.sh
check-protocol: check-model-correspondence check-protocol: check-model-correspondence
./tests/kind/protocol-conformance-run.sh bash ./tests/kind/protocol-conformance-run.sh
check-product: test-frontend check-frontend build-frontend check-web-contract check-product: test-frontend check-frontend build-frontend check-web-contract
./tests/kind/product-run.sh bash ./tests/kind/product-run.sh
check-browser: test-frontend check-frontend build-frontend check-browser: test-frontend check-frontend build-frontend
./tests/kind/browser-run.sh bash ./tests/kind/browser-run.sh
check-web-contract: check-web-contract:
./tests/web-contract.sh bash ./tests/web-contract.sh
generate-web-contract: generate-web-contract:
cabal run nuthatch-generate-web-contract --offline -- web/src/generated/contract.ts web/test/fixtures/contract.json cabal run nuthatch-generate-web-contract --offline -- web/src/generated/contract.ts web/test/fixtures/contract.json
third-party-notices:
cabal build exe:nuthatch --dry-run --offline --builddir=dist-newstyle/third-party
npm --prefix web ci
node scripts/generate-third-party-notices.mjs dist-newstyle/third-party/cache/plan.json web/package-lock.json web/node_modules THIRD_PARTY_NOTICES.txt
check-runtime: test check-recovery check-runtime: test check-recovery
./tests/runtime-static.sh bash ./tests/runtime-static.sh
./tests/kind/runtime-resilience-run.sh bash ./tests/kind/runtime-resilience-run.sh
check-v1: check-v1:
./tests/check-v1.sh bash ./tests/check-v1.sh
check-specs: check-specs:
./tests/spec-contracts.sh bash ./tests/spec-contracts.sh
app-up: app-up:
NUTHATCH_APP_INTERACTIVE=1 ./tests/kind/auth-run.sh NUTHATCH_APP_INTERACTIVE=1 bash ./tests/kind/auth-run.sh
app-down: app-down:
./tests/kind/destroy.sh bash ./tests/kind/destroy.sh
test-frontend: test-frontend:
npm --prefix web test npm --prefix web test

32
README.md Normal file
View file

@ -0,0 +1,32 @@
# Nuthatch
Small web UI for personal and shared S3-compatible storage.
Made for a personal homelab. Shared as-is.
## Build and test
Install the [required tools](docs/spec/testing.md), then run:
```sh
cabal build exe:nuthatch
make test
npm --prefix web ci
make test-frontend check-frontend build-frontend
```
Full check, including local kind clusters:
```sh
make check-v1
```
## Docs
- [Behavior and design](docs/spec/nuthatch.md)
- [Configuration and interfaces](docs/spec/contracts.md)
- [Mutation protocol](docs/protocol/Nuthatch.tla)
## License
[BSD 3-Clause](LICENSE). See [`THIRD_PARTY_NOTICES.txt`](THIRD_PARTY_NOTICES.txt) for bundled dependencies.

8474
THIRD_PARTY_NOTICES.txt Normal file

File diff suppressed because it is too large Load diff

308
cabal.project.freeze Normal file
View file

@ -0,0 +1,308 @@
active-repositories: hackage.haskell.org:merge
constraints: any.Cabal ==3.12.1.0,
any.Cabal-syntax ==3.12.1.0,
any.HTTP ==4000.5.0,
HTTP -conduit10 -warn-as-error -warp-tests,
any.HUnit ==1.6.2.0,
any.OneTuple ==0.4.3,
OneTuple +base-ge-4-15 +base-ge-4-16,
any.Only ==0.1,
any.QuickCheck ==2.16.0.0,
QuickCheck -old-random +templatehaskell,
any.StateVar ==1.2.2,
any.adjunctions ==4.4.4,
any.aeson ==2.2.5.0,
aeson +ordered-keymap,
any.ansi-terminal ==1.1.5,
ansi-terminal -example,
any.ansi-terminal-types ==1.1.3,
any.appar ==0.1.8,
any.array ==0.5.8.0,
any.assoc ==1.1.1,
assoc -tagged,
any.async ==2.2.6,
async -bench -debug-auto-label,
any.attoparsec ==0.14.4,
attoparsec -developer,
any.attoparsec-aeson ==2.2.2.0,
any.auto-update ==0.2.6,
any.aws ==0.25.2,
aws -examples,
any.barbies ==2.1.1.0,
any.base ==4.20.2.0,
any.base-compat ==0.15.0,
any.base-orphans ==0.9.4,
any.base16 ==1.0,
any.base16-bytestring ==1.0.2.0,
any.base64 ==1.0,
any.base64-bytestring ==1.2.1.0,
any.basement ==0.0.16,
any.bifunctors ==5.6.3,
bifunctors +tagged,
any.binary ==0.8.9.3,
any.bitvec ==1.1.6.0,
bitvec +simd,
any.blaze-builder ==0.4.4.1,
any.blaze-html ==0.9.2.0,
any.blaze-markup ==0.8.3.0,
any.boring ==0.2.2.1,
boring +tagged,
any.bsb-http-chunked ==0.0.0.4,
any.byteable ==0.1.1,
any.byteorder ==1.0.4,
any.bytestring ==0.12.2.0,
any.bytestring-lexing ==0.5.0.15,
any.cabal-doctest ==1.0.12,
any.call-stack ==0.4.0,
any.case-insensitive ==1.2.1.0,
any.cborg ==0.2.10.0,
cborg +optimize-gmp,
any.cereal ==0.5.8.3,
cereal -bytestring-builder,
any.character-ps ==0.1,
any.clock ==0.8.4,
clock -llvm,
any.colour ==2.3.7,
any.comonad ==5.0.10,
comonad +containers +distributive +indexed-traversable,
any.concise ==0.1.0.1,
any.concurrent-output ==1.10.21,
any.conduit ==1.3.6.1,
any.conduit-extra ==1.3.8,
any.constraints ==0.14.4,
any.containers ==0.7,
any.contravariant ==1.5.6,
contravariant +statevar,
any.cookie ==0.5.1,
any.crypto-token ==0.1.2,
any.cryptohash-md5 ==0.11.101.0,
any.cryptohash-sha1 ==0.11.101.0,
any.crypton ==1.0.6,
crypton -check_alignment +integer-gmp -old_toolchain_inliner +support_aesni +support_deepseq +support_pclmuldq +support_rdrand -support_sse +use_target_attributes,
any.crypton-asn1-encoding ==0.10.0,
any.crypton-asn1-parse ==0.10.0,
any.crypton-asn1-types ==0.4.1,
any.crypton-connection ==0.4.5,
any.crypton-pem ==0.3.0,
any.crypton-socks ==0.6.2,
crypton-socks -example +network-3-0-0-0,
any.crypton-x509 ==1.8.0,
any.crypton-x509-store ==1.8.0,
any.crypton-x509-system ==1.8.0,
any.crypton-x509-validation ==1.8.0,
any.data-default ==0.8.0.2,
any.data-default-class ==0.2.0.0,
any.data-fix ==0.3.4,
any.dec ==0.0.6,
any.deepseq ==1.5.0.0,
any.directory ==1.3.8.5,
any.distributive ==0.6.3,
distributive +tagged,
any.dlist ==1.0,
dlist -werror,
any.easy-file ==0.2.5,
any.ech-config ==0.0.1,
ech-config -devel,
any.entropy ==0.4.1.11,
entropy -donotgetentropy,
any.envparse ==0.6.0,
any.erf ==2.0.0.0,
any.errors ==2.3.0,
any.exceptions ==0.10.9,
any.fail ==4.9.0.0,
any.fast-logger ==3.2.6,
any.file-embed ==0.0.16.0,
any.filepath ==1.5.4.0,
any.free ==5.2,
any.generics-sop ==0.5.1.4,
any.ghc-bignum ==1.3,
any.ghc-boot-th ==9.10.3,
any.ghc-internal ==9.1003.0,
any.ghc-prim ==0.12.0,
any.half ==0.3.3,
any.happy ==2.2,
any.happy-lib ==2.2,
any.hashable ==1.5.1.0,
hashable -arch-native -random-initial-seed,
any.haskell-lexer ==1.2.1,
any.hedgehog ==1.7,
any.hedis ==0.15.2,
hedis -dev,
any.hpke ==0.0.0,
any.hsc2hs ==0.68.10,
hsc2hs -in-ghc-tree,
any.http-api-data ==0.7,
http-api-data -use-text-show,
any.http-client ==0.7.19,
http-client +network-uri,
any.http-client-tls ==0.3.6.4,
any.http-conduit ==2.3.9.1,
http-conduit +aeson,
any.http-date ==0.0.11,
any.http-media ==0.8.1.1,
any.http-semantics ==0.4.1,
any.http-types ==0.12.5,
any.http2 ==5.4.3,
http2 -devel -h2spec,
any.indexed-traversable ==0.1.5,
indexed-traversable +base-ge-4-18,
any.indexed-traversable-instances ==0.1.2.1,
any.integer-conversion ==0.1.1,
any.integer-gmp ==1.1,
any.integer-logarithms ==1.0.5,
integer-logarithms -check-bounds +integer-gmp,
any.invariant ==0.6.5,
any.iproute ==1.7.15,
any.jose ==0.12,
jose -demos,
any.kan-extensions ==5.2.8,
any.lens ==5.3.6,
lens -benchmark-uniplate -dump-splices +inlining -j +test-hunit +test-properties +test-templates +trustworthy,
any.lifted-async ==0.11.0,
any.lifted-base ==0.2.3.12,
any.megaparsec ==9.8.1,
megaparsec -dev,
any.memory ==0.18.0,
memory +support_bytestring +support_deepseq,
any.mime-types ==0.1.2.2,
any.mmorph ==1.2.2,
any.modern-uri ==0.3.6.1,
modern-uri -dev,
any.monad-control ==1.0.3.1,
any.monad-time ==0.4.0.0,
any.mono-traversable ==1.0.21.0,
any.mtl ==2.3.1,
any.mtl-compat ==0.2.2,
mtl-compat -two-point-one -two-point-two,
any.network ==3.2.8.0,
network -devel,
any.network-bsd ==2.8.1.0,
any.network-byte-order ==0.1.8,
any.network-control ==0.1.7,
any.network-info ==0.2.1,
any.network-uri ==2.6.4.2,
any.old-locale ==1.0.0.7,
any.old-time ==1.1.1.0,
any.optparse-applicative ==0.19.0.0,
optparse-applicative +process,
any.os-string ==2.0.7,
any.parallel ==3.3.0.0,
any.parsec ==3.1.18.0,
any.parser-combinators ==1.3.1,
parser-combinators -dev,
any.postgresql-libpq ==0.11.0.0,
postgresql-libpq -use-pkg-config,
any.postgresql-libpq-configure ==0.11,
any.postgresql-migration ==0.2.1.8,
any.postgresql-simple ==0.7.0.1,
any.pretty ==1.1.3.6,
any.pretty-show ==1.10,
any.prettyprinter ==1.7.2,
prettyprinter -buildreadme +text,
any.prettyprinter-ansi-terminal ==1.1.4,
prettyprinter-ansi-terminal +text,
any.primitive ==0.9.1.0,
any.process ==1.6.26.1,
any.profunctors ==5.6.3,
any.psqueues ==0.2.8.3,
any.random ==1.3.1,
any.recv ==0.1.1,
any.reflection ==2.1.9,
reflection -slow +template-haskell,
any.resource-pool ==0.4.0.0,
any.resourcet ==1.3.0,
any.retry ==0.9.3.1,
retry -lib-werror,
any.rts ==1.0.2,
any.safe ==0.3.21,
any.safe-exceptions ==0.1.7.4,
any.scanner ==0.3.1,
any.scientific ==0.3.8.1,
scientific -integer-simple,
any.semialign ==1.4,
semialign +semigroupoids,
any.semigroupoids ==6.0.2,
semigroupoids +comonad +containers +contravariant +tagged +unordered-containers,
any.serialise ==0.2.6.1,
serialise +newtime15,
any.servant ==0.20.3.0,
any.servant-server ==0.20.3.0,
any.simple-sendfile ==0.2.32,
simple-sendfile +allow-bsd -fallback,
any.singleton-bool ==0.1.8,
any.some ==1.1,
some +newtype-unsafe,
any.sop-core ==0.5.0.2,
any.split ==0.2.5,
any.splitmix ==0.1.3.2,
splitmix -optimised-mixer,
any.stm ==2.5.3.1,
any.streaming-commons ==0.2.3.1,
streaming-commons -use-bytestring-builder,
any.strict ==0.5.1,
any.tagged ==0.8.10,
tagged +deepseq +template-haskell,
any.tasty ==1.5.4,
tasty +unix,
any.tasty-hedgehog ==1.4.0.2,
any.tasty-hunit ==0.10.2,
any.template-haskell ==2.22.0.0,
any.temporary ==1.3,
any.terminal-size ==0.3.4,
any.text ==2.1.3,
any.text-iso8601 ==0.1.1.1,
any.text-short ==0.1.6.1,
text-short -asserts,
any.th-abstraction ==0.7.2.0,
any.th-compat ==0.1.7,
any.these ==1.2.1,
any.time ==1.12.2,
any.time-compat ==1.9.9,
any.time-hourglass ==0.3.0,
any.time-manager ==0.3.2,
any.tls ==2.2.2,
tls -devel,
any.tls-session-manager ==0.0.9,
any.transformers ==0.6.1.1,
any.transformers-base ==0.4.6.1,
transformers-base +orphaninstances,
any.transformers-compat ==0.7.2,
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
any.typed-process ==0.2.13.0,
any.unicode-data ==0.8.0,
unicode-data -dev-has-icu,
any.unicode-transforms ==0.4.0.1,
unicode-transforms -bench-show -dev -has-icu -has-llvm -use-gauge,
any.unix ==2.8.7.0,
any.unix-compat ==0.7.4.1,
any.unix-time ==0.4.17,
any.unliftio-core ==0.2.1.0,
any.unordered-containers ==0.2.21,
unordered-containers -debug,
any.utf8-string ==1.0.2,
any.uuid ==1.3.16.1,
any.uuid-types ==1.0.6.1,
any.vault ==0.3.2.0,
vault +useghc,
any.vector ==0.13.2.0,
vector +boundschecks -internalchecks -unsafechecks -wall,
any.vector-algorithms ==0.9.1.0,
vector-algorithms +bench +boundschecks -internalchecks -llvm -unsafechecks,
any.vector-stream ==0.1.0.1,
any.wai ==3.2.4,
any.wai-app-static ==3.2.1,
wai-app-static -print,
any.wai-extra ==3.1.18,
wai-extra -build-example,
any.wai-logger ==2.5.0,
any.warp ==3.4.14,
warp +allow-sendfilefd +include-warp-version -network-bytestring -warp-debug +x509,
any.warp-tls ==3.4.14,
any.witherable ==0.5,
any.wl-pprint-annotated ==0.1.0.1,
any.word8 ==0.1.3,
any.xml-conduit ==1.10.1.0,
any.xml-types ==0.3.8,
any.zlib ==0.7.1.1,
zlib -bundled-c-zlib +non-blocking-ffi +pkg-config
index-state: hackage.haskell.org 2026-07-17T09:45:02Z

View file

@ -2,18 +2,69 @@ cabal-version: 3.0
name: nuthatch name: nuthatch
version: 0.1.0.0 version: 0.1.0.0
synopsis: User-facing S3 BFF synopsis: User-facing S3 BFF
license: NONE description:
author: Nuthatch A user-facing web application and API for small S3-compatible object stores.
maintainer: nuthatch@localhost Nuthatch gives OIDC-authenticated users identity-scoped personal roots and a
shared root while keeping object-store credentials behind the service.
license: BSD-3-Clause
license-file: LICENSE
copyright: 2026 ms2k
author: ms2k
maintainer: [email protected]
homepage: https://code.quorate.dev/ms2k/nuthatch
bug-reports: mailto:[email protected]
tested-with: GHC == 9.10.3
category: Network category: Network
build-type: Simple build-type: Simple
extra-doc-files:
README.md
extra-source-files: extra-source-files:
.dockerignore
Dockerfile
Makefile
THIRD_PARTY_NOTICES.txt
cabal.project
cabal.project.freeze
deploy/kubernetes/*.yaml.in
docs/protocol/*.cfg
docs/protocol/*.tla
docs/spec/*.md
sql/001_initial_schema.sql sql/001_initial_schema.sql
web/src/generated/contract.ts scripts/*.mjs
web/src/transport.ts tests/*.sh
web/test/contract-integration.mjs tests/kind/*.env
web/test/fixtures/contract.json tests/kind/*.sh
web/test/fixtures/upload-content-sha256.json tests/kind/*.yaml
tests/kind/*.yaml.in
tests/kind/fixture/*.sh
tests/kind/provider/nerdctl
tests/model-correspondence/*.cfg
tests/model-correspondence/*.env
tests/model-correspondence/*.json
tests/model-correspondence/*.mjs
tests/model-correspondence/*.sh
web/.node-version
web/index.html
web/package-lock.json
web/package.json
web/playwright.config.ts
web/src/*.css
web/src/*.svelte
web/src/*.ts
web/src/components/*.svelte
web/src/generated/*.ts
web/src/workflows/*.ts
web/svelte.config.js
web/test/*.mjs
web/test/browser/*.ts
web/test/components/*.ts
web/test/fixtures/*.json
web/tsconfig.json
web/vite.config.ts
source-repository head
type: git
location: https://code.quorate.dev/ms2k/nuthatch
common shared common shared
default-language: Haskell2010 default-language: Haskell2010
@ -100,7 +151,7 @@ common shared
, warp-tls >= 3.4 && < 3.5 , warp-tls >= 3.4 && < 3.5
, xml-conduit >= 1.9 && < 1.11 , xml-conduit >= 1.9 && < 1.11
library library nuthatch-internal
import: shared import: shared
hs-source-dirs: src hs-source-dirs: src
exposed-modules: exposed-modules:
@ -137,7 +188,7 @@ executable nuthatch
main-is: Main.hs main-is: Main.hs
hs-source-dirs: app hs-source-dirs: app
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, base , base
ghc-options: ghc-options:
-threaded -threaded
@ -149,7 +200,7 @@ executable nuthatch-generate-web-contract
main-is: GenerateContract.hs main-is: GenerateContract.hs
hs-source-dirs: app hs-source-dirs: app
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, base , base
executable nuthatch-web-contract-integration executable nuthatch-web-contract-integration
@ -157,10 +208,10 @@ executable nuthatch-web-contract-integration
main-is: WebContractIntegration.hs main-is: WebContractIntegration.hs
hs-source-dirs: test hs-source-dirs: test
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, base , base
, bytestring , bytestring
, process , process >= 1.6 && < 1.7
, text , text
ghc-options: ghc-options:
-threaded -threaded
@ -170,7 +221,7 @@ executable nuthatch-storage-integration
main-is: Integration.hs main-is: Integration.hs
hs-source-dirs: test hs-source-dirs: test
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, base , base
, async , async
, bytestring , bytestring
@ -185,7 +236,7 @@ executable nuthatch-recovery-probe
main-is: RecoveryPhase.hs main-is: RecoveryPhase.hs
hs-source-dirs: test hs-source-dirs: test
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, base , base
, bytestring , bytestring
, text , text
@ -201,7 +252,7 @@ executable nuthatch-protocol-conformance
other-modules: other-modules:
Nuthatch.ModelEvidence Nuthatch.ModelEvidence
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, base , base
, bytestring , bytestring
, crypton , crypton
@ -220,7 +271,7 @@ executable nuthatch-model-evidence
other-modules: other-modules:
Nuthatch.ModelEvidence Nuthatch.ModelEvidence
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, aeson , aeson
, base , base
, text , text
@ -230,7 +281,7 @@ executable nuthatch-runtime-probe
main-is: RuntimeProbe.hs main-is: RuntimeProbe.hs
hs-source-dirs: test hs-source-dirs: test
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, base , base
ghc-options: ghc-options:
-threaded -threaded
@ -248,13 +299,13 @@ executable nuthatch-auth-probe
Nuthatch.Probe.Oidc Nuthatch.Probe.Oidc
Nuthatch.Probe.ProductClient Nuthatch.Probe.ProductClient
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, aeson , aeson
, base , base
, bytestring , bytestring
, crypton , crypton
, memory , memory
, process , process >= 1.6 && < 1.7
, temporary , temporary
ghc-options: ghc-options:
-threaded -threaded
@ -271,13 +322,13 @@ executable nuthatch-product-probe
Nuthatch.Probe.Oidc Nuthatch.Probe.Oidc
Nuthatch.Probe.ProductClient Nuthatch.Probe.ProductClient
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, aeson , aeson
, base , base
, bytestring , bytestring
, crypton , crypton
, memory , memory
, process , process >= 1.6 && < 1.7
, temporary , temporary
ghc-options: ghc-options:
-threaded -threaded
@ -295,14 +346,14 @@ executable nuthatch-idempotency-probe
Nuthatch.Probe.Oidc Nuthatch.Probe.Oidc
Nuthatch.Probe.ProductClient Nuthatch.Probe.ProductClient
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, aeson , aeson
, async , async
, base , base
, bytestring , bytestring
, crypton , crypton
, memory , memory
, process , process >= 1.6 && < 1.7
, temporary , temporary
ghc-options: ghc-options:
-threaded -threaded
@ -320,7 +371,7 @@ executable nuthatch-streaming-probe
Nuthatch.Probe.Oidc Nuthatch.Probe.Oidc
Nuthatch.Probe.ProductClient Nuthatch.Probe.ProductClient
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, aeson , aeson
, async , async
, base , base
@ -328,7 +379,7 @@ executable nuthatch-streaming-probe
, crypton , crypton
, directory , directory
, memory , memory
, process , process >= 1.6 && < 1.7
, temporary , temporary
ghc-options: ghc-options:
-threaded -threaded
@ -345,13 +396,13 @@ executable nuthatch-runtime-http-probe
Nuthatch.Probe.Oidc Nuthatch.Probe.Oidc
Nuthatch.Probe.ProductClient Nuthatch.Probe.ProductClient
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, aeson , aeson
, base , base
, bytestring , bytestring
, crypton , crypton
, memory , memory
, process , process >= 1.6 && < 1.7
, temporary , temporary
ghc-options: ghc-options:
-threaded -threaded
@ -365,7 +416,7 @@ executable nuthatch-runtime-audit-probe
other-modules: other-modules:
Nuthatch.Probe.Assert Nuthatch.Probe.Assert
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, aeson , aeson
, base , base
, bytestring , bytestring
@ -379,13 +430,13 @@ executable nuthatch-fixture-probe
Nuthatch.Probe.Assert Nuthatch.Probe.Assert
Nuthatch.Probe.Http Nuthatch.Probe.Http
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, base , base
, bytestring , bytestring
, crypton , crypton
, memory , memory
, postgresql-simple , postgresql-simple
, process , process >= 1.6 && < 1.7
, temporary , temporary
, text , text
@ -398,12 +449,12 @@ executable nuthatch-dependency-probe
Nuthatch.Probe.Curl Nuthatch.Probe.Curl
Nuthatch.Probe.Http Nuthatch.Probe.Http
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, base , base
, bytestring , bytestring
, hedis , hedis
, postgresql-simple , postgresql-simple
, process , process >= 1.6 && < 1.7
, temporary , temporary
executable nuthatch-browser-session-fixture executable nuthatch-browser-session-fixture
@ -411,7 +462,7 @@ executable nuthatch-browser-session-fixture
main-is: BrowserSessionFixture.hs main-is: BrowserSessionFixture.hs
hs-source-dirs: test hs-source-dirs: test
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, aeson , aeson
, base , base
, text , text
@ -436,7 +487,7 @@ test-suite nuthatch-unit
Nuthatch.Test.Streaming Nuthatch.Test.Streaming
Nuthatch.Test.UploadIdentityFixture Nuthatch.Test.UploadIdentityFixture
build-depends: build-depends:
, nuthatch , nuthatch:nuthatch-internal
, base , base
, bytestring , bytestring
, hedgehog >= 1.7 && < 1.8 , hedgehog >= 1.7 && < 1.8

View file

@ -0,0 +1,252 @@
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { basename, dirname, join, resolve } from 'node:path';
const [planPath, lockPath, nodeModulesPath, outputPath] = process.argv.slice(2);
if ([planPath, lockPath, nodeModulesPath, outputPath].some((value) => value === undefined)) {
throw new Error('usage: generate-third-party-notices.mjs PLAN LOCK NODE_MODULES OUTPUT');
}
const plan = readJson(planPath);
const lock = readJson(lockPath);
const cabalCache = fieldFromCommand('cabal', ['path'], 'cache-home');
const ghcLibDir = run('ghc', ['--print-libdir']).trim();
const ghcRoot = resolve(ghcLibDir, '../../..');
const ghcDocParent = join(ghcRoot, 'share', 'doc');
const ghcDocRootName = readdirSync(ghcDocParent).find((name) => name.endsWith(`-ghc-${compilerVersion(plan)}`));
if (ghcDocRootName === undefined) throw new Error('GHC package license directory not found');
const ghcDocRoot = join(ghcDocParent, ghcDocRootName);
const haskellPackages = runtimePackages(plan['install-plan'])
.map((pkg) => haskellNotice(pkg, cabalCache, ghcDocRoot, compilerVersion(plan)))
.sort(comparePackage);
const npmPackages = npmProductionPackages(lock)
.map((pkg) => npmNotice(pkg, nodeModulesPath))
.sort(comparePackage);
validateLicenses([...haskellPackages, ...npmPackages]);
const sections = [
'NUTHATCH THIRD-PARTY NOTICES',
'',
'This file is generated by scripts/generate-third-party-notices.mjs.',
'It conservatively covers the Haskell dependency plan selected for exe:nuthatch',
'and every non-development package in the locked browser dependency closure.',
'',
'The runtime image separately records its installed Debian packages in',
'/usr/share/doc/nuthatch/debian-packages.txt. Their copyright and license',
'materials remain in the image under /usr/share/doc.',
'',
'Container images used only by the test fixture are separate programs and are',
'not redistributed as part of Nuthatch. Their licenses remain their own.',
'',
inventory('HASKELL INVENTORY', haskellPackages),
inventory('BROWSER PRODUCTION DEPENDENCY INVENTORY', npmPackages),
'LICENSE AND NOTICE TEXTS',
...haskellPackages.flatMap(renderNotice),
...npmPackages.flatMap(renderNotice)
];
writeFileSync(outputPath, `${sections.join('\n').replace(/\n+$/, '')}\n`);
function readJson(path) {
return JSON.parse(readFileSync(path, 'utf8'));
}
function run(command, args) {
return execFileSync(command, args, { encoding: 'utf8' });
}
function fieldFromCommand(command, args, field) {
const prefix = `${field}:`;
const line = run(command, args).split('\n').find((candidate) => candidate.startsWith(prefix));
if (line === undefined) throw new Error(`${field} not reported by ${command}`);
return line.slice(prefix.length).trim();
}
function compilerVersion(planValue) {
const match = /^ghc-(.+)$/.exec(planValue['compiler-id']);
if (match === null) throw new Error(`unsupported compiler id ${planValue['compiler-id']}`);
return match[1];
}
function uniquePackages(packages) {
const unique = new Map();
for (const pkg of packages) {
const key = `${pkg['pkg-name']}\u0000${pkg['pkg-version']}`;
const existing = unique.get(key);
if (existing !== undefined && existing.type !== pkg.type) {
throw new Error(`inconsistent package source for ${pkg['pkg-name']}-${pkg['pkg-version']}`);
}
unique.set(key, pkg);
}
return [...unique.values()];
}
function runtimePackages(packages) {
const byId = new Map(packages.map((pkg) => [pkg.id, pkg]));
const root = packages.find((pkg) => pkg['pkg-name'] === 'nuthatch' && pkg['component-name'] === 'exe:nuthatch');
if (root === undefined) throw new Error('exe:nuthatch not found in Cabal plan');
const selected = [];
const visited = new Set();
const pending = [...(root.depends ?? [])];
while (pending.length > 0) {
const id = pending.pop();
if (visited.has(id)) continue;
visited.add(id);
const pkg = byId.get(id);
if (pkg === undefined) throw new Error(`Cabal dependency ${id} not found in plan`);
pending.push(...(pkg.depends ?? []));
if (pkg['pkg-name'] !== 'nuthatch') selected.push(pkg);
}
return uniquePackages(selected);
}
function haskellNotice(pkg, cacheRoot, docRoot, ghcVersion) {
const name = pkg['pkg-name'];
const version = pkg['pkg-version'];
if (pkg.type === 'configured') {
const archive = join(cacheRoot, 'packages', 'hackage.haskell.org', name, version, `${name}-${version}.tar.gz`);
if (!existsSync(archive)) {
run('cabal', ['get', `${name}-${version}`, '--destdir=dist-newstyle/third-party/license-sources']);
}
if (!existsSync(archive)) throw new Error(`Hackage archive not found after fetch: ${archive}`);
const entries = run('tar', ['-tzf', archive]).trim().split('\n');
const cabalEntry = entries.find((entry) => dirname(entry) === `${name}-${version}` && entry.endsWith('.cabal'));
if (cabalEntry === undefined) throw new Error(`Cabal metadata not found in ${archive}`);
const cabalText = run('tar', ['-xOzf', archive, cabalEntry]);
const licenseValue = /^license:\s*(\S.*?)\s*$/im.exec(cabalText)?.[1];
if (licenseValue === undefined) throw new Error(`license field not found for ${name}-${version}`);
const license = normalizeLicense(licenseValue);
const documents = archiveDocuments(archive, entries, `${name}-${version}`);
if (documents.length === 0) {
documents.push({
name: 'PACKAGE METADATA',
text: `The source archive supplies no standalone license or notice file.\nIts Cabal metadata declares ${license}.\n`
});
}
return {
ecosystem: 'Haskell',
name,
version,
license,
documents
};
}
if (pkg.type === 'pre-existing') {
const packageId = `${name}-${version}`;
const licenseOutput = run('ghc-pkg', ['--global', 'field', packageId, 'license']);
const licenseValue = /^license:\s*(\S.*?)\s*$/im.exec(licenseOutput)?.[1];
if (licenseValue === undefined) throw new Error(`GHC package license not found for ${packageId}`);
const license = normalizeLicense(licenseValue);
const packageLicense = join(docRoot, packageId, 'LICENSE');
const fallbackLicense = join(docRoot, `ghc-${ghcVersion}`, 'LICENSE');
const licensePath = existsSync(packageLicense) ? packageLicense : fallbackLicense;
if (!existsSync(licensePath)) throw new Error(`license text not found for ${packageId}`);
return {
ecosystem: 'Haskell',
name,
version,
license,
documents: [{ name: 'LICENSE', text: readFileSync(licensePath, 'utf8') }]
};
}
throw new Error(`unsupported Cabal plan package type ${pkg.type}`);
}
function archiveDocuments(archive, entries, root) {
const candidates = entries.filter((entry) => {
const relative = entry.slice(root.length + 1);
return !relative.includes('/') && /^(licen[cs]e|copying|notice)([._-].*)?$/i.test(relative);
});
return candidates.sort().map((entry) => ({
name: basename(entry),
text: run('tar', ['-xOzf', archive, entry])
}));
}
function npmProductionPackages(lockValue) {
return Object.entries(lockValue.packages)
.filter(([path, metadata]) => path !== '' && metadata.dev !== true)
.map(([path, metadata]) => ({
path: path.replace(/^node_modules\//, ''),
name: metadata.name ?? path.replace(/^node_modules\//, ''),
version: metadata.version,
license: metadata.license
}));
}
function npmNotice(pkg, modulesRoot) {
if (pkg.version === undefined || pkg.license === undefined) {
throw new Error(`unclassified npm package ${pkg.name}`);
}
const packageRoot = join(modulesRoot, pkg.path);
const candidates = readdirSync(packageRoot)
.filter((name) => /^(licen[cs]e|copying|notice)([._-].*)?$/i.test(name))
.sort();
const documents = candidates.map((name) => ({ name, text: readFileSync(join(packageRoot, name), 'utf8') }));
if (documents.length === 0) {
documents.push({
name: 'PACKAGE METADATA',
text: `The installed package supplies no standalone license or notice file.\nIts locked metadata declares ${pkg.license}.\n`
});
}
return {
ecosystem: 'npm',
name: pkg.name,
version: pkg.version,
license: pkg.license,
documents
};
}
function comparePackage(left, right) {
return compareText(left.name, right.name) || compareText(left.version, right.version);
}
function compareText(left, right) {
return left < right ? -1 : left > right ? 1 : 0;
}
function normalizeLicense(license) {
if (license === 'BSD2') return 'BSD-2-Clause';
if (license === 'BSD3') return 'BSD-3-Clause';
return license;
}
function validateLicenses(packages) {
const allowed = new Set(['Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'MIT']);
const unsupported = packages.filter((pkg) => !allowed.has(pkg.license));
if (unsupported.length > 0) {
throw new Error(`unsupported shipped licenses: ${unsupported.map((pkg) => `${pkg.name} (${pkg.license})`).join(', ')}`);
}
}
function inventory(title, packages) {
return [
title,
...packages.map((pkg) => `${pkg.name}\t${pkg.version}\t${pkg.license}`),
''
].join('\n');
}
function renderNotice(pkg) {
const separator = '='.repeat(79);
return [
separator,
`${pkg.ecosystem}: ${pkg.name} ${pkg.version} (${pkg.license})`,
...pkg.documents.flatMap((document) => [
`--- ${document.name} ---`,
normalizeDocument(document.text),
''
])
];
}
function normalizeDocument(document) {
return document
.replace(/\r\n?/g, '\n')
.split('\n')
.map((line) => line.replace(/[ \t]+$/, ''))
.join('\n')
.replace(/\n+$/, '');
}

View file

@ -3,9 +3,11 @@ set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.." cd "$(dirname "${BASH_SOURCE[0]}")/.."
if ! git diff --quiet || ! git diff --cached --quiet || [[ -n "$(git ls-files --others --exclude-standard)" ]]; then if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
if ! git diff --quiet || ! git diff --cached --quiet || [[ -n "$(git ls-files --others --exclude-standard)" ]]; then
echo "check-v1 requires a clean checkout" >&2 echo "check-v1 requires a clean checkout" >&2
exit 1 exit 1
fi
fi fi
unset NUTHATCH_APP_INTERACTIVE unset NUTHATCH_APP_INTERACTIVE
@ -16,6 +18,8 @@ unset NUTHATCH_IDEMPOTENCY_FIRST_LANE_ONLY
unset NUTHATCH_RECOVERY_TARGET unset NUTHATCH_RECOVERY_TARGET
unset NUTHATCH_PROTOCOL_TARGET unset NUTHATCH_PROTOCOL_TARGET
npm --prefix web ci
targets=( targets=(
test test
check-dependencies check-dependencies

View file

@ -30,13 +30,16 @@ prepare_tools() {
} }
configure_provider() { configure_provider() {
local provider_bin="$RUNTIME/provider-bin"
if [[ -z "${NUTHATCH_REAL_NERDCTL:-}" ]]; then if [[ -z "${NUTHATCH_REAL_NERDCTL:-}" ]]; then
NUTHATCH_REAL_NERDCTL="$(command -v nerdctl)" || return NUTHATCH_REAL_NERDCTL="$(command -v nerdctl)" || return
fi fi
mkdir -p "$provider_bin"
install -m 0755 "$TESTBED/provider/nerdctl" "$provider_bin/nerdctl"
export NUTHATCH_REAL_NERDCTL export NUTHATCH_REAL_NERDCTL
export CONTAINERD_NAMESPACE="$NERDCTL_NAMESPACE" export CONTAINERD_NAMESPACE="$NERDCTL_NAMESPACE"
export KIND_EXPERIMENTAL_PROVIDER=nerdctl export KIND_EXPERIMENTAL_PROVIDER=nerdctl
export PATH="$TESTBED/provider:$PATH" export PATH="$provider_bin:$PATH"
} }
runtime_ctr() { runtime_ctr() {