Compare commits
364 Commits
122febceb4
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 87527646fa | |||
| 30af54b750 | |||
| ac0ebb5302 | |||
| 4bf24c2b18 | |||
| 20b396effd | |||
| b3677dad4c | |||
| b181f59f4c | |||
| 5d849384bc | |||
| 5e640711ce | |||
| 44e4605be8 | |||
| 60ba3328fa | |||
| 5a5301fefb | |||
| 4ee7c34161 | |||
| fe1d19a9c7 | |||
| 6d8541f052 | |||
| ba2359f8b9 | |||
| 9ef09754be | |||
| cbe223166e | |||
| fb2785f694 | |||
| f9dcf157a0 | |||
| 83a482de03 | |||
| 208f383925 | |||
| 05f9535a9b | |||
| c18fd9c44e | |||
| 07fa302d43 | |||
| 4a2b27f54a | |||
| ff507ea16a | |||
| 639196f0c9 | |||
| 404cb2d98c | |||
| 665d808660 | |||
| 15027fef29 | |||
| 26ae12f638 | |||
| 56393380b9 | |||
| d51d1a1266 | |||
| af5599ba54 | |||
| ce8017c6d2 | |||
| b64f8ddffa | |||
| e926d562c0 | |||
| 064adbc0b7 | |||
| 4872b95c43 | |||
| 4547dd5681 | |||
| 655ac1404a | |||
| 521d768635 | |||
| f736c46b81 | |||
| f537b99d02 | |||
| ad4b03871e | |||
| 4ef4bbd4ad | |||
| 2e44bfa09a | |||
| 424b4c6971 | |||
| 4130448a55 | |||
| ed6a6e5a34 | |||
| 36ccd48cfc | |||
| 61f2b376ee | |||
| b59888dc99 | |||
| d29e120c72 | |||
| 16cc43ad28 | |||
| 3773af632c | |||
| 2aeb58d9ed | |||
| affc7521d6 | |||
| f5a7d78d4f | |||
| c42c636231 | |||
| 99b21dbe8e | |||
| 3af4546839 | |||
| ace7801796 | |||
| 7d0a920c8c | |||
| f24f1ff4b7 | |||
| 703359301b | |||
| 66da50eb92 | |||
| 757a24894a | |||
| 4b19aabba6 | |||
| 626642cd25 | |||
| a1c91da619 | |||
| fac4169389 | |||
| a5f1ea1358 | |||
| db8df130fe | |||
| 52f4ab3157 | |||
| 8519d854fb | |||
| edd01e4445 | |||
| 9ea2820411 | |||
| 967818f315 | |||
| ca8e06f300 | |||
| a611b5010f | |||
| 2bc353c8fc | |||
| d4dd979311 | |||
| cd4bafbed3 | |||
| 51d7222c84 | |||
| 888df5c664 | |||
| 220394ed0d | |||
| 89280d6087 | |||
| cb3ad780b1 | |||
| 9615c940bb | |||
| 33e0fb21d6 | |||
| 2a8b2b3a40 | |||
| f765e1d38f | |||
| e6c011d007 | |||
| ba8aaa4ade | |||
| 9a93c6f983 | |||
| 803e9e00e0 | |||
| bb795a9555 | |||
| ac81aeed31 | |||
| 6a4d6d88c5 | |||
| 32ae7e2b38 | |||
| 4e62da3d52 | |||
| b19ae63ceb | |||
| 17c7e2446c | |||
| 1dab2c159a | |||
| 9cabb1053d | |||
| caa2f2d59c | |||
| 6a23042b74 | |||
| 33d13bb455 | |||
| 0074ad6643 | |||
| e2d3c05376 | |||
| c62f30ec32 | |||
| 536a108040 | |||
| c1525eaa60 | |||
| dcd2e1cd19 | |||
| 19865adb6a | |||
| b705e9e1e5 | |||
| 87a6a3f1f8 | |||
| 5914e2e578 | |||
| e39c7a47fc | |||
| 24c55e142b | |||
| de19519be0 | |||
| 3d87e928d4 | |||
| c89f338601 | |||
| 6feaddc542 | |||
| eaecbcf41f | |||
| 7b07b75747 | |||
| 0f397b0ee3 | |||
| 69918e681f | |||
| cb90b72836 | |||
| f5a6ffa8de | |||
| d3845601e9 | |||
| 1385472aaf | |||
| 5fd15c706b | |||
| 5bd502c72f | |||
| f39c4a51dd | |||
| 8e857abf29 | |||
| 2abd95da6d | |||
| 15a3697679 | |||
| a2d7299e72 | |||
| dcdda64b9e | |||
| 5cc5b04b18 | |||
| 0c87d0cc65 | |||
| a7ea5780cc | |||
| 6f06fc9503 | |||
| 6bc1f2d05f | |||
| c736041f3c | |||
| 8a5ad04776 | |||
| 6a726a331d | |||
| b0fe5b4a44 | |||
| 0e4aaabb54 | |||
| 8ddbae5ee2 | |||
| cfa0dd5a9a | |||
| 5644789d64 | |||
| c7c8a96a57 | |||
| f73512107c | |||
| f8401d00a4 | |||
| b79037c055 | |||
| 8af9a55187 | |||
| 1b200f217b | |||
| c0e6d13a1f | |||
| 2b00e03999 | |||
| bf2dabe3f4 | |||
| d0f549ce22 | |||
| 89165e9d88 | |||
| a601e93d00 | |||
| fa4d275cc0 | |||
| 9867d4f232 | |||
| e3b3484905 | |||
| def393b930 | |||
| c1096e2b01 | |||
| 04e1dad952 | |||
| e170bafffc | |||
| 23f31a8c4d | |||
| d5a5375379 | |||
| 7d520d864e | |||
| 16063402be | |||
| 0520592fc5 | |||
| f27a25fdf5 | |||
| 0181a703f9 | |||
| aad7967402 | |||
| 7814c0323e | |||
| eb1ee09769 | |||
| 5265f3e83c | |||
| 5aa03f88dc | |||
| ff2a56ac12 | |||
| 51ce3374b9 | |||
| 16576fd1f6 | |||
| 3ff52465dd | |||
| 0e9f8e2dd6 | |||
| 4225f3c8da | |||
| fff8c26df9 | |||
| 7d8a986784 | |||
| 4e25b796ca | |||
| 581c1c0c0a | |||
| 8d1b6b114a | |||
| 75ca9d485d | |||
| 74dc86d56c | |||
| 3e3c866ec0 | |||
| 2b0c63a783 | |||
| 8386beac1d | |||
| dc9694fb32 | |||
| 70a155a0a7 | |||
| 673aaf1c13 | |||
| c059bc34d8 | |||
| f2a3552286 | |||
| f491fe517a | |||
| f6c8f4fae6 | |||
| b43ea19e1e | |||
| c6ff40e288 | |||
| 86509e2598 | |||
| adc5498641 | |||
| 74d779ee35 | |||
| cc25a91cf1 | |||
| 13c909f085 | |||
| 2823433f7c | |||
| abfb28dea2 | |||
| ebac73704b | |||
| 74f1df6893 | |||
| 1ef4c22ec6 | |||
| 262516d3da | |||
| 1304c5180f | |||
| c3cabed6e7 | |||
| 274b79f293 | |||
| aa76b5ad79 | |||
| 84496271c4 | |||
| ac4670492e | |||
| aaed0c2547 | |||
| ed13289934 | |||
| a575ecdd68 | |||
| 0a2a4c68fa | |||
| d9692da89c | |||
| fbbc8d2d8b | |||
| d27186a177 | |||
| 812d59e4dd | |||
| 995c258aa5 | |||
| bd9af70a97 | |||
| 465c40b927 | |||
| 6f11b8c453 | |||
| 86ec9e9d54 | |||
| 78fe3e7956 | |||
| ad8a8c3cac | |||
| 46596fc5f6 | |||
| f09bbf8d4a | |||
| 83e0f1cda5 | |||
| aa4f63a8ce | |||
| 499a10837f | |||
| 405ccd1709 | |||
| b735c95c84 | |||
| e61a0f1de9 | |||
| 52b013e165 | |||
| 76081fd88f | |||
| 4a9649f096 | |||
| 061b6986a3 | |||
| ff49052baa | |||
| 7bd5255c63 | |||
| 473f4b19f0 | |||
| e04d1e55a1 | |||
| e23f053b89 | |||
| ddc416a45a | |||
| c07f55e8d4 | |||
| d3e832f064 | |||
| 6e5c1c158c | |||
| 07a326114e | |||
| 2725d4085c | |||
| 860cd61ba5 | |||
| 4bfd6d05ca | |||
| 9e0af5fdff | |||
| 8ac3e35b1a | |||
| cc4d918a07 | |||
| 63e3b7fb17 | |||
| 562cfaebf8 | |||
| b47b670d94 | |||
| 7aa8403b70 | |||
| 6ec142e8f4 | |||
| 49cf91b789 | |||
| 4437e86f72 | |||
| 8ec5e261d4 | |||
| 815c820e6a | |||
| 4490194389 | |||
| 71052b8c26 | |||
| dc43029892 | |||
| bf592223de | |||
| 8b3fb3f4a2 | |||
| e9909ef057 | |||
| e49ccc5aa3 | |||
| e78c1cba6d | |||
| 689ff9bd5c | |||
| 7c3ce01b27 | |||
| 6081b7ad79 | |||
| 0e902c1f57 | |||
| df20a2ea1e | |||
| 870fdc3f65 | |||
| 5f11d27f58 | |||
| e9df237ca9 | |||
| 8b6081dbbf | |||
| f064c794e3 | |||
| 960e959bd6 | |||
| ba30ad22f8 | |||
| 29db3ce86a | |||
| 2d89475f0b | |||
| f71fbdafac | |||
| 90b16dad40 | |||
| 05b9308c1b | |||
| ca8e6824ea | |||
| a0f2505728 | |||
| 3651679c86 | |||
| b0bffe5d72 | |||
| 86446de9a8 | |||
| a63b8c6a0b | |||
| ef681069c7 | |||
| 5797bc85bf | |||
| 4c829133a3 | |||
| 2328446520 | |||
| af4adc0fc8 | |||
| f61a00d292 | |||
| 7d4ce6f073 | |||
| db5b029f78 | |||
| e8754afca2 | |||
| c179adccb2 | |||
| caed7bbfdf | |||
| ff9df11a15 | |||
| 5396812290 | |||
| 9996a47d0c | |||
| abd249030b | |||
| 3c132ca4dc | |||
| f219908d67 | |||
| 5e371cf734 | |||
| 76901304c4 | |||
| 83c25e328f | |||
| ccd7140d6d | |||
| 0db52b89f0 | |||
| 93cda87bbd | |||
| 35ef330df8 | |||
| 348b76aa04 | |||
| df748183ab | |||
| e470dfc258 | |||
| 56e2c037b5 | |||
| c5c4167361 | |||
| 2f70f87f41 | |||
| 0519d155fa | |||
| 6d801d0f0e | |||
| fd21d77526 | |||
| 5b927900ae | |||
| 5fcb2bcc83 | |||
| 6dd4caf947 | |||
| 506d1adfcc | |||
| e506f7c2e6 | |||
| 574b2d6646 | |||
| b0adbd4419 | |||
| 8125081177 | |||
| 229339dc17 | |||
| 99b54df287 | |||
| 70689047ca | |||
| b4be3ed2a6 | |||
| 8e814917b6 | |||
| a8686d9169 | |||
| 830f63641b | |||
| 3e0def4305 | |||
| 722a304023 | |||
| 423fa1ed26 | |||
| 6cdc0ad1e6 | |||
| 65b3169d98 |
@@ -5,3 +5,6 @@ indent_size = 2
|
||||
|
||||
[*.{md,markdown,mdx}]
|
||||
max_line_length = 80
|
||||
|
||||
[{Makefile,*.mk}]
|
||||
indent_style = tab
|
||||
|
||||
@@ -26,3 +26,4 @@ pnpm-debug.log*
|
||||
**/*.tfstate.backup
|
||||
|
||||
*.sqlite
|
||||
DIGITALOCEAN_TOKEN
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
include make/vars.mk
|
||||
|
||||
submake_file = $(shell [ -f "$(module)/Makefile" ] && echo "$(module)/Makefile")
|
||||
install_submake_file = $(shell [ -f "$(module)/install.mk" ] && echo "$(module)/install.mk")
|
||||
|
||||
define make_module_rule =
|
||||
.PHONY: make_$(module)
|
||||
make_$(module): $(module)/.env
|
||||
$(if $(submake_file),\
|
||||
$(MAKE) --makefile=$(notdir $(submake_file)) --directory=$(module))
|
||||
endef
|
||||
|
||||
define module_env_rule =
|
||||
$(module)/.env: .env
|
||||
cp .env $(module)/.env
|
||||
endef
|
||||
|
||||
define install_module_rule =
|
||||
.PHONY: install_$(module)
|
||||
install_$(module): install_openrc_$(module) install_dyndns_$(module) $(if $(SUBDOMAIN_$(module)),install_tls_$(SUBDOMAIN_$(module))) $(install_submake_file)
|
||||
$(if $(install_submake_file),$(MAKE) --makefile=$(notdir $(install_submake_file)) --directory=$(dir $(install_submake_file)) install)
|
||||
endef
|
||||
|
||||
define reinstall_module_rule =
|
||||
.PHONY: reinstall_$(module)
|
||||
reinstall_$(module): reinstall_openrc_$(module) reinstall_dyndns_$(module) $(if $(SUBDOMAIN_$(module)),reinstall_tls_$(SUBDOMAIN_$(module))) $(install_submake_file)
|
||||
$(if $(install_submake_file),$(MAKE) --makefile=$(notdir $(install_submake_file)) --directory=$(dir $(install_submake_file)) reinstall)
|
||||
endef
|
||||
|
||||
define uninstall_module_rule =
|
||||
.PHONY: uninstall_$(module)
|
||||
uninstall_$(module): uninstall_openrc_$(module) uninstall_dyndns_$(module) $(if $(SUBDOMAIN_$(module)),install_tls_$(SUBDOMAIN_$(module)))
|
||||
$(if $(install_submake_file),\
|
||||
$(MAKE) --makefile=$(notdir $(install_submake_file)) --directory $(dir $(install_submake_file)) uninstall
|
||||
)
|
||||
endef
|
||||
|
||||
.PHONY: all
|
||||
all: $(ENV_RULES) $(MAKE_RULES) $(BUILD_RULES) $(PUSH_RULES)
|
||||
|
||||
$(foreach module,$(ALL_MODULES),$(eval $(call make_module_rule)))
|
||||
$(foreach module,$(ALL_MODULES),$(eval $(call module_env_rule)))
|
||||
|
||||
.PHONY: install
|
||||
install: install_tls install_nginx $(ENV_RULES) $(INSTALL_RULES) install_crontab
|
||||
|
||||
.PHONY: reinstall
|
||||
reinstall: reinstall_tls reinstall_nginx reinstall_dyndns $(ENV_RULES) $(REINSTALL_RULES) reinstall_crontab
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall: uninstall_nginx uninstall_dyndns uninstall_tls uninstall_joeac.net_service $(UNINSTALL_RULES) uninstall_crontab
|
||||
|
||||
$(foreach module,$(ALL_MODULES),$(eval $(install_module_rule)))
|
||||
$(foreach module,$(ALL_MODULES),$(eval $(reinstall_module_rule)))
|
||||
$(foreach module,$(ALL_MODULES),$(eval $(uninstall_module_rule)))
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
$(foreach module,$(MAKE_MODULES),$(MAKE) --directory=$(module) clean;)
|
||||
|
||||
include make/container.mk
|
||||
include make/openrc.mk
|
||||
include make/nginx.mk
|
||||
include make/crontab.mk
|
||||
include make/dyndns.mk
|
||||
include make/tls.mk
|
||||
@@ -2,44 +2,103 @@
|
||||
|
||||
Joe Carstairs' public Internet presence
|
||||
|
||||
Structure:
|
||||
To install:
|
||||
|
||||
```
|
||||
/
|
||||
├── capsule My Gemini capsule
|
||||
├── smtp A local SMTP server
|
||||
└── website My Website
|
||||
```sh
|
||||
wget -O- https://git.joeac.net/joeac/joeac.net/raw/branch/main/install.sh | sh
|
||||
```
|
||||
|
||||
## Running with Podman
|
||||
## DNS setup
|
||||
|
||||
These instructions will probably work with Docker, too: just substitute `podman`
|
||||
for `docker` in all the commands.
|
||||
A/AAAA DNS records are added automatically by `make install`, but in order to
|
||||
get `mox` working as an email server, you'll have to manually add the following
|
||||
DNS records.
|
||||
|
||||
To run with Podman, first set up your environment variables. Copy `example.env`
|
||||
to `.env` and edit the values accordingly.
|
||||
Deliver mail for mail.joeac.net to mail.joeac.net:
|
||||
|
||||
Then, create the `remote_smtp_password` secret, storing the password for the
|
||||
remote SMTP server which will send the contact emails on behalf of the website.
|
||||
|
||||
```bash
|
||||
sudo podman secret create remote_smtp_password /path/to/remote/smtp/password
|
||||
```
|
||||
mail.joeac.net. MX 10 mail.joeac.net.
|
||||
```
|
||||
|
||||
Now build and start the containers:
|
||||
All emails from the email server will be signed with two DKIM keys. So that
|
||||
clients can verify the authenticity of the messages, provide these keys as TXT
|
||||
records:
|
||||
|
||||
```bash
|
||||
sudo podman-compose build && sudo podman-compose up -d
|
||||
```
|
||||
2026a._domainkey.mail.joeac.net. TXT v=DKIM1;h=sha256;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvYfjrzmYgBtYofzBwI80aiW98+M2g6z+gd1Iwz9g0y30rPFNTctFn9GwBNuYBgZiZxB+sqjEbPMbr3li/R7i0A9t/KbNNJhkNC+4IjKJjk+jw1CXm4vXOUa4YSYWwy7NVYTH/QwZGz6fwjVM7YvDnnE4gG2NwrVx+AXlONt2R1G+qgV6HAIIvVi8T0yCjjqEc5B5bKlqk0XU9vSyUFJhhKnR/KNRe79C+H9GWJzcU7HUCmIHX04Xi0JeB/wm3weF1xjtGTsWyy5BmHHsfWGqSr2Dbg5o6AI5W0h4VkQ4QzdEYGVQ9ZBDyqFQwQFXLn0oHZjCD/vFzPOPdM5pxF/OgwIDAQAB
|
||||
2026b._domainkey.mail.joeac.net. TXT v=DKIM1;h=sha256;p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzcrk9FUt6AdrvnAP3KawuOTLw7uL5SJ+ZYmShuz41zwM6bPQteGSSwddFXIxcqVlJrdFahrK4KvHX/sw/hWVfZoPLDdwsGN5eI8cqQjNDE+JDu9BbPlTituva4Hkve0hbAKDqA8jmbcZg6aU7b44Kzq8UpWAlPO273Rq2tsbCBcITt8B3NFoeY9CSsZU1LqGl855GUtaNyhlPaAvfab3Q9/4wyusPhCHlBYaRK+ZzuSMs5KEOG6n4kbZfMVi2+4c/bPU5PdTuyvbSIEqjNH4TpfatE0I9ubGv0WbAzr5EZbv5+xtukZ/dIisPPMjn1AbjpSJYNYr2OYgey6+WvzRmQIDAQAB
|
||||
```
|
||||
|
||||
## Running on the host machine
|
||||
Specify the MX host is allowed to send for our domain and for itself (for DSNs).
|
||||
~all means softfail for anything else, which is done instead of -all to prevent
|
||||
older mail servers from rejecting the message because they never get to looking
|
||||
for a dkim/dmarc pass.
|
||||
|
||||
To run on the host machine, first, as before, set up your environment variables
|
||||
by copying `example.env` to `.env` and editing the values as appropriate.
|
||||
|
||||
```bash
|
||||
npm run start
|
||||
```
|
||||
mail.joeac.net. TXT v=spf1 ip4:217.155.190.42 ip6:fdc9:6aec:7a18:0:2e0:4cff:fe61:9b17 mx ~all
|
||||
```
|
||||
|
||||
Note that emails may not work locally without further setup. These instructions
|
||||
are of course woefully incomplete.
|
||||
Emails that fail the DMARC check (without aligned DKIM and without aligned SPF)
|
||||
should be rejected, and request reports. If you email through mailing lists that
|
||||
strip DKIM-Signature headers and don't rewrite the From header, you may want to
|
||||
set the policy to p=none.
|
||||
|
||||
```
|
||||
_dmarc.mail.joeac.net. TXT v=DMARC1;p=reject;rua=mailto:dmarcreports@mail.joeac.net!10m
|
||||
```
|
||||
|
||||
Remote servers can use MTA-STS to verify our TLS certificate with the WebPKI
|
||||
pool of CA's (certificate authorities) when delivering over SMTP with
|
||||
STARTTLSTLS.
|
||||
|
||||
```
|
||||
mta-sts.mail.joeac.net. CNAME mail.joeac.net.
|
||||
_mta-sts.mail.joeac.net. TXT v=STSv1; id=20260705T153220
|
||||
```
|
||||
|
||||
Request reporting about TLS failures.
|
||||
|
||||
```
|
||||
_smtp._tls.mail.joeac.net. TXT v=TLSRPTv1; rua=mailto:tlsreports@mail.joeac.net
|
||||
```
|
||||
|
||||
Client settings will reference a subdomain of the hosted domain, making it
|
||||
easier to migrate to a different server in the future by not requiring settings
|
||||
in all clients to be updated.
|
||||
|
||||
```
|
||||
clientsettings.mail.joeac.net. CNAME mail.joeac.net.
|
||||
```
|
||||
|
||||
Autoconfig is used by Thunderbird. Autodiscover is (in theory) used by
|
||||
Microsoft.
|
||||
|
||||
```
|
||||
autoconfig.mail.joeac.net. CNAME mail.joeac.net.
|
||||
_autodiscover._tcp.mail.joeac.net. SRV 0 1 443 mail.joeac.net.
|
||||
```
|
||||
|
||||
For secure IMAP and submission autoconfig, point to mail host.
|
||||
|
||||
```
|
||||
_imaps._tcp.mail.joeac.net. SRV 0 1 993 mail.joeac.net.
|
||||
_submissions._tcp.mail.joeac.net. SRV 0 1 465 mail.joeac.net.
|
||||
```
|
||||
|
||||
Next records specify POP3 and non-TLS ports are not to be used. These are
|
||||
optional and safe to leave out (e.g. if you have to click a lot in a DNS admin
|
||||
web interface).
|
||||
|
||||
```
|
||||
_imap._tcp.mail.joeac.net. SRV 0 0 0 .
|
||||
_submission._tcp.mail.joeac.net. SRV 0 0 0 .
|
||||
_pop3._tcp.mail.joeac.net. SRV 0 0 0 .
|
||||
_pop3s._tcp.mail.joeac.net. SRV 0 0 0 .
|
||||
```
|
||||
|
||||
Optional: You could mark Let's Encrypt as the only Certificate Authority allowed
|
||||
to sign TLS certificates for your domain.
|
||||
|
||||
```
|
||||
mail.joeac.net. CAA 0 issue "letsencrypt.org"
|
||||
```
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
FROM alpine:3.23 AS agate
|
||||
RUN apk --update --no-cache add rust cargo \
|
||||
&& wget -O - https://github.com/mbrubeck/agate/archive/refs/tags/v3.3.20.tar.gz | tar -xz \
|
||||
&& cargo install --path agate-3.3.20/ \
|
||||
&& apk del rust cargo \
|
||||
&& rm -rf agate-3.3.20 agate-target
|
||||
|
||||
FROM alpine:3.23 AS comitium
|
||||
RUN apk --no-cache add make go scdoc
|
||||
RUN wget -O - https://git.sr.ht/~nytpu/comitium/archive/v1.8.2.tar.gz | tar -xz \
|
||||
&& make --directory=comitium-v1.8.2 \
|
||||
&& mv comitium-v1.8.2/build/comitium /usr/local/bin/comitium \
|
||||
&& apk del make go scdoc \
|
||||
&& rm -rf comitium-v1.8.2
|
||||
|
||||
FROM alpine:3.23 AS final
|
||||
RUN mkdir -p /var/app/content
|
||||
WORKDIR /var/app
|
||||
COPY capsule/.certificates .certificates
|
||||
RUN crontab -l > crontab.tmp \
|
||||
&& echo "0 */6 * * * /usr/local/bin/comitium refresh --data /var/app/comitium-data" >> crontab.tmp \
|
||||
&& crontab crontab.tmp \
|
||||
&& rm crontab.tmp
|
||||
|
||||
# bash is needed to run the build scripts
|
||||
# busybox-openrc provides rc-service, which runs crond
|
||||
# gcc is a dependency for agate
|
||||
# make is needed to run the Makefile
|
||||
RUN apk --no-cache add bash busybox-openrc gcc make
|
||||
RUN rc-update add crond
|
||||
COPY --from=agate /root/.cargo/bin/agate /usr/local/bin/agate
|
||||
COPY --from=comitium /usr/local/bin/comitium /usr/local/bin/comitium
|
||||
|
||||
COPY capsule .
|
||||
COPY common /var/common
|
||||
RUN make
|
||||
|
||||
CMD ./run.sh
|
||||
@@ -167,7 +167,7 @@ Some authors have managed to recognise the ambiguity of the term. Both King et a
|
||||
|
||||
So it seems that although many academics have treated the term ‘hallucination’ as jargon, in actual fact, there is no widely agreed specific meaning of the word.
|
||||
|
||||
<hr>
|
||||
---
|
||||
|
||||
I will slide in here with a quick side note. While some authors treated ‘hallucination’ as jargon for unfaithfulness or unfactuality, other authors contemporaneously managed to talk about these topics without using the word ‘hallucination’ at all. Here are some examples:
|
||||
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
# "381: how the church as we know it was made"
|
||||
|
||||
The church which defines our world now is in a significant way the one which emerged out of sixty years of controversy from the Council of Constantinople in 381. I've been charting what happened, why, and the ongoing legacy.
|
||||
|
||||
Published on: 5 Oct 2025
|
||||
|
||||
Athanasius defined the fourth century. Not that he was a god, or even a king, or that he always got his way. But he wrote the history books. His tale of an epic battle fought tooth-and-nail between Arian heretics and him and his loyal allies has come to be the standard account of how, over the course of the fourth century, the Church redefined what orthodoxy means and how it is declared and identified.
|
||||
|
||||
The result was the Nicene Creed. It had been first written for a very particular polemical purpose in 325, but later found itself the centre of a strange theological revival, and was finally revised in a council at Constantinople in 381. In so doing, the bishops assembled a recognisable 'Nicene' tradition which is still one of the defining features of planet Earth.
|
||||
|
||||
For better and for worse, the church as we know it has a capacity both for great humility, faith and submission to the mystery of God, but it also has a capacity for great intolerance. This is the church created in 381.
|
||||
|
||||
To understand the church as we know it today, then, we need to understand the complex, confusing journey from 325 to 381.
|
||||
|
||||
Athanasius' chronicle of that journey is temptingly simple. The only problem with it is that it isn't true. Indeed, his 'history' was never meant to function as an all-encompassing narrative of Church history, to be read for centuries ever after. His accounts function as polemics, meant to cajole, condemn and persuade his readers in his own time of his vision for their future.
|
||||
|
||||
Nevertheless, whatever Athanasius' real significance in how his times unfolded, his witness is important. He fully inhabited his times, often in the middle of the fray. Whether or not we buy Athanasius' portrayal of himself as fighting the good fight, he was certainly a fighter. By looking through his eyes, then, we can get a perspective on how the Church as we know it came to be.
|
||||
|
||||
So it makes sense to start with him. As a young priest in his native Alexandria, he became tangled up in a controversy which would come to define his career. A strong-minded and fearless young priest had begun to preach. His name was Arius.
|
||||
|
||||
---
|
||||
|
||||
According to the Egyptian tradition, Alexander, the bishop of Alexandria, was the nineteenth in a direct line of succession from Mark the Evangelist himself. With a great deal of justice, he would have regarded himself as one of the most important Christian leaders in the world, and at least the equal of the bishop of Rome.
|
||||
|
||||
Small wonder, then, that the insubordination that plagued his diocese bothered him. First Erescentius had started a schism, disputing the rule he used for calculating the date of Easter.
|
||||
|
||||
Then there was Meletius. During the persecution under the Roman emperor Diocletian, Meletius had already rubbed a few people the wrong way: while other bishops were in hiding or in prison, he took the initiative to resolve problems and ordain priests without properly consulting the absent bishops' representatives. Perhaps it was intended kindly: it was seen as meddling. Now Meletius accused Alexander of being too soft on Christians who had caved into the threat of torture and made sacrifices to the pagan cults. When he decided Alexander was never going to match his high rigorist standards, he broke away, too.
|
||||
|
||||
Alexander must have longed for the relatively good order of the Greek and Roman churches, where bickering subordinates were generally willing to let their bishop have the last say. The throne of St Mark was in trouble. If Christ's body wasn't to get chopped up any more than it already was, he needed to establish his personal authority.
|
||||
|
||||
This was the context in which Arius, a young firebrand priest, steps onto stage right. He surely knew his own bishop's teaching: God is one substance and one essence, unchangeable, indivisible. Christ his Son is in every way God: God from God, light from light, true God from true God, eternally begotten of the Father before all ages. How else could Christ, by adopting human flesh, mediate the transcendent God to fallen humanity?
|
||||
|
||||
But Arius didn't like this one bit. If God is unchangeable, how could he adopt flesh? That suggests he was not flesh, and then became flesh. And in any case, if the martyrs were right to give up their lives to know God, he must have the perfect, uncompromising transcendence which the martyrs so admired. But how can God adopt flesh, never mind suffer and die on a cross, without compromising that transcendence? Something had to give. For Arius, the solution was to modify the relationship between the Father and the Son.
|
||||
|
||||
Arius accepted that Christ had to be in some sense divine, in order to mediate God to humanity. But he denied that he was quite as much God as God is. He has something like his Father's essence, not in a co-equal way, but rather in a derivative way. This makes sense of Father-Son language, which suggests the Father came first, and the Son came next, a derivative of the Father. So the Son is God from God, but not true God from true God. The Son was begotten in time, and is not eternal: only God the Father himself is eternal.
|
||||
|
||||
At another time in another place, Arius might have passed for a creative, independent thinker without much notice. But Arius was directly contradicting Alexander just as the latter was desperate to assert his authority. It got ugly.
|
||||
|
||||
Alexander called a council of local bishops in about 320. The council condemned Arius and removed him from his post as priest. In response, Arius went on the campaign trail, visiting bishops in Palestine and Asia Minor who he thought would be sympathetic to his theology. Shortly afterwards, he returned to Alexandria, triumphantly brandishing vindications from two councils, one in Jerusalem and one in Bithynia. He wasn't going to make it easy for Alexander.
|
||||
|
||||
Luckily for Alexander, the Emperor Constantine had just united the eastern and western halves of the Empire. He had famously converted to Christianity after seeing the sign of the cross at the Battle of Milvian Bridge in 312, and saw the bishops as means towards his mission of uniting the Empire under one government and one God. Constantine had been made aware of the dispute between Arius and Alexander, and he didn't want schisms in the church any more than Alexander did.
|
||||
|
||||
He called a council in his own palace in Nicaea, paying the travel expenses and hotel bills of all the bishops in attendance. For those bishops, many carrying the scars of torture they had endured under Diocletian, it must have been a bewildering experience. Alexander was in attendance. His secretary was Athanasius.
|
||||
|
||||
In 325, the council condemned Arius. To avoid anyone else following in his path, they produced a statement of faith, designed to exclude Arius' teaching, no matter who taught it. This statement of faith is now known as the Nicene Creed.
|
||||
|
||||
The council also fixed the date of Easter to boot. Alexander must have been relieved.
|
||||
|
||||
You might have thought that would have been the end for Arius. In fact, Constantine engineered his re-admittance into the church as soon as 328. Arius died in peace in 336. Constantine's mission wasn't to purge the church, but to unite the church. As long as all sides worshipped God and could live in peace, he wanted as many people as possible included. His mission was unity, not uniformity.
|
||||
|
||||
Bishops like Eusebius of Caesarea in Syria got this. He had been provisionally excommunicated on suspicion of Arianism in 325, but was reconciled at Nicaea given the chance to explain himself and sign up to the Nicene Creed. No sooner had he done this, however, than he had started explaining to the faithful back home how they could carry on believing that the Son was not really eternal, even as the Creed was designed to exclude exactly such a claim. While Eusebius might seem duplicitous, at the time, this was exactly the kind of tolerant pragmatism that Constantine asked of the bishops: as long as they didn't cause more out-and-out conflict.
|
||||
|
||||
Alexander didn't have long to enjoy the peace of Nicaea. He died just a few years afterward in 328. The throne of St Mark passed to Athanasius.
|
||||
|
||||
---
|
||||
|
||||
The peace didn't last long. Just as Athanasius was donning his mitre, Eusebius was plotting against Eustathius the bishop of Antioch, and engineered his deposition. In his defence, Eusebius accused Eustathius of the long-condemned heresy, Sabellianism. Then in 335, he followed up by deposing Marcellus, the bishop of Ancyra, at a council in Tyre.
|
||||
|
||||
To defend his action, he wrote _Against Marcellus_, in which he accused Marcellus of being a Sabellian, too. Sabellius' heresy was (to borrow a modern term) modalism, the view that 'Father', 'Son' and 'Spirit' are mere titles, aspects, 'modes' of God, not in any real way distinct. He also accused Marcellus of adoptionism, another agreed heresy. Marcellus taught that the Son only became an aspect of the divine nature at the Incarnation, and that in the last day, Christ would hand over his kingdom to his Father.
|
||||
|
||||
This action would cast a long shadow over the next half-century. Time and again, bishops allied to Eusebius' way of thinking, or 'Eusebians', would re-affirm their opposition to that 'heretic' Marcellus and his 'Sabellianism'. This is a crucial dynamic for understanding where theological factions drew up their battle lines, and for what compromises were needed in order to get to 381.
|
||||
|
||||
Even the bishop of Alexandria wasn't immune from Eusebius' purge. Athanasius had vigorously defended his ally, Marcellus, at the council of Tyre in 335. Eusebius set about plotting his downfall. He dug up dirt. He accused Athanasius of using threats and bribes to get himself elected, and sending goons to beat up his political opponents. Once he'd found evidence of Athanasius meddling with the crucial Egyptian grain export that kept Rome fed, he had the emperor on side. Constantine convened a meeting in 336 and exiled him to the German frontier.
|
||||
|
||||
---
|
||||
|
||||
Or at least, that's how Athanasius tells it. Athanasius loves a plot: at the time, alleging a conspiracy was a classic rhetorical technique for painting your enemies as heretics.
|
||||
|
||||
Eusebius was no stranger to rhetoric himself, and it's to his 337 best-seller, the _Life of Constantine_, that we owe our standard account of Constantine's reign. He regarded Empire and Church as allies in a joint mission, to unite the world under one government and one faith. To him, someone like Athanasius, constitutionally incapable of tolerating anyone who disagreed with him and willing to use gangster tactics to get his way, was a threat to this divine mission.
|
||||
|
||||
It's worth remembering that after Constantine died, Athanasius would be re-exiled by four more Roman emperors. In his lifetime, only Julian failed to exile Athanasius, and him only perhaps because he didn't have time in his whirlwind twenty-month reign. We also can't be sure how much influence Eusebius actually had in the expulsion of Athanasius and his allies: it coheres well enough with the emperor's anti-sectarian agenda that it might have happened with or without Eusebius' involvement.
|
||||
|
||||
Perhaps Athanasius was a brute. Still, the Roman Catholic Church manages to venerate both Eusebius and Athanasius as saints. This may seem like a contradiction. But perhaps an ability to tolerate contradiction is precisely the legacy of 381.
|
||||
|
||||
But we're not there yet. By 335, Eusebius had engineered the exile of Eustathius, Marcellus, and Athanasius. After Constantine died, he had to do it all over again, but by 339, he had persuaded his successor, Constantius, to re-assert his father's exiles of the three men. With the Empire once again split, Athanasius and Marcellus headed to Rome to re-group and re-think.
|
||||
|
||||
---
|
||||
|
||||
From Rome, Athanasius and Marcellus were safe for now from Eusebius' clutches, but also relatively impotent. In this period of exile in the 340s, in an effort to claw back his reputation, Athanasius developed the polemic which still defines the standard history of the fourth century. He invented a cunning label for Eusebius and his cronies: he called them 'Arians'.
|
||||
|
||||
Eusebius rejected the label as ridiculous. Arius had been reconciled, and more to the point, had died in 336. For that matter, why would a bishop follow the teaching of a mere priest? Not only that, but the label ignored significant differences between Arius' and Eusebius' teaching. His verdict was clear: the label 'Arian' is a baseless slur, with no other purpose than to tar his reputation as a heretic.
|
||||
|
||||
He was right, of course. But like it or not, Athanasius' theory of an Arian conspiracy began to win adherents, not least Julian, the bishop of Rome. Julian called a council to exonerate Athanasius and Marcellus. When the Greeks refused to turn up, he called a local council anyway and vindicated the two men. In the face of Greek obstinacy, Julian wrote east, pleading the bishops to take the 'Arian' threat seriously.
|
||||
|
||||
In response, the easterners held a council in Antioch in 341, agreeing four creeds which powerfully condemned Marcellus' teaching, including the influential Dedication Creed. This includes assertions that Father, Son and Spirit are 'three in subsistence, one in agreement', that the Son was generated before time began, against Marcellus' teaching that the Father, Son and Spirit are aspects of God without division in subsistence, and that there only came to be a divine Son at his incarnation. They explicitly condemned Arius, Sabellius and Marcellus.
|
||||
|
||||
So the divisions grew deeper. Without an emperor to compel the bishops to come together, there may not have been much chance of a rapprochement. But even if there were to be such an emperor, who's to say that their settlement would have satisfied the bishops?
|
||||
|
||||
---
|
||||
|
||||
Meanwhile, in the 340s and through the 350s, two further theological movements gathered steam: the homoians and the heterousians.
|
||||
|
||||
The homoians, perhaps tired of the squabbles between the Athanasian and Eusebian factions, determined to sidestep their petty debates altogether.
|
||||
|
||||
A key term of the theological disagreement was 'essence' or 'ousia'. Athanasius, in his lifelong battle to make sure Arius stayed dead, insisted that Father, Son and Spirit shared the same ousia. In contrast, Eusebius, with his anti-Sabellian polemic, needed to assert the real distinction between Father, Son and Spirit, and so asserted that each had a separate ousia. So the difference can be summed up as a counting problem. How many divine ousias are there? One or three?
|
||||
|
||||
The homoians claimed that both sides were mistaken, simply because they used the word 'ousia'. There is no mention of ousia in Scripture, so, they claimed, we have no basis for asserting it of God one way or the other. All we can truly say is that Father, Son and Spirit are distinct but somehow alike. Whereof we cannot speak, there must we remain silent.
|
||||
|
||||
This might have worked as a way forward, except that the heterousians provoked such a strong reaction that 'ousia'-talk was needed to refute them. Aetius, and his followed Eunomius, argued that since God is simple, and all generate things are divided, it follows that God is ingenerate. But the Son is generate: therefore Father and Son must be altogether unalike. They expressed this by saying that Father and Son are unlike in ousia. This teaching was swiftly branded 'neo-Arian', provoking a strong reaction. To counter the heterousian teaching, their opponents were forced to fight on their terms, and that meant using 'ousia'-talk.
|
||||
|
||||
Thus enters Basil of Caesarea. He argued that if we abandon 'ousia'-talk, we will have no way of saying that the Father and Son have anything in common at all, which makes a nonsense of the idea that the Son brings humanity knowledge of his Father. Without like essence, they might as well be two completely different Gods. Therefore we have to say at least that they have like essence -- 'homoiousia'. But without direct access to perfect knowledge of the invisible God, we're not in a position to judge that they have exactly the same essence, so he stopped short of agreeing with the 'homoousia' of the Nicene Creed which Athanasius so treasured.
|
||||
|
||||
Seeing the opportunity to make common cause against the homoians, Athanasius started to soften. He wrote an extremely charitable commentary on Basil's theology which emphasised their similarities and papered over their differences. Athanasius recognised that both he and Basil wanted to assert the unity of God while still preserving distinctions between Father, Son and Spirit. The two began to campaign against the homoian movement.
|
||||
|
||||
But Basil got there too late. In 359, the emperor Constantine II called a council in Constantinople, and in 360 it issued a homoian creed with full imperial backing. Any campaign against the homoians would have to take place sub rosa.
|
||||
|
||||
---
|
||||
|
||||
In Athanasius' and Basil's long, slow campaign against homoianism, their weapon of choice was surprising: they dusted off the Nicene Creed of 325. Athanasius argued, against the homoians, that 'ousia'-talk, although not directly Scriptural, was essential in order to draw out the consequences of Scripture while ruling out Arian mis-interpretations.
|
||||
|
||||
Thus Nicaea, conceived as a one-off meant to clean up the Arian controversy, found a new life as the anti-homoian movement -- or perhaps you could call it the Nicene revival? -- rallied around it.
|
||||
|
||||
As the movement progressed, the formerly disagreeing bishops found ways to come together. An essential move was that made in Athanasius' _Antiochene Tome_ of 362. In it, he relented on his long opposition to there being three 'hypostases' or 'substances' in the Godhead.
|
||||
|
||||
'Hypostasis' had for a long time been used interchangeably with 'ousia'. However, Athanasius claimed that perhaps God could have three hypostases, but only one ousia, at the same time. In so doing, he wedged apart a sharp technical distinction between 'hypostasis' and 'ousia' which previously wouldn't have made sense. Logical or not, it enabled the Nicene revival to have its cake and eat it. God is both one in ousia, protecting against Arianism, and three in hypostasis, protecting against Sabellianism.
|
||||
|
||||
So the Nicene revival gained a new superpower: the power to use formerly synonymous terms to assert contradictions without blushing. This power to accept apparent contradiction as part of the unknowable mystery of God is perhaps the most important legacy of the period. Arguably, the church has been at its best when it has put aside the need to know everything, and embraced this spirit of tolerance, humility and faith.
|
||||
|
||||
---
|
||||
|
||||
For much of the 360s and 370s, the homoian emperor Valens had ruled over the eastern part of the Empire, while his big brother, Valentinian, ruled the west. In the late 370s, Valentinian and then Valens died within quick succession of each other. Valentinian's twenty-year-old son, Gratian, was left to clear up the mess. In 379, Gratian delegated rule of the east to Theodosius, who was to implement a decisively different religious policy than his predecessor, Valens.
|
||||
|
||||
In 380, Theodosius issued an edict, saying that only those who agreed to the homoousios clause of the Nicene Creed could be considered 'catholic' Christians. The message was clear: the homoians were out, and the Nicenes were in.
|
||||
|
||||
In 381, he called a council to Constantinople, and it (probably) issued the revision of the 325 creed which is still used in various versions in all the world's largest Christian denominations. There would be no more revisions, and it would become, then as now, compulsory reading for all those preparing to don vestments.
|
||||
|
||||
One question is, why did the 381 creed differ in the ways it did from 325? Many of the differences, including the much-enlarged section on the Son, seem to have little controversial content: nobody was disputing that Jesus was born of the Virgin Mary, for example, though she makes her first appearance in the Creed in the 381 version. Some historians think this suggests that the 381 was based on a similar, but distinct creed from 325. This seems unlikely to me, given that about half the creed is in verbatim agreement with 325.
|
||||
|
||||
However, a couple of edits stand out. There are some clear signs of anti-Marcellianism: 'his \[the Son's] kingdom shall have no end', the Son is begotten of the Father 'before all ages'. Perhaps a clear emphasis on the eternal relationship between the Son and the Father was part of the diplomacy needed to get the Eusebian faction on-side.
|
||||
|
||||
The new details on the Holy Spirit are interesting too. They suggest a delicate compromise. Some bishops were reluctant to suppose that the Father and the Spirit have the same essence. On the other hand, others reckoned that they must share the same essence, given that they are equally deserving of worship. Thus the creed does not have a 'homoousios' clause for the Spirit, but does assert that the Spirit 'together with the Father and with the Son is worshipped and glorified'. With a spoonful of humility, both sides can be satisfied with that.
|
||||
|
||||
The revised Nicene Creed was the focus point, the distillation of a growing theological movement, formed by the various anti-homoian bishops finding a way to keep true to their own convictions while respecting each other's red lines.
|
||||
|
||||
As a result of the context of 325, Athanasius' relentless anti-Arian polemic which kept that movement alive, and the 'neo-Arian' heterousian movement, the new Nicene tradition insisted on the full co-equal divinity of Father, Son and Holy Spirit. This doctrine ensures Nicenes can affirm that Christ mediates true knowledge of the transcendent Godhead to humanity: the one who was born of Mary, suffered and died on the cross, was raised from the dead and ascended into heaven was true God from true God, of the same essence as his Father.
|
||||
|
||||
To satisfy the Eusebian strain, which defined itself by opposition to Marcellus, the Nicene tradition included a commitment to a robust distinction between Father, Son and Spirit, and to the eternity of the Son: begotten of the Father before all ages, his kingdom shall have no end. As a result, Nicenes inherited a way of thinking about God's action in the world, as instrinsically co-operative without being divided.
|
||||
|
||||
The biggest change between 325 and 381 was not the text, but what the text is used for. In 325, the Creed functioned to condemn Arius in order to heal the divisions his teachings had caused. In its second life, the Creed found an altogether new purpose: to serve as a common statement of orthodox faith. It started life as a way to define who was out. It ended up defining who was in.
|
||||
|
||||
Where was Athanasius? Consider that when Athanasius was appointed bishop in 328, he was relatively young for a bishop at thirty-five. That means that in 381, he would have been the ripe old age of eighty-eight. In fact, he didn't make it that far: he died in peace in the countryside outside his native Alexandria in 373. If he had seen the outcome of 381, he might have regarded his life project complete. Perhaps he knew that with the new generation of bishops, the tide was turning for good, and died in peace. Perhaps not. Either way, his compromises, and his beloved homoousios, have left a permanent mark on the church.
|
||||
|
||||
This is the legacy of 381. It is two-faced: any common statement of faith can be used to exclude. Indeed, in the late fourth century, both non-Nicene Christians and pagans found themselves the victims of increasing state-backed sectarian violence.
|
||||
|
||||
However, 381 also bears witness to the power of humility and faith. Once we stop grasping at perfect knowledge we cannot attain, we can begin to appreciate the mystery of God. This is one legacy I hope we can carry forward into our century.
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
I looked through some concordances while I was bored in the New College Library the other day.
|
||||
|
||||
I really love Robert Young's Analytical Concordance. Young's is a concordance to the Authorised Version, so the entries are the English words used in the AV. But each English-language entry groups together all the words in the Greek New Testament and Hebrew Old Testament which correspond to that English word, and lists all the occurrences of each of those words in their respective source texts. That makes it a perfect tool for understanding the relationships between the biblical source texts. The only disadvantage is that it doesn't seem at all interested in the Septuagint, which is really important for understanding the way New Testament authors use the Old Testament.
|
||||
|
||||
It astonished me that there are concordances available of the NIV and ESV texts. Why would you want that? The texts don't correspond very well to the Greek or Hebrew source texts, and they don't have nearly as much clout as self-standing faith texts as the Authorised Version does. I really don't get it. If I had one, I would probably never open it.
|
||||
|
||||
I couldn't get ahold of a Strong's, but I did happen to drop in Armchair Books on the way back from the library, and they have a beautiful 19th-century Cruden's Concordance. Cruden's was the first English-language concordance when its eponymous author painstakingly constructed it in 1737, and has never been out of print since then. By the looks of it, it remains very relevant for English-speaking readers of the Bible. And going for a bargain as well... if you get there before me, I'll eat your head.
|
||||
@@ -0,0 +1,7 @@
|
||||
So I've now got half a dozen services running on my homelab across two machines. I need to install the services on each machine, keep them running after crashes and reboots, update and restart them when the source code changes, and co-ordinate them with each other (since I can only have one public IP on my home network, but I want to serve stuff over the Internet from both machines).
|
||||
|
||||
I am currently managing this with Podman, some Dockerfiles and Docker-Compose files, Makefiles, and a lot of manual interference. This just about works, and is probably reasonably sensible for the small, low-consequence situation.
|
||||
|
||||
Still, it is a faff. I'm wondering: is this kind of what Kubernetes is for? I've never understood. Looking at the documentation, I find it really hard to understand what Kubernetes is for. Maybe it's for situations kind of like this (only typically much bigger)?
|
||||
|
||||
Answers on a postcard please.
|
||||
@@ -0,0 +1,11 @@
|
||||
Writing is hard! I've just spent 2 days fixing 1 paragraph in my dissertation. The problem was to give the reader good reason to think that the fossil fuel industry exerts a strong influence on the majority epistemology. The end result is a single paragraph with four citations. You'd think that would take half a day, not 2 full days.
|
||||
|
||||
I think what made it hard was that I needed to gain understanding. If I just needed a fact or two, it would have been easier: skim a few abstracts, extract a few figures, call it a day. But I didn't know in advance what fossil-think looks like. I couldn't write a paragraph that was almost done but for a few missing statistics. I needed to know the whens, the hows, the whos.
|
||||
|
||||
What I ended up doing was collecting a score of browser tabs with relevant articles and ebooks (which was easy enough), then going through each one and reading it: properly reading it, for understanding, not just skimming abstracts.
|
||||
|
||||
This worked: after about half a dozen articles, I reviewed my notes, realised I had enough to be getting on with, and wrote up my paragraph. Along the way I gained a (very shallow) understanding of how the fossil fuel industry influences public discourse.
|
||||
|
||||
But: it took a heck of a long time for one paragraph.
|
||||
|
||||
I really wish I had an experienced researcher looking over my shoulder, popping by a couple of times a day, to offer advice. Maybe they would tell me: sorry, that's just how it is, you need to gain understanding and that takes time. Or maybe they would say: you should really be cutting the corner here, let me show you how. It's one of the many things you cannot get from a monthly check-in with a supervisor.
|
||||
@@ -0,0 +1,3 @@
|
||||
I've been following Ink and Switch's blog for a while. They're working creatively with computers. This is a great discussion about LLMs which covers what they are, how they are being abused to de-humanise the world, and how things could be different, concluding in a call for 'punk computing.' Let's have it!
|
||||
|
||||
=> https://www.inkandswitch.com/tangents/artificial/ Artificial | Ink and Switch
|
||||
+77
-23
@@ -1,41 +1,95 @@
|
||||
services:
|
||||
capsule:
|
||||
image: git.joeac.net/joeac/joeac.net-capsule
|
||||
build:
|
||||
context: .
|
||||
dockerfile: capsule.Dockerfile
|
||||
actualbudget:
|
||||
image: docker.io/actualbudget/actual-server:latest-alpine
|
||||
container_name: joeac.net-actualbudget
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ${ACTUALBUDGET_DATA_DIR}:/data/
|
||||
ports:
|
||||
- "1965:1965"
|
||||
- "${ACTUALBUDGET_PORT}:5006"
|
||||
|
||||
website:
|
||||
image: git.joeac.net/joeac/joeac.net-website
|
||||
gemini:
|
||||
image: git.joeac.net/joeac/${IMAGE_PREFIX}joeac.net-gemini
|
||||
container_name: joeac.net-gemini
|
||||
build:
|
||||
context: .
|
||||
dockerfile: website.Dockerfile
|
||||
args:
|
||||
LOCAL_SMTP_HOST: smtp
|
||||
LOCAL_SMTP_PORT: 2500
|
||||
LOCAL_SMTP_PASSWORD: smtp
|
||||
env_file:
|
||||
- .env
|
||||
dockerfile: gemini.Dockerfile
|
||||
ports:
|
||||
- "8000:4321"
|
||||
- "${GEMINI_PORT}:1965"
|
||||
volumes:
|
||||
- ${GEMINI_CERTIFICATES_DIR}:/var/app/.certificates
|
||||
- ${GEMINI_COMITIUM_DATA_DIR}:/var/app/comitium-data
|
||||
|
||||
http:
|
||||
image: git.joeac.net/joeac/${IMAGE_PREFIX}joeac.net-http
|
||||
container_name: joeac.net-http
|
||||
build:
|
||||
context: .
|
||||
dockerfile: http.Dockerfile
|
||||
environment:
|
||||
MAX_DAILY_EMAILS: ${MAX_DAILY_EMAILS}
|
||||
CONTACT_MAILBOX: ${CONTACT_MAILBOX}
|
||||
CONTACT_MAILBOX_NAME: ${CONTACT_MAILBOX_NAME}
|
||||
LOCAL_SMTP_FROM: ${LOCAL_SMTP_FROM}
|
||||
LOCAL_SMTP_FROM_NAME: ${LOCAL_SMTP_FROM_NAME}
|
||||
LOCAL_SMTP_HOST: ${LOCAL_SMTP_HOST}
|
||||
LOCAL_SMTP_PORT: ${LOCAL_SMTP_PORT}
|
||||
LOCAL_SMTP_USER: ${LOCAL_SMTP_USER}
|
||||
LOCAL_SMTP_PASSWORD: ${LOCAL_SMTP_PASSWORD}
|
||||
DB_SCHEME: ${DB_SCHEME}
|
||||
DB_PATH: ${DB_PATH}
|
||||
ports:
|
||||
- "${HTTP_PORT}:80"
|
||||
|
||||
etherpad:
|
||||
image: git.joeac.net/joeac/${IMAGE_PREFIX}joeac.net-etherpad
|
||||
container_name: joeac.net-etherpad
|
||||
build:
|
||||
context: .
|
||||
dockerfile: etherpad.Dockerfile
|
||||
args:
|
||||
ALPINE_VERSION: ${ALPINE_VERSION}
|
||||
ETHERPAD_VERSION: ${ETHERPAD_VERSION}
|
||||
volumes:
|
||||
- ${ETHERPAD_DATA_DIR}:/var/app/var
|
||||
environment:
|
||||
NODE_ENV: ${ETHERPAD_NODE_ENV}
|
||||
ADMIN_PASSWORD: ${ETHERPAD_ADMIN_PASSWORD}
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${ETHERPAD_PORT}:9001"
|
||||
|
||||
smtp:
|
||||
image: git.joeac.net/joeac/joeac.net-smtp
|
||||
image: git.joeac.net/joeac/${IMAGE_PREFIX}joeac.net-smtp
|
||||
container_name: joeac.net-smtp
|
||||
build:
|
||||
context: smtp
|
||||
context: .
|
||||
dockerfile: smtp.Dockerfile
|
||||
args:
|
||||
LOCAL_SMTP_PORT: 2500
|
||||
env_file:
|
||||
- .env
|
||||
SMTP_PORT: ${LOCAL_SMTP_PORT}
|
||||
environment:
|
||||
LOCAL_SMTP_PORT: 2500
|
||||
LOCAL_SMTP_PASSWORD: smtp
|
||||
LOCAL_SMTP_ENVELOPE_FROM: ${LOCAL_SMTP_ENVELOPE_FROM}
|
||||
REMOTE_SMTP_HOST: ${REMOTE_SMTP_HOST}
|
||||
REMOTE_SMTP_PORT: ${REMOTE_SMTP_PORT}
|
||||
REMOTE_SMTP_USER: ${REMOTE_SMTP_USER}
|
||||
SMTP_PORT: ${LOCAL_SMTP_PORT}
|
||||
SMTP_USER: ${LOCAL_SMTP_USER}
|
||||
SMTP_PASSWORD: ${LOCAL_SMTP_PASSWORD}
|
||||
REMOTE_SMTP_PASSWORD_FILE: /run/secrets/remote_smtp_password
|
||||
secrets:
|
||||
- remote_smtp_password
|
||||
|
||||
vaultwarden:
|
||||
image: docker.io/vaultwarden/server:latest
|
||||
container_name: joeac.net-vaultwarden
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DOMAIN: "https://pwd.joeac.net"
|
||||
volumes:
|
||||
- ${VAULTWARDEN_DATA_DIR}:/data/
|
||||
ports:
|
||||
- "${VAULTWARDEN_PORT}:80"
|
||||
|
||||
secrets:
|
||||
remote_smtp_password:
|
||||
external: true
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
ARG ALPINE_VERSION=3.23
|
||||
FROM docker.io/alpine:${ALPINE_VERSION}
|
||||
RUN mkdir -p /var/app
|
||||
WORKDIR /var/app
|
||||
ARG ETHERPAD_VERSION
|
||||
RUN apk --no-cache add pnpm \
|
||||
&& ENV=~/.profile SHELL=sh pnpm setup \
|
||||
&& source ~/.profile \
|
||||
&& pnpm install --global tsx
|
||||
COPY etherpad/etherpad-${ETHERPAD_VERSION} .
|
||||
CMD pnpx tsx src/node/server.ts
|
||||
@@ -0,0 +1,2 @@
|
||||
var
|
||||
etherpad-*
|
||||
@@ -0,0 +1,27 @@
|
||||
ETHERPAD_VERSION := 3.3.2
|
||||
SRC_DIR := etherpad-$(ETHERPAD_VERSION)
|
||||
|
||||
.PHONY: all
|
||||
all: download_etherpad install_deps
|
||||
|
||||
.PHONY: download_etherpad
|
||||
download_etherpad: $(SRC_DIR)
|
||||
|
||||
$(SRC_DIR):
|
||||
wget -O- https://github.com/ether/etherpad/archive/refs/tags/v3.3.2.tar.gz | tar -xz
|
||||
|
||||
.PHONY: install_deps
|
||||
install_deps: $(SRC_DIR)/node_modules
|
||||
|
||||
$(SRC_DIR)/node_modules: $(SRC_DIR) $(SRC_DIR)/settings.json $(SRC_DIR)/package.json $(SRC_DIR)/pnpm-lock.yaml
|
||||
cd $(SRC_DIR) && pnpm install
|
||||
|
||||
$(SRC_DIR)/settings.json: $(SRC_DIR) settings.json.template
|
||||
ifdef ETHERPAD_PORT
|
||||
ETHERPAD_PORT=$(ETHERPAD_PORT) envsubst -no-unset -i settings.json.template -o $(SRC_DIR)/settings.json
|
||||
else
|
||||
$(error ETHERPAD_PORT is not defined)
|
||||
endif
|
||||
|
||||
clean:
|
||||
rm -rf etherpad-*/
|
||||
@@ -0,0 +1,13 @@
|
||||
.PHONY: install
|
||||
install: $(ETHERPAD_DATA_DIR)
|
||||
|
||||
.PHONY: reinstall
|
||||
reinstall: $(ETHERPAD_DATA_DIR)
|
||||
|
||||
$(ETHERPAD_DATA_DIR):
|
||||
sudo mkdir -p $(ETHERPAD_DATA_DIR)
|
||||
ETHERPAD_USER=$(whoami) && sudo chown $$ETHERPAD_USER:$$ETHERPAD_USER $(ETHERPAD_DATA_DIR)
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall:
|
||||
$(info Nothing to do)
|
||||
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"title": "joeac's docs",
|
||||
"showRecentPads": true,
|
||||
"favicon": null,
|
||||
"skinName": "colibris",
|
||||
"skinVariants": "super-light-toolbar super-light-editor light-background",
|
||||
"ip": "0.0.0.0",
|
||||
"port": ${ETHERPAD_PORT},
|
||||
"showSettingsInAdminPage": true,
|
||||
"enableMetrics": "$${ENABLE_METRICS:true}",
|
||||
"cleanup": {
|
||||
"enabled": false,
|
||||
"keepRevisions": 5
|
||||
},
|
||||
"dbType": "sqlite",
|
||||
"dbSettings": {
|
||||
"filename": "var/etherpad.sq3"
|
||||
},
|
||||
"defaultPadText" : "",
|
||||
"padOptions": {
|
||||
"noColors": false,
|
||||
"showControls": true,
|
||||
"showChat": true,
|
||||
"showLineNumbers": true,
|
||||
"useMonospaceFont": false,
|
||||
"userName": null,
|
||||
"userColor": null,
|
||||
"rtl": false,
|
||||
"alwaysShowChat": false,
|
||||
"chatAndUsers": false,
|
||||
"lang": null
|
||||
},
|
||||
"padShortcutEnabled" : {
|
||||
"altF9": true,
|
||||
"altC": true,
|
||||
"cmdShift2": true,
|
||||
"delete": true,
|
||||
"return": true,
|
||||
"esc": true,
|
||||
"cmdS": true,
|
||||
"tab": true,
|
||||
"cmdZ": true,
|
||||
"cmdY": true,
|
||||
"cmdI": true,
|
||||
"cmdB": true,
|
||||
"cmdU": true,
|
||||
"cmd5": true,
|
||||
"cmdShiftL": true,
|
||||
"cmdShiftN": true,
|
||||
"cmdShift1": true,
|
||||
"cmdShiftC": true,
|
||||
"cmdH": true,
|
||||
"ctrlHome": true,
|
||||
"pageUp": true,
|
||||
"pageDown": true
|
||||
},
|
||||
"updateServer": "https://etherpad.org/ep_infos",
|
||||
"suppressErrorsInPadText": false,
|
||||
"requireSession": false,
|
||||
"editOnly": false,
|
||||
"minify": true,
|
||||
"maxAge": 21600,
|
||||
"soffice": null,
|
||||
"docxExport": true,
|
||||
"allowUnknownFileEnds": true,
|
||||
"requireAuthentication": false,
|
||||
"requireAuthorization": false,
|
||||
"trustProxy": false,
|
||||
"cookie": {
|
||||
"keyRotationInterval": 86400000,
|
||||
"sameSite": "Lax",
|
||||
"sessionLifetime": 864000000,
|
||||
"sessionRefreshInterval": 86400000,
|
||||
"sessionCleanup": true
|
||||
},
|
||||
"disableIPlogging": false,
|
||||
"automaticReconnectionTimeout": 0,
|
||||
"scrollWhenFocusLineIsOutOfViewport": {
|
||||
"percentage": {
|
||||
"editionAboveViewport": 0,
|
||||
"editionBelowViewport": 0
|
||||
},
|
||||
"duration": 0,
|
||||
"scrollWhenCaretIsInTheLastLineOfViewport": false,
|
||||
"percentageToScrollWhenUserPressesArrowUp": 0
|
||||
},
|
||||
"users": {
|
||||
"admin": {
|
||||
"password": "admin",
|
||||
"is_admin": true
|
||||
}
|
||||
},
|
||||
"socketTransportProtocols" : ["websocket", "polling"],
|
||||
"socketIo": {
|
||||
"maxHttpBufferSize": 1000000
|
||||
},
|
||||
"loadTest": false,
|
||||
"dumpOnUncleanExit": false,
|
||||
"importExportRateLimiting": {
|
||||
"windowMs": 90000,
|
||||
"max": 10
|
||||
},
|
||||
"importMaxFileSize": 52428800, // 50 * 1024 * 1024
|
||||
"authenticationMethod": "$${AUTHENTICATION_METHOD:sso}",
|
||||
"enableDarkMode": "$${ENABLE_DARK_MODE:true}",
|
||||
"enablePadWideSettings": "$${ENABLE_PAD_WIDE_SETTINGS:false}",
|
||||
"commitRateLimiting": {
|
||||
"duration": 1,
|
||||
"points": 10
|
||||
},
|
||||
"logLayoutType": "colored",
|
||||
"customLocaleStrings": {},
|
||||
"enableAdminUITests": false,
|
||||
"lowerCasePadIds": false,
|
||||
"sso": {
|
||||
"issuer": "$${SSO_ISSUER:http://localhost:${ETHERPAD_PORT}}",
|
||||
"clients": [
|
||||
{
|
||||
"client_id": "$${ADMIN_CLIENT:admin_client}",
|
||||
"client_secret": "$${ADMIN_SECRET:admin}",
|
||||
"grant_types": ["authorization_code"],
|
||||
"response_types": ["code"],
|
||||
"redirect_uris": ["$${ADMIN_REDIRECT:http://localhost:${ETHERPAD_PORT}/admin/}"]
|
||||
},
|
||||
{
|
||||
"client_id": "$${USER_CLIENT:user_client}",
|
||||
"client_secret": "$${USER_SECRET:user}",
|
||||
"grant_types": ["authorization_code"],
|
||||
"response_types": ["code"],
|
||||
"redirect_uris": ["$${USER_REDIRECT:http://localhost:${ETHERPAD_PORT}/}"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+26
-2
@@ -1,6 +1,11 @@
|
||||
# The hostname to use for the local SMTP server
|
||||
LOCAL_SMTP_HOST=smtp
|
||||
|
||||
# The username for authenticating to the local SMTP server
|
||||
# Recommended to be the same as REMOTE_SMTP_USER, else the remote may reject it
|
||||
LOCAL_SMTP_USER=
|
||||
LOCAL_SMTP_USER=smtp
|
||||
|
||||
# The password for authenticating to the local SMTP server
|
||||
LOCAL_SMTP_PASSWORD=smtp
|
||||
|
||||
# The envelope-from used by the local SMTP server
|
||||
# Theoretically should be "LOCAL_SMTP_FROM_NAME <LOCAL_SMTP_FROM>", but in
|
||||
@@ -26,3 +31,22 @@ REMOTE_SMTP_USER=
|
||||
|
||||
# The email address where contact emails will be sent
|
||||
CONTACT_MAILBOX=
|
||||
|
||||
# The name to associate with the contact mailbox
|
||||
CONTACT_MAILBOX_NAME=
|
||||
|
||||
# When this 24-hour limit is reached, the website will no longer
|
||||
# send emails to the contact mailbox
|
||||
MAX_DAILY_EMAILS=
|
||||
|
||||
# The scheme part of the URI to the website database
|
||||
DB_SCHEME=sqlite
|
||||
|
||||
# The path part of the URI to the website database
|
||||
DB_PATH=/var/app/joeac.net-http.sqlite
|
||||
|
||||
# The Node.JS environment for the Etherpad instance (production, development)
|
||||
ETHERPAD_NODE_ENV=production
|
||||
|
||||
# The admin password for the Etherpad instance
|
||||
ETHERPAD_ADMIN_PASSWORD=
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM git.joeac.net/joeac/${IMAGE_PREFIX}agate:3.3.20-alpine3.23 AS agate
|
||||
FROM git.joeac.net/joeac/${IMAGE_PREFIX}comitium:1.8.2-alpine3.23 AS comitium
|
||||
FROM git.joeac.net/joeac/${IMAGE_PREFIX}crond:1.37.0-r30-alpine3.23 AS final
|
||||
RUN apk --no-cache add gcc # dependency for agate
|
||||
COPY --from=agate /root/.cargo/bin/agate /usr/local/bin/agate
|
||||
COPY --from=comitium /usr/local/bin/comitium /usr/local/bin/comitium
|
||||
|
||||
RUN mkdir -p /var/app/content
|
||||
WORKDIR /var/app
|
||||
RUN crontab -l > crontab.tmp \
|
||||
&& echo "0 */6 * * * /usr/local/bin/comitium refresh --data /var/app/comitium-data" >> crontab.tmp \
|
||||
&& crontab crontab.tmp \
|
||||
&& rm crontab.tmp
|
||||
|
||||
COPY gemini/run.sh /var/app/
|
||||
COPY gemini/content /var/app/content
|
||||
COPY common /var/common
|
||||
|
||||
CMD ./run.sh
|
||||
@@ -6,8 +6,8 @@ LOGLOG_HEADER := $(TEMPLATES_DIR)/loglog-header.gmi
|
||||
MICROLOG_ENTRIES := $(wildcard microlog/*)
|
||||
LONGLOG_ENTRIES := $(wildcard content/logs/longlog/*)
|
||||
LOGLOG_TARGET := content/logs/loglog.gmi
|
||||
FEED_TARGET := $(COMITIUM_DATA)/subscriptions.json $(COMITIUM_DATA)/feed.gmi $(COMITIUM_DATA)/subscriptions.gmi
|
||||
FEED_OUTPUTS := $(FEED_TARGET) $(COMITIUM_DATA)/subscriptions.json $(COMITIUM_DATA)/feed.gmi $(COMITIUM_DATA)/subscriptions.gmi
|
||||
FEED_TARGET := $(COMITIUM_DATA)/comitium.json $(COMITIUM_DATA)/feed.gmi $(COMITIUM_DATA)/subscriptions.gmi
|
||||
FEED_OUTPUTS := $(FEED_TARGET) $(COMITIUM_DATA)/comitium.json $(COMITIUM_DATA)/feed.gmi $(COMITIUM_DATA)/subscriptions.gmi
|
||||
MICROLOG_TARGET := content/logs/microlog.gmi
|
||||
MICROLOG_OUTPUTS := $(MICROLOG_TARGET) $(wildcard content/logs/microlog/*)
|
||||
LONGLOG_TARGET := content/logs/longlog.gmi
|
||||
@@ -25,7 +25,7 @@ $(LOGLOG_TARGET): $(MICROLOG_TARGET) $(LONGLOG_TARGET) $(LOGLOG_HEADER)
|
||||
bash ./build_loglog.bash
|
||||
|
||||
$(FEED_TARGET): $(COMITIUM_DATA)/feeds.txt
|
||||
rm -f $(COMITIUM_DATA)/comitium.json
|
||||
rm -f $(FEED_TARGET); \
|
||||
while read feed; do \
|
||||
comitium add --data "$(COMITIUM_DATA)" $$feed; \
|
||||
done <$(COMITIUM_DATA)/feeds.txt
|
||||
@@ -0,0 +1,22 @@
|
||||
GEMINI_COMITIUM_DATA_FILES := $(addprefix $(GEMINI_COMITIUM_DATA_DIR)/,$(notdir $(wildcard comitium-data/*)))
|
||||
|
||||
.PHONY: install
|
||||
install: $(GEMINI_CERTIFICATES_DIR) $(GEMINI_COMITIUM_DATA_FILES)
|
||||
|
||||
.PHONY: reinstall
|
||||
reinstall: $(GEMINI_CERTIFICATES_DIR) $(GEMINI_COMITIUM_DATA_FILES)
|
||||
|
||||
$(GEMINI_CERTIFICATES_DIR):
|
||||
sudo mkdir -p $(GEMINI_CERTIFICATES_DIR)
|
||||
$(let gemini_user,$(shell whoami),sudo chown -R $(gemini_user):$(gemini_user) $(GEMINI_CERTIFICATES_DIR))
|
||||
|
||||
$(GEMINI_COMITIUM_DATA_DIR)/%: comitium-data/% $(GEMINI_COMITIUM_DATA_DIR)
|
||||
sudo cp $< $@
|
||||
$(let gemini_user,$(shell whoami),sudo chown $(gemini_user):$(gemini_user) $@)
|
||||
|
||||
$(GEMINI_COMITIUM_DATA_DIR):
|
||||
sudo mkdir -p $(GEMINI_COMITIUM_DATA_DIR)
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall:
|
||||
sudo rm -rf $(GEMINI_COMITIUM_DATA_DIR)
|
||||
@@ -1,5 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
make && \
|
||||
( ( sleep 5; comitium refresh --data comitium-data/ ) & ) \
|
||||
( ( sleep 5; comitium refresh --data comitium-data/ ) & ) \
|
||||
&& agate --content content/ --addr [::]:1965 --addr 0.0.0.0:1965 --lang en-GB
|
||||
@@ -0,0 +1,13 @@
|
||||
FROM docker.io/arm32v7/nginx:1.30.3-alpine3.23
|
||||
RUN apk --update add php85-fpm php85-pdo php85-pdo_sqlite
|
||||
COPY http/vendor /usr/share/nginx/vendor
|
||||
COPY http/php /usr/share/nginx/php
|
||||
COPY http/out /usr/share/nginx/html
|
||||
COPY http/php-fpm.conf /etc/php85/php-fpm.d/joeac.net-http_pool.conf
|
||||
COPY http/nginx.conf /etc/nginx/nginx.conf
|
||||
CMD mkdir -p "$(dirname "$DB_PATH")" \
|
||||
&& touch $DB_PATH \
|
||||
&& chown -R nginx:nginx "$(dirname "$DB_PATH")" \
|
||||
&& chmod -R 700 "$(dirname "$DB_PATH")" \
|
||||
&& php-fpm85 \
|
||||
&& nginx -g "daemon off;"
|
||||
+6
-2
@@ -1,4 +1,8 @@
|
||||
out
|
||||
vendor
|
||||
src/microlog
|
||||
src/blog
|
||||
src/microlog/*.microlog.upphtml
|
||||
src/microlog/*.microlog.upphtml.args
|
||||
src/blog/*.blog.upphtml
|
||||
src/blog/*.blog.upphtml.args
|
||||
bin/pp
|
||||
share/man/man1/pp.1
|
||||
|
||||
+125
-79
@@ -1,97 +1,132 @@
|
||||
all: microlog blog public out
|
||||
include config.mk
|
||||
|
||||
.PHONY: prune
|
||||
prune: prune_microlog prune_blog prune_out
|
||||
|
||||
.SILENT: prune_out
|
||||
.PHONY: prune_out
|
||||
prune_out:
|
||||
for out in $$(find out -type f); do \
|
||||
out_basename="$$(basename "$$out")"; \
|
||||
out_ext="$${out_basename##*\.}"; \
|
||||
rel_path="$$(echo "$${out}" | cut -c5-)"; \
|
||||
if [ -f "public/$${rel_path}" ]; then \
|
||||
continue 2; \
|
||||
fi; \
|
||||
base_rel_path="$${rel_path%%.$${out_ext}}"; \
|
||||
src_path="src/$${base_rel_path}.upp$${out_ext}"; \
|
||||
if [ -f "$$src_path" ]; then \
|
||||
continue 2; \
|
||||
fi; \
|
||||
for layout in $$(find share/layouts -name "*.layout.upp$${out_ext}"); do \
|
||||
layout="$$(basename "$$layout")"; \
|
||||
layout="$${layout::$${#layout}-15}"; \
|
||||
rel_path="$$(echo "$${out}" | cut -c5-)"; \
|
||||
base_rel_path="$${rel_path%%.$${out_ext}}"; \
|
||||
src_path="src/$${base_rel_path}.$${layout}.upp$${out_ext}"; \
|
||||
if [ -f "$$src_path" ]; then \
|
||||
continue 2; \
|
||||
fi; \
|
||||
done; \
|
||||
echo "No source file found to match $${out}: removing it"; \
|
||||
rm "$$out"; \
|
||||
done
|
||||
#############
|
||||
# VARIABLES #
|
||||
#############
|
||||
|
||||
.SILENT: prune_microlog
|
||||
.PHONY: prune_microlog
|
||||
prune_microlog:
|
||||
for post in $$(find src/microlog -name *.microlog.upphtml); do \
|
||||
post_basename="$$(basename "$$post")"; \
|
||||
post_src="blogs/microlog/$${post_basename::$${#post_basename}-17}.gmi"; \
|
||||
if ! [ -f "$$post_src" ]; then \
|
||||
echo "Microlog post source file $${post_src} has been deleted: removing $${post}"; \
|
||||
rm -f "$$post" "$${post}.args"; \
|
||||
fi; \
|
||||
done
|
||||
|
||||
.SILENT: prune_blog
|
||||
.PHONY: prune_blog
|
||||
prune_blog:
|
||||
for post in $$(find src/blog -name *.blog.upphtml); do \
|
||||
post_basename="$$(basename "$$post")"; \
|
||||
post_src="blogs/blog/$${post_basename::$${#post_basename}-13}.gmi"; \
|
||||
if ! [ -f "$$post_src" ]; then \
|
||||
echo "Blog post source file $${post_src} has been deleted: removing $${post}"; \
|
||||
rm -f "$$post" "$${post}.args"; \
|
||||
fi; \
|
||||
done
|
||||
|
||||
src_files := $(filter-out %.args,$(wildcard src/*.upp*))
|
||||
src_files += $(filter-out %.args,$(wildcard src/**/*.upp*))
|
||||
src_files := $(filter-out %.args,$(wildcard src/*.upp*)) $(filter-out %.args,$(wildcard src/**/*.upp*))
|
||||
src_files_ext := $(suffix $(src_files))
|
||||
out_files_ext := $(src_files_ext:.upp%=%)
|
||||
out_deps_ext := $(src_files_ext:.upp%=%)
|
||||
|
||||
component_files := $(shell find -L share/components -type f)
|
||||
layout_files := $(wildcard share/layouts/*.layout.upp*)
|
||||
layouts := $(notdir $(basename $(basename $(layout_files))))
|
||||
out_files := $(addprefix out/,$(src_files:src/%=%))
|
||||
remove_layout_from_out_files = out_files := $$(subst $(1).upp,.,$$(out_files))
|
||||
$(foreach layout,$(layouts),$(eval $(call remove_layout_from_out_files,.$(layout))))
|
||||
$(eval $(call remove_layout_from_out_files,))
|
||||
|
||||
define out_rule_for_ext_and_layout =
|
||||
out/%.$(1): src/%$(2).upp$(1)
|
||||
remove_all_layouts = $(call remove_layouts,$(layouts),$(1))
|
||||
remove_layouts = $(let first rest,$(1),$(call remove_layout,$(first),$(if $(rest),$(call remove_layouts,$(rest),$(2)),$(2))))
|
||||
remove_layout = $(subst .$(1).upp,.,$(2))
|
||||
out_deps := $(subst .upp,.,$(call remove_all_layouts,$(addprefix out/,$(src_files:src/%=%))))
|
||||
out_deps_without_src := $(filter-out $(out_deps),$(shell find -L out -type f))
|
||||
out_deps += $(out_deps_without_src)
|
||||
|
||||
blogs := $(subst blogs/,,$(wildcard blogs/*))
|
||||
|
||||
|
||||
#############
|
||||
# FUNCTIONS #
|
||||
#############
|
||||
|
||||
blog_post_target = src/$(blog)/%$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).upphtml
|
||||
blog_post_src = blogs/$(blog)/%.$(BLOG_EXT_$(blog)) $$(wildcard bin/*)
|
||||
blog_src = $(patsubst \
|
||||
blogs/$(blog)/%.$(BLOG_EXT_$(blog)),\
|
||||
src/$(blog)/%$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).upphtml,\
|
||||
$(shell find -L "blogs/$(blog)" -type f -and -name *.$(BLOG_EXT_$(blog))))
|
||||
blog_src_without_post_src = $(filter-out $(blog_src),$(shell find -L "src/$(blog)" -type f -and -name *.$(BLOG_LAYOUT_$(blog)).upphtml))
|
||||
default_layout_file = $(if \
|
||||
$(shell [ -f "share/layouts/default.layout.$(ext)" ] && echo 1),\
|
||||
share/layouts/default.layout.$(ext))
|
||||
layout_file = $(if $(layout),share/layouts/$(layout).layout.upp$(ext),$(default_layout_file))
|
||||
|
||||
define out_rule =
|
||||
out/%.$(ext): src/%$(if $(layout),.$(layout)).upp$(ext) $(layout_file) $(component_files) bin/pp
|
||||
./bin/mkws https://joeac.net "$$$$(realpath $$<)"
|
||||
endef
|
||||
|
||||
$(foreach ext,$(sort $(out_files_ext)),\
|
||||
$(eval $(call out_rule_for_ext_and_layout,$(ext),)))
|
||||
$(foreach layout,$(layouts),\
|
||||
$(foreach ext,$(sort $(out_files_ext)),\
|
||||
$(eval $(call out_rule_for_ext_and_layout,$(ext),.$(layout)))))
|
||||
define blog_rss_feed_rule =
|
||||
out/$(blog)/rss.xml: src/$(blog)/rss.uppxml $(blog_src) $(wildcard bin/*) bin/pp
|
||||
./bin/mkws https://joeac.net "$$(realpath src/$(blog)/rss.uppxml)"
|
||||
endef
|
||||
|
||||
define blog_rule =
|
||||
.PHONY: $(blog)
|
||||
$(blog): $(blog_src_without_post_src) $(blog_src) out/$(blog).html
|
||||
endef
|
||||
|
||||
define blog_post_rule =
|
||||
$(blog_post_target): $(blog_post_src) $(let ext,html,$(layout_file) $(component_files))
|
||||
./bin/blog \
|
||||
-i $(blog) \
|
||||
$(if $(BLOG_LAYOUT_$(blog)),-l $(BLOG_LAYOUT_$(blog))) \
|
||||
$(BLOG_FLAGS_$(blog)) \
|
||||
-f $$*.$(BLOG_EXT_$(blog))
|
||||
endef
|
||||
|
||||
define blog_index_rule =
|
||||
out/$(blog).html: src/$(blog).upphtml $(blog_src) $(wildcard bin/*) bin/pp
|
||||
./bin/mkws https://joeac.net "$$(realpath src/$(blog).upphtml)"
|
||||
endef
|
||||
|
||||
define prune_blog_src_file_rule =
|
||||
$(let rel_basename,$(patsubst src/$(blog)/%.$(BLOG_LAYOUT_$(blog)).upphtml,%,$(file)),
|
||||
blogs/$(blog)/$(rel_basename).$(BLOG_EXT_$(blog)):
|
||||
rm $(file)
|
||||
)
|
||||
endef
|
||||
|
||||
define prune_blog_out_file_rule =
|
||||
$(let rel_basename,$(patsubst src/$(blog)/%.$(BLOG_LAYOUT_$(blog)).upphtml,%,$(file)),
|
||||
$(file):
|
||||
rm out/$(rel_basename).html
|
||||
)
|
||||
endef
|
||||
|
||||
define prune_out_file_rule =
|
||||
$(let orig_ext,$(patsubst .%,%,$(suffix $(file))),
|
||||
$(let ext,$(patsubst upp%,%,$(orig_ext)),
|
||||
$(let rel_basename,$(patsubst out/%.$(orig_ext),%,$(file)),
|
||||
$(let src_file,src/$(rel_basename).upp$(ext),
|
||||
$(let src_files_with_layouts,$(foreach layout,$(layouts),src/$(rel_basename).$(layout).upp$(ext)),
|
||||
$(let public_file,public/$(rel_basename).$(orig_ext),
|
||||
$(src_file) $(src_files_with_layouts) $(public_file):
|
||||
rm $(file)
|
||||
))))))
|
||||
endef
|
||||
|
||||
|
||||
#########
|
||||
# RULES #
|
||||
#########
|
||||
|
||||
all: $(blogs) public out vendor
|
||||
$(MAKE) out # if blog posts have been pruned, outfiles must be re-calculated
|
||||
|
||||
share/man/man1/pp.1:
|
||||
cp pp/pp.1 share/man/man1/pp.1
|
||||
|
||||
.PHONY: out
|
||||
out: $(out_files)
|
||||
out: $(out_deps)
|
||||
|
||||
.PHONY: microlog
|
||||
microlog: $(patsubst blogs/microlog/%.gmi,src/microlog/%.microlog.upphtml,$(wildcard blogs/microlog/*.gmi))
|
||||
bin/pp:
|
||||
$(MAKE) --directory=pp && chmod +x pp/pp && cp pp/pp bin/pp
|
||||
|
||||
src/microlog/%.microlog.upphtml: blogs/microlog/%.gmi bin/blog bin/gmi2upphtml
|
||||
./bin/blog -i microlog -l microlog -t -p gmi2upphtml -f $*.gmi
|
||||
$(foreach ext,$(sort $(out_deps_ext)),\
|
||||
$(eval $(out_rule)))
|
||||
$(foreach layout,$(layouts),\
|
||||
$(foreach ext,$(sort $(out_deps_ext)),\
|
||||
$(eval $(out_rule))))
|
||||
|
||||
.PHONY: blog
|
||||
blog: $(patsubst blogs/blog/%.gmi,src/blog/%.blog.upphtml,$(wildcard blogs/blog/*.gmi))
|
||||
$(foreach blog,$(blogs),$(eval $(blog_rule)))
|
||||
$(foreach blog,$(blogs),$(eval $(blog_index_rule)))
|
||||
|
||||
src/blog/%.blog.upphtml: blogs/blog/%.gmi bin/blog bin/gmi2upphtml bin/cut_longlog_meta
|
||||
./bin/blog -i blog -l blog -p 'cut_longlog_meta | gmi2upphtml' -a extract_longlog_meta -f $*.gmi
|
||||
$(foreach blog,$(blogs),\
|
||||
$(foreach layout,$(layouts),\
|
||||
$(eval $(call blog_post_rule,$(blog),$(layout)))))
|
||||
$(foreach blog,$(blogs),\
|
||||
$(eval $(call blog_post_rule,$(blog),)))
|
||||
|
||||
$(foreach blog,$(blogs),$(eval $(blog_rss_feed_rule)))
|
||||
|
||||
.PHONY: public
|
||||
public: $(subst public/,out/,$(shell find -L public/ -type f))
|
||||
@@ -99,6 +134,17 @@ public: $(subst public/,out/,$(shell find -L public/ -type f))
|
||||
out/%: public/%
|
||||
mkdir -p $(dir $@) && cp $< $@
|
||||
|
||||
$(foreach file,$(out_deps_without_src),$(eval $(prune_out_file_rule)))
|
||||
$(foreach blog,$(blogs),$(foreach file,$(blog_src_without_post_src),$(eval $(prune_blog_src_file_rule))))
|
||||
|
||||
vendor: composer.json composer.lock
|
||||
composer install
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf out src/microlog src/blog
|
||||
rm -rf \
|
||||
vendor \
|
||||
out \
|
||||
$(foreach blog,$(blogs), \
|
||||
$(foreach ext,upphtml upphtml.args,\
|
||||
src/$(blog)/*$(if $(BLOG_LAYOUT_$(blog)),.$(BLOG_LAYOUT_$(blog))).$(ext)))
|
||||
|
||||
+5
-6
@@ -37,7 +37,7 @@ USAGE
|
||||
extract_args_cmd="head -n 0 | echo \"POST_TITLE=\\\"\${POST_TITLE}\\\"\""
|
||||
file_pattern="*"
|
||||
log_name=""
|
||||
layout="default"
|
||||
layout_name="default"
|
||||
postprocess_cmd="echo"
|
||||
use_filenames_for_titles=0
|
||||
|
||||
@@ -99,12 +99,11 @@ do
|
||||
dest="${SRC}/${log_name}/${name}${layout_ext}.upphtml"
|
||||
if [ $use_filenames_for_titles -eq 1 ]
|
||||
then
|
||||
title="$name"
|
||||
cat "$entry" | eval "POST_TITLE=\"${title}\" ${postprocess_cmd}" > "$dest"
|
||||
export POST_TITLE="$name"
|
||||
cat "$entry" | eval "${postprocess_cmd}" > "$dest"
|
||||
else
|
||||
title="$(grep --regexp "^# " --max-count 1 "$entry" | cut -c3-)"
|
||||
export POST_TITLE="$title"
|
||||
cat "$entry" | grep --regexp "^# " --invert-match | eval "$postprocess_cmd" > "$dest"
|
||||
export POST_TITLE="$(grep -e "^# " -m 1 "$entry" | cut -c3-)"
|
||||
cat "$entry" | grep -e "^# " -v | eval "$postprocess_cmd" > "$dest"
|
||||
fi
|
||||
cat "$entry" | eval "$extract_args_cmd" > "${dest}.args"
|
||||
echo "Processed ${entry#$(dirname ${BLOGS})/} to ${dest#$(dirname ${SRC})/}"
|
||||
|
||||
@@ -53,7 +53,14 @@ close_all_open_blocks()
|
||||
|
||||
escape_upphtml()
|
||||
{
|
||||
echo "$1" | sed "s/\`/\\\\\`/g" | sed "s/\"/\\\\\"/g" | sed "s/\\\$/\\\\\$/g"
|
||||
echo "$1" \
|
||||
| sed "s/&/\\&/g" \
|
||||
| sed "s/</\\</g" \
|
||||
| sed "s/>/\\>/g" \
|
||||
| sed "s/\"/\\"/g" \
|
||||
| sed "s/'/\\'/g" \
|
||||
| sed "s/\`/\\`/g" \
|
||||
| sed "s/\\\$/\\\\\$/g"
|
||||
}
|
||||
|
||||
close_all_open_blocks_except()
|
||||
|
||||
BIN
Binary file not shown.
Generated
+5
-5
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "752c91265ea3414ab13b2ff0f8925706",
|
||||
"content-hash": "5145609ea1f80d6d838d0f9649acd36a",
|
||||
"packages": [
|
||||
{
|
||||
"name": "phpmailer/phpmailer",
|
||||
@@ -92,10 +92,10 @@
|
||||
"packages-dev": [],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": [],
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": [],
|
||||
"platform-dev": [],
|
||||
"plugin-api-version": "2.3.0"
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
BLOG_LAYOUT_blog := blog
|
||||
BLOG_EXT_blog := gmi
|
||||
BLOG_FLAGS_blog = -p 'cut_longlog_meta | gmi2upphtml' -a extract_longlog_meta
|
||||
|
||||
BLOG_LAYOUT_microlog := microlog
|
||||
BLOG_EXT_microlog := gmi
|
||||
BLOG_FLAGS_microlog := -t -p gmi2upphtml
|
||||
@@ -0,0 +1,47 @@
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
error_log /var/log/nginx/error.log notice;
|
||||
pid /run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name joeac.net;
|
||||
|
||||
access_log /var/log/nginx/host.access.log main;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
try_files $uri $uri.html $uri/index.html $uri.php $uri/index.php =404;
|
||||
}
|
||||
|
||||
error_page 404 /error.html;
|
||||
|
||||
location ~ \.php$ {
|
||||
root html;
|
||||
fastcgi_pass unix:/run/php8.5-fpm-joeac.net-http.sock;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME /usr/share/nginx/html$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
location ~ /\.sqlite {
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
[joeac.net]
|
||||
user = nginx
|
||||
group = nginx
|
||||
listen = /run/php8.5-fpm-joeac.net-http.sock
|
||||
listen.owner = nginx
|
||||
listen.group = nginx
|
||||
listen.mode = 0660
|
||||
|
||||
access.log = /var/log/php85/$pool.access.log
|
||||
access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{milli}d %{kilo}M %C%%"
|
||||
catch_workers_output = yes
|
||||
php_admin_value[error_log] = /var/log/php85/$pool.error.log
|
||||
php_admin_flag[log_errors] = on
|
||||
|
||||
php_admin_value[disable_functions] = exec,passthru,shell_exec,system
|
||||
php_admin_flag[allow_url_fopen] = off
|
||||
|
||||
pm = dynamic
|
||||
pm.max_children = 20
|
||||
pm.start_servers = 4
|
||||
pm.min_spare_servers = 2
|
||||
pm.max_spare_servers = 8
|
||||
pm.max_requests = 500
|
||||
|
||||
clear_env = no
|
||||
env[MAX_DAILY_EMAILS] = $MAX_DAILY_EMAILS
|
||||
env[CONTACT_MAILBOX] = $CONTACT_MAILBOX
|
||||
env[CONTACT_MAILBOX_NAME] = $CONTACT_MAILBOX_NAME
|
||||
env[LOCAL_SMTP_FROM] = $LOCAL_SMTP_FROM
|
||||
env[LOCAL_SMTP_FROM_NAME] = $LOCAL_SMTP_FROM_NAME
|
||||
env[LOCAL_SMTP_HOST] = $LOCAL_SMTP_HOST
|
||||
env[LOCAL_SMTP_PORT] = $LOCAL_SMTP_PORT
|
||||
env[LOCAL_SMTP_USER] = $LOCAL_SMTP_USER
|
||||
env[LOCAL_SMTP_PASSWORD] = $LOCAL_SMTP_PASSWORD
|
||||
env[DB_SCHEME] = $DB_SCHEME
|
||||
env[DB_PATH] = $DB_PATH
|
||||
@@ -0,0 +1,2 @@
|
||||
pp
|
||||
*.o
|
||||
@@ -0,0 +1,15 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2021, Adrian Emil Grigore <adrian.emil.grigore@gmail.com>
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
@@ -0,0 +1,29 @@
|
||||
CC = cc
|
||||
LD = ${CC}
|
||||
|
||||
PREFIX = /usr/local
|
||||
MANPREFIX = ${PREFIX}/share/man
|
||||
|
||||
CFLAGS = -Wall -Wextra -pedantic
|
||||
|
||||
pp: pp.o
|
||||
pp.o: pp.c
|
||||
|
||||
.c.o:
|
||||
$(CC) $(CPPFLAGS) $< $(CFLAGS) -c -o $@
|
||||
.o:
|
||||
$(LD) $< $(LDFLAGS) -o $@
|
||||
|
||||
clean:
|
||||
rm -rf pp *.o
|
||||
|
||||
install: pp pp.1
|
||||
mkdir -p $(DESTDIR)$(PREFIX)/bin
|
||||
mkdir -p $(DESTDIR)$(MANPREFIX)/man1
|
||||
cp pp $(DESTDIR)$(PREFIX)/bin/pp
|
||||
cp pp.1 $(DESTDIR)$(MANPREFIX)/man1/
|
||||
chmod 755 $(DESTDIR)$(PREFIX)/bin/pp
|
||||
|
||||
uninstall:
|
||||
rm $(DESTDIR)$(PREFIX)/bin/pp
|
||||
rm $(DESTDIR)$(MANPREFIX)/man1/pp.1
|
||||
@@ -0,0 +1,188 @@
|
||||
# `pp(1)`
|
||||
|
||||
a preprocessor
|
||||
|
||||
`pp(1)` allows embedding
|
||||
[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
|
||||
code in files of any type by nesting it inside the `#!\n` token, where
|
||||
`\n` is a new line. That means that if you'd like a simple loop or an
|
||||
`if` inside an HTML file for instance,
|
||||
you could use `pp(1)`.
|
||||
|
||||
## Basic usage
|
||||
|
||||
For the following code:
|
||||
|
||||
<!doctype html>
|
||||
<title>pp(1) example</title>
|
||||
<ul>
|
||||
#!
|
||||
i=1
|
||||
while test $i -le 10
|
||||
do
|
||||
if test $((i % 2)) -eq 0
|
||||
then
|
||||
#!
|
||||
<li class=even>$i</li>
|
||||
#!
|
||||
else
|
||||
#!
|
||||
<li class=odd>$i</li>
|
||||
#!
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
#!
|
||||
</ul>
|
||||
|
||||
`pp(1)` outputs:
|
||||
|
||||
<!doctype html>
|
||||
<title>pp(1) example</title>
|
||||
<ul>
|
||||
<li class=odd>1</li>
|
||||
<li class=even>2</li>
|
||||
<li class=odd>3</li>
|
||||
<li class=even>4</li>
|
||||
<li class=odd>5</li>
|
||||
<li class=even>6</li>
|
||||
<li class=odd>7</li>
|
||||
<li class=even>8</li>
|
||||
<li class=odd>9</li>
|
||||
<li class=even>10</li>
|
||||
</ul>
|
||||
|
||||
## Arguments
|
||||
|
||||
You can also pass arguments to it by appending them to the command call:
|
||||
|
||||
<!doctype html>
|
||||
<title>$1</title>
|
||||
<p>
|
||||
#!
|
||||
echo $2
|
||||
#!
|
||||
</p>
|
||||
|
||||
Calling the above code with
|
||||
`pp tmp.upphtml 'pp(1) example' 'hello, world'`
|
||||
will result in:
|
||||
|
||||
<!doctype html>
|
||||
<title>pp(1) example</title>
|
||||
<p>
|
||||
hello world
|
||||
</p>
|
||||
|
||||
## Pipes
|
||||
|
||||
`pp(1)`'s `stdin` is sent to the child
|
||||
[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
|
||||
|
||||
<!doctype html>
|
||||
<title>pp(1) example</title>
|
||||
<p>
|
||||
#!
|
||||
cat
|
||||
#!
|
||||
</p>
|
||||
|
||||
`echo 'hello, world' | pp tmp.upphtml` will output:
|
||||
|
||||
<!doctype html>
|
||||
<title>pp(1) example</title>
|
||||
<p>
|
||||
hello, world
|
||||
</p>
|
||||
|
||||
## Debugging
|
||||
|
||||
`pp(1)` also takes an optional `-d` flag. If passed, `pp(1)` will dump
|
||||
the generated
|
||||
[`sh(1)`](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html)
|
||||
code instead of executing it:
|
||||
|
||||
<!doctype html>
|
||||
<title>pp(1) example</title>
|
||||
<ul>
|
||||
#!
|
||||
i=1
|
||||
while test $i -le 10
|
||||
do
|
||||
if test $((i % 2)) -eq 0
|
||||
then
|
||||
#!
|
||||
<li class=even>$i</li>
|
||||
#!
|
||||
else
|
||||
#!
|
||||
<li class=odd>$i</li>
|
||||
#!
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
#!
|
||||
</ul>
|
||||
|
||||
`pp -d tmp.upphtml` will output:
|
||||
|
||||
echo "<!doctype html>
|
||||
<title>pp(1) example</title>
|
||||
<ul>"
|
||||
i=1
|
||||
while test $i -le 10
|
||||
do
|
||||
if test $((i % 2)) -eq 0
|
||||
then
|
||||
echo " <li class=even>$i</li>"
|
||||
else
|
||||
echo " <li class=odd>$i</li>"
|
||||
fi
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo "</ul>"
|
||||
|
||||
## Example
|
||||
|
||||
<!doctype html>
|
||||
<html lang=en>
|
||||
<title>pp(1)</title>
|
||||
<meta charset=UTF-8>
|
||||
<meta name=viewport content='width=device-width'>
|
||||
<link rel=stylesheet href=../theme.css>
|
||||
<body>
|
||||
<a href=../#myprojects>Home</a>
|
||||
#!
|
||||
addcls() {
|
||||
sed "s/<code>/<code class=language-bash>/g"
|
||||
}
|
||||
smu README | addcls
|
||||
#!
|
||||
<h2>Files</h2>
|
||||
<ul>
|
||||
#!
|
||||
for f in *
|
||||
do
|
||||
case $f in
|
||||
*.upp*|*.html|*.tgz)
|
||||
continue
|
||||
;;
|
||||
esac
|
||||
#!
|
||||
<li><a href=$f>$f</a></li>
|
||||
#!
|
||||
done
|
||||
#!
|
||||
</ul>
|
||||
<h2>Download</h2>
|
||||
<a href=pp.tgz>pp.tgz</a>
|
||||
<script src=../prism.js data-manual></script>
|
||||
<script src=../enhance.js></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
## See also
|
||||
|
||||
* <https://github.com/radare/spp>
|
||||
* <https://werc.cat-v.org/docs/rc-template-lang>
|
||||
* <https://code.9front.org/hg/werc/file/92f7463dac1a/bin/template.awk>
|
||||
@@ -0,0 +1,46 @@
|
||||
.Dd Apr 25, 2019
|
||||
.Dt pp 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm pp
|
||||
.Nd preprocessor
|
||||
.Sh SYNOPSIS
|
||||
.Nm pp
|
||||
.Op Fl d
|
||||
.Ar file
|
||||
.Op args ...
|
||||
.Sh DESCRIPTION
|
||||
The
|
||||
.Nm
|
||||
utility processes
|
||||
.Ar file
|
||||
to produce output that is used as input to another
|
||||
program.
|
||||
.Pp
|
||||
It executes code enclosed in
|
||||
.Dq #!\en
|
||||
delimiters using
|
||||
.Xr sh 1 .
|
||||
.Op args ...
|
||||
are passed to the embbeded script and are available
|
||||
via the usual
|
||||
.Em $1, $2, ...
|
||||
positional parameters.
|
||||
.Pp
|
||||
The options are as follows:
|
||||
.Bl -tag -width Ds
|
||||
.It Fl d
|
||||
.Nm
|
||||
will dump the generated
|
||||
.Xr sh 1
|
||||
code instead of executing it. This is useful for debugging.
|
||||
.El
|
||||
.Sh EXIT STATUS
|
||||
.Ex -std
|
||||
.Sh EXAMPLES
|
||||
Preprocess file with arguments:
|
||||
.Pp
|
||||
.Dl $ pp file.upphtml a b
|
||||
.Pp
|
||||
.Sh SEE ALSO
|
||||
.Xr sh 1
|
||||
+369
@@ -0,0 +1,369 @@
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#if !__APPLE__
|
||||
/*
|
||||
* Appends src to string dst of size dsize (unlike strncat, dsize is the
|
||||
* full size of dst, not space left). At most dsize-1 characters
|
||||
* will be copied. Always NUL terminates (unless dsize <= strlen(dst)).
|
||||
* Returns strlen(src) + MIN(dsize, strlen(initial dst)).
|
||||
* If retval >= dsize, truncation occurred.
|
||||
*/
|
||||
size_t
|
||||
strlcat(char *dst, const char *src, size_t dsize)
|
||||
{
|
||||
const char *odst = dst;
|
||||
const char *osrc = src;
|
||||
size_t n = dsize;
|
||||
size_t dlen;
|
||||
|
||||
/* Find the end of dst and adjust bytes left but don't go past end. */
|
||||
while (n-- != 0 && *dst != '\0')
|
||||
dst++;
|
||||
dlen = dst - odst;
|
||||
n = dsize - dlen;
|
||||
|
||||
if (n-- == 0)
|
||||
return(dlen + strlen(src));
|
||||
while (*src != '\0') {
|
||||
if (n != 0) {
|
||||
*dst++ = *src;
|
||||
n--;
|
||||
}
|
||||
src++;
|
||||
}
|
||||
*dst = '\0';
|
||||
|
||||
return(dlen + (src - osrc)); /* count does not include NUL */
|
||||
}
|
||||
#endif /* !__APPLE__ */
|
||||
|
||||
#define MAXSIZE 64000
|
||||
#define MAXINDENTSIZE 80
|
||||
#define MAXARGSIZE 20
|
||||
|
||||
char *argv0;
|
||||
|
||||
int
|
||||
efgetc(FILE *fp)
|
||||
{
|
||||
int c;
|
||||
|
||||
c = fgetc(fp);
|
||||
|
||||
if (c == EOF &&
|
||||
ferror(fp) != 0) {
|
||||
perror(argv0);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
return c;
|
||||
}
|
||||
|
||||
int
|
||||
efseek(FILE *fp, long offset, int whence)
|
||||
{
|
||||
int s;
|
||||
|
||||
s = fseek(fp, offset, whence);
|
||||
|
||||
if (s == -1) {
|
||||
perror(argv0);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
int
|
||||
eungetc(int c, FILE *fp)
|
||||
{
|
||||
int uc;
|
||||
|
||||
uc = ungetc(c, fp);
|
||||
|
||||
if (uc == EOF) {
|
||||
perror(argv0);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
return uc;
|
||||
}
|
||||
|
||||
size_t
|
||||
estrlcat(char *dst, const char *src, size_t dstsize)
|
||||
{
|
||||
size_t n;
|
||||
|
||||
n = strlcat(dst, src, dstsize);
|
||||
|
||||
if (n >= dstsize) {
|
||||
fprintf(stderr, "%s: Buffer overflow\n", argv0);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
int
|
||||
eclose(int d)
|
||||
{
|
||||
int s;
|
||||
|
||||
s = close(d);
|
||||
|
||||
if (s == -1) {
|
||||
perror(argv0);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
char *
|
||||
strappend(char *s, char c)
|
||||
{
|
||||
size_t len;
|
||||
|
||||
len = strlen(s);
|
||||
|
||||
s[len] = c;
|
||||
s[len + 1] = '\0';
|
||||
|
||||
return s ;
|
||||
}
|
||||
|
||||
int
|
||||
shebang(FILE *fp)
|
||||
{
|
||||
int c;
|
||||
|
||||
c = efgetc(fp);
|
||||
|
||||
if (c != '#') {
|
||||
eungetc(c, fp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
c = efgetc(fp);
|
||||
|
||||
if (c != '!') {
|
||||
eungetc(c, fp);
|
||||
eungetc('#', fp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
c = efgetc(fp);
|
||||
|
||||
if (c == '\n') {
|
||||
eungetc(c, fp);
|
||||
eungetc('!', fp);
|
||||
eungetc('#', fp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
c = efgetc(fp);
|
||||
if (c == '\n')
|
||||
break;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
int
|
||||
token(FILE *fp, int consume)
|
||||
{
|
||||
int c;
|
||||
|
||||
c = efgetc(fp);
|
||||
|
||||
if (c != '#') {
|
||||
eungetc(c, fp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
c = efgetc(fp);
|
||||
if (c != '!') {
|
||||
eungetc(c, fp);
|
||||
eungetc('#', fp);
|
||||
return 0;
|
||||
};
|
||||
|
||||
c = efgetc(fp);
|
||||
|
||||
if (c != '\n') {
|
||||
eungetc(c, fp);
|
||||
eungetc('!', fp);
|
||||
eungetc('#', fp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (consume == 0) {
|
||||
eungetc(c, fp);
|
||||
eungetc('!', fp);
|
||||
eungetc('#', fp);
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t
|
||||
output(char *s)
|
||||
{
|
||||
return estrlcat(s, "echo \"", MAXSIZE);
|
||||
}
|
||||
|
||||
size_t
|
||||
outputend(char *s)
|
||||
{
|
||||
return estrlcat(s, "\"\n", MAXSIZE);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
char s[MAXSIZE] = "", indent[MAXINDENTSIZE] = "",
|
||||
*eargv[MAXARGSIZE] = {"sh", "-c", s, "sh", NULL};
|
||||
FILE *fp;
|
||||
int size, c, i, fd[2], o = 0, status = 0, d = 0;
|
||||
|
||||
|
||||
argv0 = argv[0];
|
||||
if (argc == 1) {
|
||||
fprintf(stderr, "usage: %s file [args ...]\n", argv[0]);
|
||||
return 1;
|
||||
|
||||
}
|
||||
|
||||
if (strcmp(argv[1], "-d") == 0) {
|
||||
if (argc == 2) {
|
||||
fprintf(stderr, "usage: %s file [args ...]\n", argv[0]);
|
||||
return 1;
|
||||
|
||||
}
|
||||
d = 1;
|
||||
fp = fopen(argv[2], "r");
|
||||
} else {
|
||||
fp = fopen(argv[1], "r");
|
||||
}
|
||||
|
||||
|
||||
if (!fp) {
|
||||
fprintf(stderr, "%s: %s: %s\n",
|
||||
argv[0], d ? argv[2] : argv[1], strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
efseek (fp, 0, SEEK_END);
|
||||
size = ftell(fp);
|
||||
|
||||
if(size == 0) return 0;
|
||||
|
||||
efseek(fp, -1, SEEK_END);
|
||||
c = efgetc(fp);
|
||||
|
||||
if (c != '\n') {
|
||||
fprintf(stderr, "%s: %s: No newline at end of file\n",
|
||||
argv[0], d ? argv[2] : argv[1]);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
efseek(fp, 0, SEEK_SET);
|
||||
|
||||
shebang(fp);
|
||||
|
||||
if (!token(fp, 1)) {
|
||||
output(s);
|
||||
o = 1;
|
||||
}
|
||||
|
||||
for(;;) {
|
||||
if (token(fp, 1)) {
|
||||
if (!o) {
|
||||
c = efgetc(fp);
|
||||
if (c == EOF)
|
||||
goto done;
|
||||
eungetc(c, fp);
|
||||
output(s);
|
||||
} else
|
||||
outputend(s);
|
||||
o = !o;
|
||||
}
|
||||
|
||||
c = efgetc(fp);
|
||||
if (c == EOF)
|
||||
break;
|
||||
|
||||
if (c == '\n') {
|
||||
indent[0] = 0;
|
||||
c = efgetc(fp);
|
||||
if (c == EOF)
|
||||
break;
|
||||
|
||||
for (;;) {
|
||||
if (c != ' ' && c != '\t')
|
||||
break;
|
||||
strappend(indent, c);
|
||||
c = efgetc(fp);
|
||||
if (c == EOF)
|
||||
break;
|
||||
}
|
||||
|
||||
eungetc(c, fp);
|
||||
|
||||
if (token(fp, 0) && o)
|
||||
continue;
|
||||
|
||||
strappend(s, '\n');
|
||||
estrlcat(s, indent, MAXSIZE);
|
||||
continue;
|
||||
}
|
||||
|
||||
strappend(s, c);
|
||||
}
|
||||
if (o) {
|
||||
outputend(s);
|
||||
o = 0;
|
||||
}
|
||||
done:
|
||||
|
||||
if(d) {
|
||||
puts(s);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (pipe(fd) == -1) {
|
||||
perror(argv0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
switch (fork()) {
|
||||
case -1:
|
||||
perror(argv0);
|
||||
return 1;
|
||||
case 0:
|
||||
eclose(fd[1]);
|
||||
eclose(fd[0]);
|
||||
for (i = 2; i < argc; i++)
|
||||
eargv[i+2] = argv[i];
|
||||
|
||||
if (execv("/bin/sh", eargv) == -1) {
|
||||
perror(argv0);
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
eclose(fd[1]);
|
||||
eclose(fd[0]);
|
||||
if (wait(&status) == -1) {
|
||||
perror(argv0);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
@@ -1,4 +1,11 @@
|
||||
<!-- Primary Meta Tags -->
|
||||
#!
|
||||
if [ -z "${NO_META}" ]; then
|
||||
#!
|
||||
|
||||
<title>${TITLE}</title>
|
||||
<meta name="title" content="${TITLE}" />
|
||||
<meta name="description" content="${DESCRIPTION}" />
|
||||
<meta name='title' content='${TITLE}' />
|
||||
<meta name='description' content='${DESCRIPTION}' />
|
||||
|
||||
#!
|
||||
fi
|
||||
#!
|
||||
|
||||
@@ -12,6 +12,8 @@ CANONICAL_URL="${SITE_URL}/blog/$(basename "${PAGE}")"
|
||||
#!
|
||||
pp "${SHARE}"/components/head.upphtml
|
||||
#!
|
||||
|
||||
<link rel="stylesheet" href="/css/post.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -23,7 +25,7 @@ pp "${SHARE}"/components/navbar.upphtml
|
||||
<article class='h-entry'>
|
||||
|
||||
<aside>
|
||||
<span>This is a blog post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span>
|
||||
<span>This is a <a href='/blog'>blog</a> post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span>
|
||||
#!
|
||||
if [ -z "${DATE_UPDATED}" ]; then
|
||||
#!
|
||||
|
||||
@@ -10,7 +10,11 @@ pp "${SHARE}"/components/head.upphtml
|
||||
<body>
|
||||
#!
|
||||
pp "${SHARE}"/components/navbar.upphtml
|
||||
#!
|
||||
<main>
|
||||
#!
|
||||
pp "$1" "$2"
|
||||
#!
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -12,6 +12,8 @@ CANONICAL_URL="${SITE_URL}/microlog/$(basename "${SRC}")"
|
||||
#!
|
||||
pp "${SHARE}"/components/head.upphtml
|
||||
#!
|
||||
|
||||
<link rel="stylesheet" href="/css/post.css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -23,7 +25,7 @@ pp "${SHARE}"/components/navbar.upphtml
|
||||
<article class="h-entry">
|
||||
|
||||
<aside>
|
||||
<span>This is a microlog post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span>
|
||||
<span>This is a <a href='/microlog'>microlog</a> post by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</span>
|
||||
<span>Published:
|
||||
#!
|
||||
DATE="${DATE_PUBLISHED}" CLASS="dt-published" pp "${SHARE}/components/formatted_date.upphtml"
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
#!
|
||||
pp $@
|
||||
#!
|
||||
@@ -1,46 +0,0 @@
|
||||
.Dd Apr 25, 2019
|
||||
.Dt pp 1
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm pp
|
||||
.Nd preprocessor
|
||||
.Sh SYNOPSIS
|
||||
.Nm pp
|
||||
.Op Fl d
|
||||
.Ar file
|
||||
.Op args ...
|
||||
.Sh DESCRIPTION
|
||||
The
|
||||
.Nm
|
||||
utility processes
|
||||
.Ar file
|
||||
to produce output that is used as input to another
|
||||
program.
|
||||
.Pp
|
||||
It executes code enclosed in
|
||||
.Dq #!\en
|
||||
delimiters using
|
||||
.Xr sh 1 .
|
||||
.Op args ...
|
||||
are passed to the embbeded script and are available
|
||||
via the usual
|
||||
.Em $1, $2, ...
|
||||
positional parameters.
|
||||
.Pp
|
||||
The options are as follows:
|
||||
.Bl -tag -width Ds
|
||||
.It Fl d
|
||||
.Nm
|
||||
will dump the generated
|
||||
.Xr sh 1
|
||||
code instead of executing it. This is useful for debugging.
|
||||
.El
|
||||
.Sh EXIT STATUS
|
||||
.Ex -std
|
||||
.Sh EXAMPLES
|
||||
Preprocess file with arguments:
|
||||
.Pp
|
||||
.Dl $ pp file.upphtml a b
|
||||
.Pp
|
||||
.Sh SEE ALSO
|
||||
.Xr sh 1
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../../pp/pp.1
|
||||
@@ -4,7 +4,7 @@ SITE_URL="$2"
|
||||
export TITLE="Joe Carstairs"
|
||||
export DESCRIPTION="Joe Carstairs' personal website"
|
||||
|
||||
POSTS="$(ls "${SRC}/blog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\.blog.upphtml$")"
|
||||
POSTS="$(ls --reverse "${SRC}/blog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\.blog.upphtml$")"
|
||||
set --
|
||||
for post in $POSTS
|
||||
do
|
||||
@@ -28,8 +28,6 @@ YEARS="$@"
|
||||
pp "${SHARE}"/components/meta.upphtml
|
||||
#!
|
||||
|
||||
<main>
|
||||
|
||||
<section class='h-feed'>
|
||||
|
||||
<h1>My blog</h1>
|
||||
@@ -40,7 +38,7 @@ pp "${SHARE}"/components/meta.upphtml
|
||||
</aside>
|
||||
|
||||
<section>
|
||||
<p><a href='/blog/subscribe'>How to subscribe to this blog</a></p>
|
||||
<p><a href='/subscribe'>How to subscribe to my logs</a></p>
|
||||
</section>
|
||||
|
||||
#!
|
||||
@@ -53,7 +51,7 @@ for year in $YEARS; do
|
||||
#!
|
||||
for post in $POSTS; do if [ "$(echo "${post}" | cut -c1-4)" = "$year" ]; then
|
||||
title="$(grep "^export TITLE=" "${SRC}/blog/${post}" | cut -c15-)"
|
||||
title="${title::${#title}-16}"
|
||||
title="$(echo ${title} | cut -c1-$(( ${#title} - 16 )) )"
|
||||
date_published="$(echo "${post}" | cut -c1-10)"
|
||||
href="${SITE_URL}/blog/$(echo "$post" | cut -c1-10).html"
|
||||
#!
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#!
|
||||
SITE_URL="$1"
|
||||
POSTS="$(ls --reverse "${SRC}/blog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\(\.[0-9]\+\)\?\.blog.upphtml$")"
|
||||
NOW_RFC_822="$(TZ=UT date +"%a, %d %b %Y %H:%M:%S UT")"
|
||||
#!
|
||||
|
||||
<rss version='2.0'>
|
||||
|
||||
<channel>
|
||||
<title>Joe Carstairs’ blog</title>
|
||||
<description>Short posts on random topics I find interesting</description>
|
||||
<link>${SITE_URL}/blog</link>
|
||||
<image>${SITE_URL}/images/headshot.webp</image>
|
||||
<language>en-GB</language>
|
||||
<pubDate>${NOW_RFC_822}</pubDate>
|
||||
|
||||
#!
|
||||
for post in $POSTS; do
|
||||
POST_ID="$(echo ${post} | cut -c1-$(( ${#post} - 13 )) )"
|
||||
. "${SRC}/blog/${post}.args"
|
||||
DATE_PUBLISHED_RFC_822="$(TZ=UT date --date="$( echo "${POST_ID}" | cut -c1-10)" +"%a, %d %b %Y %H:%M:%S UT")"
|
||||
#!
|
||||
<item>
|
||||
|
||||
<title>${POST_TITLE}</title>
|
||||
<link>${SITE_URL}/blog/${POST_ID}</link>
|
||||
<guid isPermaLink='true'>${SITE_URL}/blog/${POST_ID}</guid>
|
||||
<pubDate>${DATE_PUBLISHED_RFC_822}</pubDate>
|
||||
<description>
|
||||
#!
|
||||
NO_META=1 pp "${SRC}/blog/$post" "${SITE_URL}" \
|
||||
| sed "s/&/\\&/g" \
|
||||
| sed "s/</\\</g" \
|
||||
| sed "s/>/\\>/g" \
|
||||
| sed "s/\"/\\"/g" \
|
||||
| sed "s/'/\\'/g" \
|
||||
| sed "s/\`/\\`/g" \
|
||||
| sed "s/\\\$/\\\\\$/g"
|
||||
#!
|
||||
</description>
|
||||
<author>Joe Carstairs</author>
|
||||
|
||||
</item>
|
||||
#!
|
||||
done
|
||||
#!
|
||||
|
||||
</channel>
|
||||
|
||||
</rss>
|
||||
@@ -0,0 +1,4 @@
|
||||
#!
|
||||
SITE_URL="${1}"
|
||||
#!
|
||||
<meta http-equiv=refresh content=0;url=${SITE_URL}/subscribe />
|
||||
@@ -8,7 +8,6 @@ pp "${SHARE}"/components/meta.upphtml
|
||||
pp "${SHARE}"/components/otp-dialog.upphtml
|
||||
#!
|
||||
|
||||
<main>
|
||||
<form class=contact-form>
|
||||
<h1>Contact me</h1>
|
||||
<p hidden class=error></p>
|
||||
@@ -25,7 +24,7 @@ pp "${SHARE}"/components/otp-dialog.upphtml
|
||||
<h1>You sent me a message!</h1>
|
||||
<p>Thanks for that. I may be in touch.</p>
|
||||
<p>In case you forgot, your message was this:</p>
|
||||
<hr>
|
||||
<hr />
|
||||
<dl>
|
||||
<dt>Name</dt> <dd class=sentname>???</dd>
|
||||
<dt>Email</dt> <dd class=sentemail>???</dd>
|
||||
@@ -95,4 +94,3 @@ pp "${SHARE}"/components/otp-dialog.upphtml
|
||||
await submitOtpForm(selectors);
|
||||
});
|
||||
</script>
|
||||
</main>
|
||||
|
||||
@@ -305,11 +305,6 @@ strong {
|
||||
|
||||
/** Blog feed */
|
||||
|
||||
.h-feed ul {
|
||||
list-style: none;
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
/** Block quotes */
|
||||
blockquote {
|
||||
padding-inline-start: var(--spacing-inline-lg);
|
||||
@@ -317,6 +312,10 @@ blockquote {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
blockquote :first-child {
|
||||
margin-block-start: 0;
|
||||
}
|
||||
|
||||
blockquote footer {
|
||||
font-style: initial;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
margin-block-start: var(--spacing-block-md);
|
||||
}
|
||||
|
||||
.h-feed ul:has(> .h-entry) {
|
||||
list-style-type: none;
|
||||
margin-inline: 0;
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
}
|
||||
|
||||
.h-feed .h-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -4,7 +4,6 @@ export DESCRIPTION="Error page for Joe Carstairs' personal website"
|
||||
pp "${SHARE}"/components/meta.upphtml
|
||||
#!
|
||||
|
||||
<main>
|
||||
<section>
|
||||
<h1>Got lost?</h1>
|
||||
|
||||
@@ -13,4 +12,3 @@ pp "${SHARE}"/components/meta.upphtml
|
||||
my <a href='/'>homepage</a> instead.
|
||||
</p>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -4,7 +4,6 @@ export DESCRIPTION="Joe Carstairs' personal website"
|
||||
pp "${SHARE}"/components/meta.upphtml
|
||||
#!
|
||||
|
||||
<main>
|
||||
<section class="h-card">
|
||||
<div>
|
||||
<img class="u-photo" src="/images/headshot.webp" height="96" width="96" />
|
||||
@@ -46,4 +45,3 @@ pp "${SHARE}"/components/meta.upphtml
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#!
|
||||
SITE_URL="${1}"
|
||||
#!
|
||||
<meta http-equiv=refresh content=0;url=${SITE_URL}/microlog/rss.xml />
|
||||
@@ -0,0 +1,84 @@
|
||||
#!
|
||||
PAGE="$1"
|
||||
SITE_URL="$2"
|
||||
export TITLE="Joe Carstairs"
|
||||
export DESCRIPTION="Joe Carstairs' personal website"
|
||||
|
||||
POSTS="$(ls --reverse "${SRC}/microlog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\(\.[0-9]\+\)\?\.microlog.upphtml$")"
|
||||
set --
|
||||
for post in $POSTS
|
||||
do
|
||||
this_year="$( echo "$post" | cut -c1-4 )"
|
||||
has_this_year=0
|
||||
for year in $@
|
||||
do
|
||||
if [ "$this_year" = "$year" ]
|
||||
then
|
||||
has_this_year=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ $has_this_year -eq 0 ]
|
||||
then
|
||||
set -- $@ "$this_year"
|
||||
fi
|
||||
done
|
||||
YEARS="$@"
|
||||
|
||||
pp "${SHARE}"/components/meta.upphtml
|
||||
#!
|
||||
|
||||
<main>
|
||||
|
||||
<section class='h-feed'>
|
||||
|
||||
<h1>My microlog</h1>
|
||||
|
||||
<aside>
|
||||
<p>This microlog is written by <a class='p-author h-card' href='/'>Joe Carstairs</a>.</p>
|
||||
<p hidden><a class='u-url' href='${SITE_URL}/microlog'>Permalink</a></p>
|
||||
</aside>
|
||||
|
||||
<section>
|
||||
<p><a href='/subscribe'>How to subscribe to my logs</a></p>
|
||||
</section>
|
||||
|
||||
#!
|
||||
for year in $YEARS; do
|
||||
#!
|
||||
<h2>${year}</h2>
|
||||
|
||||
<ul>
|
||||
|
||||
#!
|
||||
for post in $POSTS; do if [ "$(echo "${post}" | cut -c1-4)" = "$year" ]; then
|
||||
title="$(grep "^export TITLE=" "${SRC}/microlog/${post}" | cut -c15-)"
|
||||
title="$(echo "${title}" | cut -c1-$(( ${#title} - 16)) )"
|
||||
date_published="$(echo "${post}" | cut -c1-10)"
|
||||
href="${SITE_URL}/microlog/$(echo ${post} | cut -c1-$(( ${#post} - 17 )) ).html"
|
||||
#!
|
||||
|
||||
<li class='h-entry'>
|
||||
<h3><a class='u-url p-name' href='${href}'>${title}</a></h3>
|
||||
<section class='e-content'>
|
||||
#!
|
||||
pp "${SRC}/microlog/${post}" "${SITE_URL}"
|
||||
#!
|
||||
</section>
|
||||
#!
|
||||
DATE="${date_published}" CLASS="dt-published" ATTRIBUTES="hidden" \
|
||||
pp "${SHARE}/components/formatted_date.upphtml"
|
||||
#!
|
||||
</li>
|
||||
|
||||
#!
|
||||
fi; done
|
||||
#!
|
||||
|
||||
</ul>
|
||||
|
||||
#!
|
||||
done
|
||||
#!
|
||||
|
||||
</section>
|
||||
@@ -0,0 +1,50 @@
|
||||
#!
|
||||
SITE_URL="$1"
|
||||
POSTS="$(ls --reverse "${SRC}/microlog/" | grep "^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}\(\.[0-9]\+\)\?\.microlog.upphtml$")"
|
||||
NOW_RFC_822="$(TZ=UT date +"%a, %d %b %Y %H:%M:%S UT")"
|
||||
#!
|
||||
|
||||
<rss version='2.0'>
|
||||
|
||||
<channel>
|
||||
<title>Joe Carstairs’ microlog</title>
|
||||
<description>My private X feed I guess?</description>
|
||||
<link>${SITE_URL}/microlog</link>
|
||||
<image>${SITE_URL}/images/headshot.webp</image>
|
||||
<language>en-GB</language>
|
||||
<pubDate>${NOW_RFC_822}</pubDate>
|
||||
|
||||
#!
|
||||
for post in $POSTS; do
|
||||
POST_ID="$(echo ${post} | cut -c1-$(( ${#post} - 17 )) )"
|
||||
. "${SRC}/microlog/${post}.args"
|
||||
DATE_PUBLISHED_RFC_822="$(TZ=UT date --date="$( echo "${POST_ID}" | cut -c1-10)" +"%a, %d %b %Y %H:%M:%S UT")"
|
||||
#!
|
||||
<item>
|
||||
|
||||
<title>${POST_TITLE}</title>
|
||||
<link>${SITE_URL}/microlog/${POST_ID}</link>
|
||||
<guid isPermaLink='true'>${SITE_URL}/microlog/${POST_ID}</guid>
|
||||
<pubDate>${DATE_PUBLISHED_RFC_822}</pubDate>
|
||||
<description>
|
||||
#!
|
||||
NO_META=1 pp "${SRC}/microlog/$post" "${SITE_URL}" \
|
||||
| sed "s/&/\\&/g" \
|
||||
| sed "s/</\\</g" \
|
||||
| sed "s/>/\\>/g" \
|
||||
| sed "s/\"/\\"/g" \
|
||||
| sed "s/'/\\'/g" \
|
||||
| sed "s/\`/\\`/g" \
|
||||
| sed "s/\\\$/\\\\\$/g"
|
||||
#!
|
||||
</description>
|
||||
<author>Joe Carstairs</author>
|
||||
|
||||
</item>
|
||||
#!
|
||||
done
|
||||
#!
|
||||
|
||||
</channel>
|
||||
|
||||
</rss>
|
||||
@@ -0,0 +1,91 @@
|
||||
#!
|
||||
PAGE="$1"
|
||||
SITE_URL="$2"
|
||||
export TITLE="How to subscribe to joeac's blog"
|
||||
export DESCRIPTION="How to subscribe to joeac's blog"
|
||||
#!
|
||||
|
||||
<section>
|
||||
<h1>How to subscribe to my logs</h1>
|
||||
|
||||
<p>
|
||||
The short version: use my <a href='/blog/rss.xml'>blog RSS feed</a> and
|
||||
<a href='/microlog/rss.xml'>microlog RSS feed</a>, or my
|
||||
<a href='gemini://joeac.net/logs/loglog.gmi'>gemlog</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If neither of those things means anything to you, no worries. You could just
|
||||
look at my website every once in a while, but that gets old. A better way is to
|
||||
use my RSS file. Here's how.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
An RSS file, if you don't know, is a file containing a list of posts in a format
|
||||
that's so stupid even computers can understand it. Mine is at
|
||||
<a href='https://joeac.net/rss.xml'>https://joeac.net/rss.xml</a>,
|
||||
and I've also got one for just my blog posts at
|
||||
<a href='https://joeac.net/blog/rss.xml'>https://joeac.net/blog/rss.xml</a>,
|
||||
and one just for microlog posts at
|
||||
<a href='https://joeac.net/microlog/rss.xml'>https://joeac.net/microlog/rss.xml</a>.
|
||||
Since computers can understand it, you can get software to read it automatically
|
||||
and tell you when I've made a new blog post. That's much less hassle than having
|
||||
to check manually yourself!
|
||||
</p>
|
||||
|
||||
<p>
|
||||
I'm not the only person serving RSS files, of course. You might not have heard
|
||||
of them, but they're actually all over the Web. In the early days of the Web,
|
||||
everyone was using them, so they've ended up built in to a whole load of
|
||||
foundational Web technology, and hence ubiquitous, even if most Web users have
|
||||
forgotten they exist. Once you've subscribed to my logs, you can subscribe to
|
||||
basically any blog, podcast, or YouTube channel, and a lot of other Websites for
|
||||
news, organisations, whatever. Point a well-designed RSS aggregator or RSS
|
||||
reader at most Websites, and it'll automatically find an associated RSS feed.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you're not persuaded, try reading
|
||||
<a href='https://doctorow.medium.com/you-should-be-using-an-rss-reader-76aed31151f9'>
|
||||
Cory Doctorow explaining why RSS is ridiculously good
|
||||
</a>. He's terrifyingly persuasive. Also, read his novels!
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you just want to give it a go quickly, probably the quickest way is to
|
||||
use someone else's RSS aggregator that they'll let you use as a service.
|
||||
<a href='https://feedly.com/news-reader'>Feedly</a> and
|
||||
<a href='https://www.inoreader.com'>Inoreader</a> have free tiers.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
If you want to have a little more control, you can self-host an RSS
|
||||
reader, like <a href='https://tt-rss.org'>Tiny Tiny RSS</a>,
|
||||
<a href='https://miniflux.app'>Miniflux</a>, or
|
||||
<a href='https://freshrss.org'>FreshRSS</a>. But why would you do that?
|
||||
Self-hosting is complicated! Why not just use a normal, local app? Here are
|
||||
some options for that you might want to investigate. All these are active
|
||||
free and open-source software projects at the time of writing.
|
||||
</p>
|
||||
|
||||
<ul>
|
||||
<li><a href='https://hyliu.me/fluent-reader'>Fluent Reader</a> (Linux, Windows, Mac, also mobile I think?)</li>
|
||||
<li><a href='https://www.rssowl.org'>RSSOwl</a> (Linux, Windows, Mac)</li>
|
||||
<li><a href='https://capyreader.com'>Capy Reader</a> (Android)</li>
|
||||
<li><a href='https://apps.kde.org/en-gb/akregator'>Akregator</a> (Linux, Windows (unstable))</li>
|
||||
<li><a href='https://gfeeds.gabmus.org'>GFeeds</a> (Linux)</li>
|
||||
<li><a href='https://lzone.de/liferea'>Liferea</a> (Linux)</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
For the terminally geeky, there is also
|
||||
<a href='https://newsboat.org'>Newsboat</a>. I used it for ages, it works
|
||||
fabulously, but it's all in the terminal, so you have to be OK with that.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can also just enter 'RSS feed reader' into your favourite search
|
||||
engine, software distribution, or app store and see what comes up. Try
|
||||
adding 'FOSS' into the query to prioritise free, open-source software.
|
||||
</p>
|
||||
</section>
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
#!/bin/sh
|
||||
|
||||
get_var_value()
|
||||
{
|
||||
expr "$(env | grep "^${1}=")" : "${1}=\(.*\)\$"
|
||||
}
|
||||
|
||||
if [ -z "$(which podman 2>/dev/null)" ]
|
||||
then
|
||||
sudo apk add podman
|
||||
fi
|
||||
|
||||
if [ -z "$(which podman-compose 2>/dev/null)" ]
|
||||
then
|
||||
sudo apk add podman-compose
|
||||
fi
|
||||
|
||||
if [ -z "$(which yq 2>/dev/null)" ]
|
||||
then
|
||||
YQ_PLATFORM="linux_$(expr "$(arch)" : "armv7" && echo arm || expr "$(arch)" : "x86_64" && echo amd64 || arch)"
|
||||
sudo wget -O /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_${YQ_PLATFORM}
|
||||
sudo chmod +x /usr/local/bin/yq
|
||||
fi
|
||||
|
||||
if [ -z "$(which envsubst 2>/dev/null)" ]
|
||||
then
|
||||
if [ "$(arch)" = "x86_64" ] || [ "$(arch)" = "arm64" ]
|
||||
then
|
||||
ENVSUBST_VERSION=1.4.3
|
||||
sudo wget -O /usr/local/bin/envsubst https://github.com/a8m/envsubst/releases/download/v${ENVSUBST_VERSION}/envsubst-Linux-$(arch)
|
||||
sudo chmod +x /usr/local/bin/envsubst
|
||||
else
|
||||
sudo apk add go
|
||||
go install github.com/a8m/envsubst/cmd/envsubst@latest
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$(which bash 2>/dev/null)" ]
|
||||
then
|
||||
sudo apk add bash
|
||||
fi
|
||||
|
||||
if [ -z "$(grep "\bjoeac.net\b" /etc/group)" ]
|
||||
then
|
||||
sudo adduser -D -h /home/joeac.net joeac.net
|
||||
sudo adduser joeac.net joeac.net
|
||||
fi
|
||||
|
||||
if ! ( expr "$(groups joeac.net)" wheel )
|
||||
then
|
||||
sudo adduser joeac.net wheel
|
||||
fi
|
||||
|
||||
su -l joeac.net
|
||||
if ! [ $(whoami) = "joeac.net" ]
|
||||
then
|
||||
echo "Could not log in to user: joeac.net. Log in manually and re-run this script as joeac.net."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [ -d /home/joeac.net/joeac.net/.git ]
|
||||
then
|
||||
git clone https://git.joeac.net/joeac/joeac.net.git /home/joeac.net/joeac.net
|
||||
fi
|
||||
sudo chown joeac.net:joeac.net /home/joeac.net/joeac.net
|
||||
sudo chmod 770 /home/joeac.netjoeac.net
|
||||
|
||||
if ! [ -f DIGITALOCEAN_TOKEN ]
|
||||
then
|
||||
if [ -z "${DIGITALOCEAN_TOKEN}" ]
|
||||
then
|
||||
read -sp "DIGITALOCEAN_TOKEN: " DIGITALOCEAN_TOKEN
|
||||
fi
|
||||
echo ${DIGITALOCEAN_TOKEN} > /home/joeac.net/joeac.net/DIGITALOCEAN_TOKEN
|
||||
fi
|
||||
|
||||
if ! ( podman secret exists remote_smtp_password )
|
||||
then
|
||||
if [ -z "${REMOTE_SMTP_PASSWORD}" ]
|
||||
then
|
||||
read -sp "REMOTE_SMTP_PASSWORD: " REMOTE_SMTP_PASSWORD
|
||||
fi
|
||||
echo "${REMOTE_SMTP_PASSWORD}" | podman secret create remote_smtp_password -
|
||||
unset REMOTE_SMTP_PASSWORD
|
||||
fi
|
||||
|
||||
while read line
|
||||
do
|
||||
if expr "${line}" : "[[:alnum:]_]\+=" 1>/dev/null
|
||||
then
|
||||
var_name="$(expr "${line}" : "\([[:alnum:]_]\+\)=")"
|
||||
|
||||
if [ -n "$(grep "^${var_name}=" /home/joeac.net/joeac.net/.env)" ]
|
||||
then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [ -z "$(get_var_value ${var_name})" ]
|
||||
then
|
||||
default_value="$(expr "${line}" : "[[:alnum:]_]\+=\(.*\)\$")"
|
||||
read -sp "${var_name}: " ${var_name}
|
||||
if [ -z "$(get_var_value ${var_name})" ]
|
||||
then
|
||||
eval ${var_name}="${default_value}"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "${var_name}=$(get_var_value ${var_name})" >> /home/joeac.net/joeac.net/.env
|
||||
fi
|
||||
done </home/joeac.net/joeac.net/example.env
|
||||
|
||||
make --directory=/home/joeac.net/joeac.net
|
||||
make --directory=/home/joeac.net/joeac.net install
|
||||
@@ -0,0 +1,14 @@
|
||||
.PHONY: install
|
||||
install: $(PUBLIC_ROOT_DIR_ln)
|
||||
|
||||
.PHONY: reinstall
|
||||
reinstall: install
|
||||
|
||||
$(PUBLIC_ROOT_DIR_ln): public
|
||||
sudo rm -rf $(PUBLIC_ROOT_DIR_ln)
|
||||
sudo mkdir -p $(PUBLIC_ROOT_DIR_ln)
|
||||
sudo cp -r public/ $(PUBLIC_ROOT_DIR_ln)/
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall:
|
||||
sudo rm -rf $(PUBLIC_ROOT_DIR_ln)
|
||||
@@ -0,0 +1 @@
|
||||
<meta http-equiv="refresh" content="0;url=https://forms.zohopublic.eu/mejo1/form/MilehousehutJuly2026/formperma/Jzvv9p6KIJxon5_kc2l29tI0FiMjKy3bJ5ICS9K67NQ" />
|
||||
@@ -0,0 +1,28 @@
|
||||
REGISTRY_DOMAIN := git.joeac.net
|
||||
REGISTRY_USER := joeac
|
||||
|
||||
container_image_name = $(shell $(COMPOSE_CMD) config | yq ".services.$(module).image")
|
||||
is_containerised = $(filter $(COMPOSE_SERVICES),$(module))
|
||||
is_own_image = $(filter git.joeac.net/%,$(container_image_name))
|
||||
has_dockerfile = $(shell test -f $(module).Dockerfile)
|
||||
|
||||
define build_module_rule =
|
||||
.PHONY: build_$(module)
|
||||
build_$(module): $(if $(is_containerised),make_$(module) $(if $(has_dockerfile),$(module).Dockerfile))
|
||||
$(if $(is_containerised),\
|
||||
$(COMPOSE_CMD) build $(module))
|
||||
endef
|
||||
|
||||
define push_module_rule =
|
||||
.PHONY: push_$(module)
|
||||
push_$(module): $(if $(is_containerised),login_registry)
|
||||
$(if $(is_containerised),$(if $(is_own_image),\
|
||||
podman push $(container_image_name)))
|
||||
endef
|
||||
|
||||
$(foreach module,$(MODULES),$(eval $(call build_module_rule)))
|
||||
$(foreach module,$(MODULES),$(eval $(call push_module_rule)))
|
||||
|
||||
.PHONY: login_registry
|
||||
login_registry:
|
||||
podman login $(REGISTRY_DOMAIN)
|
||||
@@ -0,0 +1,13 @@
|
||||
.PHONY: install_crontab
|
||||
install_crontab: /etc/periodic/daily/joeac.net
|
||||
|
||||
.PHONY: reinstall_crontab
|
||||
reinstall_crontab: /etc/periodic/daily/joeac.net
|
||||
|
||||
.PHONY: uninstall_crontab
|
||||
uninstall_crontab:
|
||||
sudo rm -f /etc/periodic/daily/joeac.net
|
||||
|
||||
/etc/periodic/daily/joeac.net: update.sh
|
||||
sudo cp update.sh /etc/periodic/daily/joeac.net
|
||||
sudo chmod +x /etc/periodic/daily/joeac.net
|
||||
@@ -0,0 +1,55 @@
|
||||
installed_dyndns_crontabs = $(wildcard /etc/periodic/daily/dyndns-*.joeac.net)
|
||||
installed_dyndns_subdomains = $(installed_dyndns_crontabs:/etc/periodic/daily/dyndns-%.joeac.net=%)
|
||||
dyndns_subdomains_to_remove = $(filter-out $(SUBDOMAINS),$(installed_dyndns_subdomains))
|
||||
dyndns_crontabs_to_remove = $(dyndns_subdomains_to_remove:%=/etc/periodic/daily/dyndns-%.joeac.net)
|
||||
|
||||
define install_dyndns_module_rule =
|
||||
.PHONY: install_dyndns_$(module)
|
||||
install_dyndns_module_$(module): $(if $(SUBDOMAIN_$(module)),/etc/periodic/daily/dyndns-$(SUBDOMAIN_$(module)).joeac.net)
|
||||
endef
|
||||
|
||||
define reinstall_dyndns_module_rule =
|
||||
.PHONY: reinstall_dyndns_$(module)
|
||||
reinstall_dyndns_$(module): $(if $(SUBDOMAIN_$(module)),/etc/periodic/daily/dyndns-$(SUBDOMAIN_$(module)).joeac.net reinstall_dyndns)
|
||||
endef
|
||||
|
||||
define uninstall_dyndns_module_rule =
|
||||
.PHONY: uninstall_dyndns_$(module)
|
||||
uninstall_dyndns_$(module):
|
||||
$(if $(SUBDOMAIN_$(module)), \
|
||||
sudo rm -f /etc/periodic/daily/dyndns-$(SUBDOMAIN_$(module)).joeac.net
|
||||
)
|
||||
endef
|
||||
|
||||
$(foreach module,$(ALL_MODULES), $(eval $(install_dyndns_module_rule)))
|
||||
$(foreach module,$(ALL_MODULES), $(eval $(reinstall_dyndns_module_rule)))
|
||||
$(foreach module,$(ALL_MODULES), $(eval $(uninstall_dyndns_module_rule)))
|
||||
|
||||
.PHONY: remove_/etc/periodic/daily/dyndns-%.joeac.net
|
||||
remove_/etc/periodic/daily/dyndns-%.joeac.net:
|
||||
rm -f $(@:remove_%=%)
|
||||
|
||||
/etc/periodic/daily/dyndns-%joeac.net: ~/digitalocean_dyndns/dyndns.sh ~/.config/dyndns/DIGITALOCEAN_TOKEN
|
||||
echo "#!/bin/sh" > crontab.tmp
|
||||
echo " $(shell realpath ~)/digitalocean_dyndns/dyndns.sh 4 $(*F)joeac.net" >> crontab.tmp
|
||||
echo "CONN_DEVICE_NAME=eth0 $(shell realpath ~)/digitalocean_dyndns/dyndns.sh 6 $(*F)joeac.net" >> crontab.tmp
|
||||
sudo mv crontab.tmp $@
|
||||
sudo chmod +x $@
|
||||
|
||||
.PHONY: reinstall_dyndns
|
||||
reinstall_dyndns: $(addprefix remove_,$(dyndns_crontabs_to_remove))
|
||||
|
||||
.PHONY: uninstall_dyndns
|
||||
uninstall_dyndns: $(foreach module,$(ALL_MODULES),$(uninstall_dyndns_$(module)))
|
||||
sudo rm -rf \
|
||||
~/digitalocean_dyndns/ \
|
||||
~/.config/dyndns \
|
||||
~/.cache/dyndns
|
||||
|
||||
~/digitalocean_dyndns/%:
|
||||
git clone https://git.joeac.net/joeac/digitalocean_dyndns.git ~/digitalocean_dyndns
|
||||
|
||||
~/.config/dyndns/DIGITALOCEAN_TOKEN: DIGITALOCEAN_TOKEN
|
||||
mkdir -p ~/.config/dyndns/
|
||||
rm -f ~/.config/dyndns/DIGITALOCEAN_TOKEN
|
||||
cp DIGITALOCEAN_TOKEN ~/.config/dyndns/DIGITALOCEAN_TOKEN
|
||||
@@ -0,0 +1,75 @@
|
||||
installed_nginx_sites = $(wildcard /etc/nginx/http.d/*.joeac.net.conf)
|
||||
installed_nginx_subdomains = $(installed_nginx_sites:/etc/nginx/http.d/%.joeac.net.conf=%)
|
||||
nginx_subdomains_to_remove = $(filter-out $(NGINX_SUBDOMAINS),$(installed_nginx_subdomains))
|
||||
nginx_sites_to_remove = $(nginx_subdomains_to_remove:%=/etc/nginx/http.d/%.joeac.net.conf)
|
||||
nginx_module_config_template = \
|
||||
$(if $(PORT_$(module)),nginx/http.d/reverse_proxy.conf.template) \
|
||||
$(if $(PUBLIC_ROOT_DIR_$(module)),nginx/http.d/static.conf.template)
|
||||
nginx_module_config_template_args = \
|
||||
CERTNAME=$(SUBDOMAIN_$(module)).joeac.net \
|
||||
DOMAIN=$(if $(filter-out @,$(SUBDOMAIN_$(module))),$(SUBDOMAIN_$(module)).)joeac.net \
|
||||
$(if $(PORT_$(module)),PORT=$(PORT_$(module)) HOST=$(HOST_$(module))) \
|
||||
$(if $(PUBLIC_ROOT_DIR_$(module)),ROOT="$(PUBLIC_ROOT_DIR_$(module))")
|
||||
|
||||
define install_nginx_module_rule =
|
||||
.PHONY: install_nginx_$(module)
|
||||
install_nginx_$(module): $(if $(SUBDOMAIN_$(module)),/etc/nginx/http.d/$(SUBDOMAIN_$(module)).joeac.net.conf)
|
||||
endef
|
||||
|
||||
define reinstall_nginx_module_rule =
|
||||
.PHONY: reinstall_nginx_$(module)
|
||||
reinstall_nginx_$(module): $(if $(SUBDOMAIN_$(module)),/etc/nginx/http.d/$(SUBDOMAIN_$(module)).joeac.net.conf)
|
||||
endef
|
||||
|
||||
define uninstall_nginx_module_rule =
|
||||
.PHONY: uninstall_nginx_$(module)
|
||||
uninstall_nginx_$(module):
|
||||
$(if $(SUBDOMAIN_$(module)),sudo rm -f /etc/nginx/http.d/$(SUBDOMAIN_$(module)).joeac.net.conf)
|
||||
endef
|
||||
|
||||
define remove_nginx_site_rule =
|
||||
remove_$(site):
|
||||
sudo rm -f $(site)
|
||||
endef
|
||||
|
||||
define nginx_module_config_rule =
|
||||
/etc/nginx/http.d/$(SUBDOMAIN_$(module)).joeac.net.conf: $(nginx_module_config_template) /etc/nginx/http.d /etc/nginx/nginx.conf
|
||||
$(nginx_module_config_template_args) envsubst -i $$< -o $$(notdir $$@).tmp
|
||||
sudo cp $$(notdir $$@).tmp $$@
|
||||
sudo rc-service nginx restart
|
||||
endef
|
||||
|
||||
$(foreach module,$(ALL_MODULES),$(eval $(install_nginx_module_rule)))
|
||||
$(foreach module,$(ALL_MODULES),$(eval $(reinstall_nginx_module_rule)))
|
||||
$(foreach module,$(ALL_MODULES),$(eval $(uninstall_nginx_module_rule)))
|
||||
$(foreach site,$(nginx_sites_to_remove),$(eval $(remove_nginx_site_rule)))
|
||||
$(foreach module,$(ALL_NGINX_MODULES),$(eval $(nginx_module_config_rule)))
|
||||
|
||||
/etc/nginx/http.d:
|
||||
sudo mkdir -p /etc/nginx/http.d
|
||||
|
||||
.PHONY: remove_/etc/nginx/http.d/%.joeac.net.conf
|
||||
remove_/etc/nginx/http.d/%.joeac.net.conf:
|
||||
rm -f $(@:remove_%=%)
|
||||
|
||||
.PHONY: install_nginx
|
||||
install_nginx: /etc/nginx/nginx.conf $(addprefix install_nginx_,$(NGINX_MODULES))
|
||||
|
||||
.PHONY: reinstall_nginx
|
||||
reinstall_nginx: /etc/nginx/nginx.conf $(add_prefix reinstall_nginx_,$(NGINX_MODULES)) $(addprefix remove_,$(nginx_sites_to_remove))
|
||||
|
||||
.PHONY: uninstall_nginx
|
||||
uninstall_nginx: $(foreach module,$(NGINX_MODULES),uninstall_nginx_$(module))
|
||||
ifeq ($(shell test -d /etc/nginx/nginx.joeac.net-backup && echo 1 || echo 0),0)
|
||||
$(warn No nginx backup config detected: doing nothing)
|
||||
else
|
||||
sudo mv /etc/nginx/nginx.joeac.net-backup /etc/nginx/nginx.conf
|
||||
sudo rc-service nginx restart
|
||||
endif
|
||||
|
||||
/etc/nginx/nginx.conf: nginx/nginx.conf /etc/nginx/nginx.joeac.net-backup
|
||||
sudo cp $< $@
|
||||
sudo rc-service nginx restart
|
||||
|
||||
/etc/nginx/nginx.joeac.net-backup:
|
||||
sudo mv /etc/nginx/nginx.conf /etc/nginx/nginx.joeac.net-backup
|
||||
@@ -0,0 +1,74 @@
|
||||
is_openrc_module = $(filter $(COMPOSE_SERVICES),$(module))
|
||||
openrc_module_target = $(if $(is_openrc_module),~/.config/rc/init.d/joeac.net.$(module))
|
||||
|
||||
define install_openrc_module_rule =
|
||||
.PHONY: install_openrc_$(module)
|
||||
install_openrc_$(module): $(if $(is_openrc_module),$(openrc_module_target) openrc_add_$(module) openrc_start_$(module))
|
||||
|
||||
~/.config/rc/init.d/joeac.net.$(module): ~/.config/rc/init.d/joeac.net ~/.config/rc/runlevels/default
|
||||
rm -f ~/.config/rc/init.d/joeac.net.$(module)
|
||||
ln -s $(shell realpath ~)/.config/rc/init.d/joeac.net ~/.config/rc/init.d/joeac.net.$(module)
|
||||
endef
|
||||
|
||||
define reinstall_openrc_module_rule =
|
||||
.PHONY: reinstall_openrc_$(module)
|
||||
reinstall_openrc_$(module): $(if $(is_openrc_module),$(openrc_module_target) openrc_restart_$(module))
|
||||
endef
|
||||
|
||||
define uninstall_openrc_module_rule =
|
||||
.PHONY: uninstall_openrc_$(module)
|
||||
uninstall_openrc_$(module):
|
||||
$(if $(is_openrc_module),\
|
||||
rc-service -U joeac.net.$(module) stop; \
|
||||
rc-update -U del joeac.net.$(module) default; \
|
||||
rm -f ~/.config/rc/init.d/joeac.net.$(module); \
|
||||
)
|
||||
endef
|
||||
|
||||
define openrc_add_rule =
|
||||
.PHONY: openrc_add_$(module)
|
||||
openrc_add_$(module):
|
||||
rc-update -U add joeac.net.$(module) default
|
||||
endef
|
||||
|
||||
define openrc_start_rule =
|
||||
.PHONY: openrc_start_$(module)
|
||||
openrc_start_$(module):
|
||||
rc-service -U joeac.net.$(module) start
|
||||
endef
|
||||
|
||||
define openrc_restart_rule =
|
||||
.PHONY: openrc_restart_$(module)
|
||||
openrc_restart_$(module):
|
||||
rc-service -U joeac.net.$(module) restart
|
||||
endef
|
||||
|
||||
$(foreach module,$(ALL_MODULES),$(eval $(install_openrc_module_rule)))
|
||||
$(foreach module,$(ALL_MODULES),$(eval $(reinstall_openrc_module_rule)))
|
||||
$(foreach module,$(ALL_MODULES),$(eval $(uninstall_openrc_module_rule)))
|
||||
$(foreach module,$(ALL_MODULES),$(eval $(openrc_add_rule)))
|
||||
$(foreach module,$(ALL_MODULES),$(eval $(openrc_start_rule)))
|
||||
$(foreach module,$(MODULES),$(eval $(openrc_restart_rule)))
|
||||
|
||||
~/.config/rc/init.d/joeac.net: openrc/init.d/joeac.net.template ~/.config/rc/init.d ~/.config/rc/runlevels/default /etc/init.d/user.$(USER) /etc/conf.d/user.$(USER)
|
||||
rm -f ~/.config/rc/init.d/joeac.net; \
|
||||
mkdir -p ~/.config/rc/init.d; \
|
||||
COMPOSECMD="$(COMPOSE_CMD)" envsubst -i openrc/init.d/joeac.net.template -o ~/.config/rc/init.d/joeac.net
|
||||
chmod +x ~/.config/rc/init.d/joeac.net
|
||||
|
||||
~/.config/rc/init.d:
|
||||
mkdir -p ~/.config/rc/init.d
|
||||
|
||||
~/.config/rc/runlevels/default:
|
||||
mkdir -p ~/.config/rc/runlevels/default
|
||||
|
||||
/etc/init.d/user.$(USER):
|
||||
sudo ln -s /etc/init.d/user /etc/init.d/user.$(USER)
|
||||
|
||||
/etc/conf.d/user.$(USER): openrc/conf.d/user.template
|
||||
USER=$(USER) envsubst -i openrc/conf.d/user.template -o openrc/conf.d/user.$(USER)
|
||||
sudo mv openrc/conf.d/user.$(USER) /etc/conf.d/user.$(USER)
|
||||
sudo rc-update add user.$(USER) default
|
||||
|
||||
.PHONY: uninstall_joeac.net_service
|
||||
rm ~/.config/rc/joeac.net
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
installed_tls_crontabs = $(wildcard /etc/periodic/daily/tls-*.joeac.net)
|
||||
installed_tls_crontab_subdomains = $(installed_tls_crontabs:/etc/periodic/daily/tls-%.joeac.net=%)
|
||||
tls_crontab_subdomains_to_remove = $(filter-out $(SUBDOMAINS),$(installed_tls_crontab_subdomains))
|
||||
tls_crontabs_to_remove = $(tls_crontab_subdomains_to_remove:%=/etc/periodic/daily/tls-%.joeac.net)
|
||||
|
||||
installed_tls_certs = $(wildcard /etc/letsencrypt/live/*.joeac.net)
|
||||
installed_tls_cert_subdomains = $(installed_tls_certs:/etc/letsencrypt/live/%.joeac.net=%)
|
||||
tls_cert_subdomains_to_delete = $(filter-out $(SUBDOMAINS),$(installed_tls_cert_subdomains))
|
||||
|
||||
tls_crontab = /etc/periodic/daily/tls-$(subdomain).joeac.net
|
||||
tls_cert = /etc/letsencrypt/live/$(subdomain).joeac.net/fullchain.pem
|
||||
|
||||
define install_tls_subdomain_rule =
|
||||
.PHONY: install_tls_$(subdomain)
|
||||
install_tls_$(subdomain): $(tls_crontab) $(tls_cert)
|
||||
endef
|
||||
|
||||
define reinstall_tls_subdomain_rule =
|
||||
.PHONY: reinstall_tls_$(subdomain)
|
||||
reinstall_tls_$(subdomain): $(tls_crontab) renew_$(tls_cert)
|
||||
endef
|
||||
|
||||
define uninstall_tls_subdomain_rule =
|
||||
.PHONY: uninstall_tls_$(subdomain)
|
||||
uninstall_tls_$(subdomain): delete_cert_$(subdomain).joeac.net
|
||||
$(if $(shell [ -f $(tls_crontab) ] && echo 1),\
|
||||
sudo rm -f $(tls_crontab)
|
||||
)
|
||||
endef
|
||||
|
||||
define obtain_or_renew_cert_cmd =
|
||||
sudo certbot certonly \
|
||||
--nginx \
|
||||
--cert-name $(subdomain).joeac.net \
|
||||
--domain $(subst @.,,$(subdomain).)joeac.net \
|
||||
--non-interactive \
|
||||
&& sudo chown -R joeac.net:joeac.net $(dir $(tls_cert))
|
||||
endef
|
||||
|
||||
is_cert_expired = $(shell sudo certbot certificates --cert-name $(subdomain).joeac.net \
|
||||
| grep "Expiry Date" | grep -E "(EXPIRED|REVOKED)")
|
||||
define cert_rule =
|
||||
$(tls_cert):
|
||||
$(obtain_or_renew_cert_cmd)
|
||||
|
||||
.PHONY: renew_$(tls_cert)
|
||||
renew_$(tls_cert):
|
||||
$$(if $$(is_cert_expired),$(obtain_or_renew_cert_cmd))
|
||||
endef
|
||||
|
||||
$(foreach subdomain,$(ALL_SUBDOMAINS), $(eval $(install_tls_subdomain_rule)))
|
||||
$(foreach subdomain,$(ALL_SUBDOMAINS), $(eval $(reinstall_tls_subdomain_rule)))
|
||||
$(foreach subdomain,$(ALL_SUBDOMAINS), $(eval $(uninstall_tls_subdomain_rule)))
|
||||
$(foreach subdomain,$(ALL_SUBDOMAINS), $(eval $(cert_rule)))
|
||||
|
||||
.PHONY: remove_/etc/periodic/daily/tls-%.joeac.net
|
||||
remove_/etc/periodic/daily/tls-%.joeac.net:
|
||||
rm -f $(@:remove_%=%)
|
||||
|
||||
.PHONY: delete_cert_%
|
||||
delete_cert_%:
|
||||
sudo certbot delete --cert-name $(@:delete_cert_%=%).joeac.net --non-interactive
|
||||
|
||||
/etc/periodic/daily/tls-%joeac.net:
|
||||
echo "#!/bin/sh" > crontab.tmp
|
||||
echo "$(obtain_or_renew_cert_cmd)" >> crontab.tmp
|
||||
sudo mv crontab.tmp $@
|
||||
sudo chmod +x $@
|
||||
|
||||
certs_to_renew = $(foreach subdomain,$(filter $(SUBDOMAINS),$(installed_tls_cert_subdomains)),$(tls_cert))
|
||||
tls_crontabs_to_install = $(foreach subdomain,$(SUBDOMAINS),$(tls_crontab))
|
||||
tls_certs_to_install = $(foreach subdomain,$(SUBDOMAINS),$(tls_cert))
|
||||
.PHONY: install_tls
|
||||
install_tls: $(tls_crontabs_to_install) $(tls_certs_to_install) $(addprefix remove_,$(tls_crontabs_to_remove)) $(addprefix delete_cert_,$(tls_cert_subdomains_to_delete)) $(addprefix renew_,$(certs_to_renew))
|
||||
|
||||
.PHONY: reinstall_tls
|
||||
reinstall_tls: install_tls
|
||||
|
||||
.PHONY: uninstall_tls
|
||||
uninstall_tls: $(foreach subdomain,$(ALL_SUBDOMAINS),$(uninstall_tls_$(subdomain)))
|
||||
|
||||
.PHONY: install_certbot
|
||||
install_certbot: /usr/bin/certbot /usr/bin/python /usr/lib/$(shell ls /usr/bin | grep python[0-9]\\.)/certbot_nginx
|
||||
|
||||
/usr/bin/certbot /usr/bin/python:
|
||||
sudo apk add certbot
|
||||
|
||||
/usr/lib/python%/certbot_nginx:
|
||||
sudo apk add certbot-nginx
|
||||
@@ -0,0 +1,91 @@
|
||||
capitalise = $(shell echo "$(1)" | tr '[:lower:]' '[:upper:]')
|
||||
|
||||
USER := $(shell whoami)
|
||||
HOSTNAME := $(shell cat /etc/hostname)
|
||||
HOSTNAMES := pi-broughton blade-canongate
|
||||
IP_ADDR_pi-broughton := 192.168.178.54
|
||||
IP_ADDR_blade-canongate := 192.168.178.75
|
||||
MASTER_NODE := blade-canongate
|
||||
IS_MASTER_NODE := $(filter $(MASTER_NODE),$(HOSTNAME))
|
||||
MODULES_pi-broughton := actualbudget http smtp vaultwarden ln
|
||||
MODULES_blade-canongate := etherpad gemini mox mox_clientsettings mox_autoconfig mox_mta_sts
|
||||
ALL_NGINX_MODULES := actualbudget http vaultwarden ln etherpad mox mox_clientsettings mox_autoconfig mox_mta_sts
|
||||
NGINX_MODULES := $(if $(IS_MASTER_NODE),$(ALL_NGINX_MODULES))
|
||||
MODULES := $(MODULES_$(HOSTNAME))
|
||||
ALL_MODULES := $(sort $(foreach hostname,$(HOSTNAMES),$(MODULES_$(hostname))))
|
||||
COMPOSE_SERVICES := $(shell podman-compose config \
|
||||
| yq ".services | keys" --output-format csv --csv-separator " ")
|
||||
MAKE_MODULES := $(foreach module,$(MODULES),\
|
||||
$(shell [ -f $(module)/Makefile ] && echo $(module)))
|
||||
|
||||
SUBDOMAIN_actualbudget := budget
|
||||
SUBDOMAIN_http := @
|
||||
SUBDOMAIN_vaultwarden := pwd
|
||||
SUBDOMAIN_etherpad := docs
|
||||
SUBDOMAIN_ln := ln
|
||||
SUBDOMAIN_mox := mail
|
||||
SUBDOMAIN_mox_autoconfig := autoconfig.mail
|
||||
SUBDOMAIN_mox_clientsettings := clientsettings.mail
|
||||
SUBDOMAIN_mox_mta_sts := mta-sts.mail
|
||||
|
||||
PORT_actualbudget := 5006
|
||||
PORT_etherpad := 9001
|
||||
PORT_http := 8080
|
||||
PORT_gemini := 1965
|
||||
PORT_mox := 81
|
||||
PORT_mox_autoconfig := $(PORT_mox)
|
||||
PORT_mox_clientsettings := $(PORT_mox)
|
||||
PORT_mox_mta_sts := $(PORT_mox)
|
||||
PORT_smtp := 2500
|
||||
PORT_vaultwarden := 9000
|
||||
|
||||
PUBLIC_ROOT_DIR_ln := /var/ln.joeac.net/public
|
||||
|
||||
export ACTUALBUDGET_DATA_DIR := /var/joeac.net-actualbudget/var
|
||||
export ETHERPAD_DATA_DIR := /var/etherpad/var
|
||||
export GEMINI_CERTIFICATES_DIR := /var/joeac.net-gemini/certificates
|
||||
export GEMINI_COMITIUM_DATA_DIR := /var/joeac.net-gemini/comitium-data
|
||||
export VAULTWARDEN_DATA_DIR := /var/vaultwarden/data
|
||||
|
||||
ALPINE_VERSION := 3.23
|
||||
ETHERPAD_VERSION := 3.3.2
|
||||
|
||||
$(foreach module,$(ALL_MODULES),$(if $(PORT_$(module)),$(eval \
|
||||
export $(call capitalise,$(module))_PORT := $(PORT_$(module)))))
|
||||
$(foreach hostname,$(HOSTNAMES),$(foreach module,$(MODULES_$(hostname)),$(eval \
|
||||
export HOST_$(module) := $(if $(filter $(HOSTNAME),$(hostname)),127.0.0.1,$(IP_ADDR_$(hostname))))))
|
||||
|
||||
ALL_SUBDOMAINS := $(foreach module,$(ALL_MODULES),$(SUBDOMAIN_$(module)))
|
||||
SUBDOMAINS := $(if $(IS_MASTER_NODE),$(ALL_SUBDOMAINS))
|
||||
NGINX_SUBDOMAINS := $(foreach module,$(NGINX_MODULES),$(SUBDOMAIN_$(module)))
|
||||
|
||||
ENV_RULES := $(foreach module,$(MODULES),$(if $(shell test -d $(module) && echo 1),$(module)/.env))
|
||||
MAKE_RULES := $(foreach module,$(MAKE_MODULES),make_$(module))
|
||||
BUILD_RULES := $(foreach module,$(filter $(COMPOSE_SERVICES),$(MODULES)),build_$(module))
|
||||
PUSH_RULES := $(foreach module,$(filter $(COMPOSE_SERVICES),$(MODULES)),push_$(module))
|
||||
INSTALL_RULES := $(foreach module,$(MODULES),install_$(module))
|
||||
REINSTALL_RULES := $(foreach module,$(MODULES),reinstall_$(module))
|
||||
UNINSTALL_RULES := $(foreach module,$(MODULES),uninstall_$(module))
|
||||
|
||||
CPU_ARCH := $(if $(shell which arch 2>/dev/null),\
|
||||
$(shell arch),\
|
||||
$(shell lscpu | grep ^Architecture: | sed "s/^Architecture:[[:space:]]*\([[:alnum:][:punct:]]\+\).*/\1/"))
|
||||
IMAGE_PREFIX := $(if $(filter armv7%,$(CPU_ARCH)),armv7/)
|
||||
COMPOSE_CMD := \
|
||||
ALPINE_VERSION="$(ALPINE_VERSION)" \
|
||||
IMAGE_PREFIX="$(IMAGE_PREFIX)" \
|
||||
GEMINI_CERTIFICATES_DIR="$(GEMINI_CERTIFICATES_DIR)" \
|
||||
GEMINI_COMITIUM_DATA_DIR="$(GEMINI_COMITIUM_DATA_DIR)" \
|
||||
ETHERPAD_DATA_DIR="$(ETHERPAD_DATA_DIR)" \
|
||||
ETHERPAD_VERSION="$(ETHERPAD_VERSION)" \
|
||||
VAULTWARDEN_DATA_DIR="$(VAULTWARDEN_DATA_DIR)" \
|
||||
LOCAL_SMTP_PORT=$(PORT_smtp) \
|
||||
$(foreach module,$(ALL_MODULES),$(call capitalise,$(module))_PORT=$(PORT_$(module))) \
|
||||
podman-compose
|
||||
|
||||
$(foreach module,$(ALL_NGINX_MODULES), \
|
||||
$(if $(SUBDOMAIN_$(module)),, \
|
||||
$(error $(module) is declared as an nginx module, but SUBDOMAIN_$(module) is not set)))
|
||||
$(foreach module,$(ALL_NGINX_MODULES), \
|
||||
$(if $(PORT_$(module)) $(HOST_$(module)),,$(if $(PUBLIC_ROOT_DIR_$(module)),, \
|
||||
$(error $(module) is declared as an nginx module, but neither PUBLIC_ROOT_DIR_$(module), nor both PORT_$(module) and HOST_$(module), are set))))
|
||||
@@ -0,0 +1,2 @@
|
||||
config/dkim
|
||||
config/adminpasswd
|
||||
@@ -0,0 +1,6 @@
|
||||
# mox
|
||||
|
||||
There are three secrets required for mox. One is the admin password, which
|
||||
should be stored in config/adminpasswd. The other two are DKIM private keys,
|
||||
which should be stored in the config/dkim directory. The public parts of these
|
||||
keys should be exposed in DNS TXT records.
|
||||
@@ -0,0 +1,52 @@
|
||||
Domains:
|
||||
mail.joeac.net:
|
||||
ClientSettingsDomain: clientsettings.mail.joeac.net
|
||||
LocalpartCatchallSeparator: +
|
||||
DKIM:
|
||||
Selectors:
|
||||
2026a:
|
||||
Expiration: 72h
|
||||
PrivateKeyFile: dkim/2026a._domainkey.mail.joeac.net.20260705T163220.rsa2048.privatekey.pkcs8.pem
|
||||
2026b:
|
||||
Expiration: 72h
|
||||
PrivateKeyFile: dkim/2026b._domainkey.mail.joeac.net.20260705T163220.rsa2048.privatekey.pkcs8.pem
|
||||
Sign:
|
||||
- 2026a
|
||||
DMARC:
|
||||
Localpart: dmarcreports
|
||||
Account: me
|
||||
Mailbox: DMARC
|
||||
MTASTS:
|
||||
PolicyID: 20260705T153220
|
||||
Mode: enforce
|
||||
MaxAge: 24h0m0s
|
||||
MX:
|
||||
- mail.joeac.net
|
||||
TLSRPT:
|
||||
Localpart: tlsreports
|
||||
Account: me
|
||||
Mailbox: TLSRPT
|
||||
Accounts:
|
||||
me:
|
||||
Domain: mail.joeac.net
|
||||
Destinations:
|
||||
me@mail.joeac.net: nil
|
||||
SubjectPass:
|
||||
Period: 12h0m0s
|
||||
RejectsMailbox: Rejects
|
||||
AutomaticJunkFlags:
|
||||
Enabled: true
|
||||
JunkMailboxRegexp: ^(junk|spam)
|
||||
NeutralMailboxRegexp: ^(inbox|neutral|postmaster|dmarc|tlsrpt|rejects)
|
||||
JunkFilter:
|
||||
Threshold: 0.950000
|
||||
Params:
|
||||
Onegrams: true
|
||||
MaxPower: 0.010000
|
||||
TopWords: 10
|
||||
IgnoreWords: 0.100000
|
||||
RareWords: 2
|
||||
NoCustomPassword: true
|
||||
MonitorDNSBLs:
|
||||
- zen.spamhaus.org
|
||||
- bl.spamcop.net
|
||||
@@ -0,0 +1,74 @@
|
||||
DataDir: ../data
|
||||
LogLevel: debug
|
||||
User: mox
|
||||
Hostname: mail.joeac.net
|
||||
AdminPasswordFile: adminpasswd
|
||||
Listeners:
|
||||
internal:
|
||||
IPs:
|
||||
- 127.0.0.1
|
||||
- ::1
|
||||
Hostname: localhost
|
||||
AccountHTTP:
|
||||
Enabled: true
|
||||
Port: 1080
|
||||
Forwarded: true
|
||||
AdminHTTP:
|
||||
Enabled: true
|
||||
Port: 1080
|
||||
Forwarded: true
|
||||
WebmailHTTP:
|
||||
Enabled: true
|
||||
Port: 1080
|
||||
Forwarded: true
|
||||
WebAPIHTTP:
|
||||
Enabled: true
|
||||
Port: 1080
|
||||
Forwarded: true
|
||||
MetricsHTTP:
|
||||
Enabled: true
|
||||
AutoconfigHTTPS:
|
||||
Enabled: true
|
||||
Port: 81
|
||||
NonTLS: true
|
||||
MTASTSHTTPS:
|
||||
Enabled: true
|
||||
Port: 81
|
||||
NonTLS: true
|
||||
WebserverHTTP:
|
||||
Enabled: true
|
||||
Port: 81
|
||||
public:
|
||||
IPs:
|
||||
- 192.168.178.75
|
||||
- fdc9:6aec:7a18:0:2e0:4cff:fe61:9b17
|
||||
NATIPs:
|
||||
- 217.155.190.42
|
||||
- fdc9:6aec:7a18:0:2e0:4cff:fe61:9b17
|
||||
TLS:
|
||||
KeyCerts:
|
||||
-
|
||||
CertFile: /etc/letsencrypt/live/mail.joeac.net/fullchain.pem
|
||||
KeyFile: /etc/letsencrypt/live/mail.joeac.net/privkey.pem
|
||||
-
|
||||
CertFile: /etc/letsencrypt/live/mta-sts.mail.joeac.net/fullchain.pem
|
||||
KeyFile: /etc/letsencrypt/live/mta-sts.mail.joeac.net/privkey.pem
|
||||
-
|
||||
CertFile: /etc/letsencrypt/live/autoconfig.mail.joeac.net/fullchain.pem
|
||||
KeyFile: /etc/letsencrypt/live/autoconfig.mail.joeac.net/privkey.pem
|
||||
-
|
||||
CertFile: /etc/letsencrypt/live/clientsettings.mail.joeac.net/fullchain.pem
|
||||
KeyFile: /etc/letsencrypt/live/clientsettings.mail.joeac.net/privkey.pem
|
||||
SMTP:
|
||||
Enabled: true
|
||||
Submissions:
|
||||
Enabled: true
|
||||
IMAPS:
|
||||
Enabled: true
|
||||
Postmaster:
|
||||
Account: me
|
||||
Mailbox: Postmaster
|
||||
HostTLSRPT:
|
||||
Account: me
|
||||
Mailbox: TLSRPT
|
||||
Localpart: tlsreports
|
||||
@@ -0,0 +1,8 @@
|
||||
server:
|
||||
auto-trust-anchor-file: /var/lib/unbound/root.key
|
||||
qname-minimisation: yes
|
||||
interface: 0.0.0.0
|
||||
access-control: 192.168.0.0/16 allow
|
||||
|
||||
remote-control:
|
||||
control-enable: yes
|
||||
@@ -0,0 +1,63 @@
|
||||
.PHONY: install
|
||||
install: install_unbound install_mox
|
||||
|
||||
.PHONY: reinstall
|
||||
reinstall: install_unbound install_mox
|
||||
|
||||
.PHONY: install_unbound
|
||||
install_unbound: /usr/sbin/unbound /etc/resolv.conf /var/lib/unbound/root.key install_unbound_anchor_crontab /etc/unbound/unbound.conf.d/dnssec.conf
|
||||
|
||||
/usr/sbin/unbound:
|
||||
sudo apk add unbound
|
||||
sudo rc-update add unbound default
|
||||
sudo rc-service unbound start
|
||||
|
||||
/etc/resolv.conf: resolv.conf /etc/resolv.conf.joeac.net-backup
|
||||
sudo cp resolv.conf /etc/resolv.conf
|
||||
|
||||
/etc/resolv.conf.joeac.net-backup:
|
||||
sudo mv /etc/resolv.conf /etc/resolv.conf.joeac.net-backup
|
||||
|
||||
/var/lib/unbound/root.key:
|
||||
sudo mkdir -p /var/lib/unbound && sudo unbound-anchor -a /var/lib/unbound/root.key
|
||||
|
||||
UNBOUND_ANCHOR_CRONTAB_ENTRY := @reboot unbound-anchor -a /var/lib/unbound/root.key # managed by joeac.net
|
||||
IS_CRONTAB_UP_TO_DATE := $(sudo grep "$(UNBOUND_ANCHOR_CRONTAB_ENTRY)" /etc/crontabs/root)
|
||||
.PHONY: install_unbound_anchor_crontab
|
||||
install_unbound_anchor_crontab:
|
||||
$(if $(IS_CRONTAB_UP_TO_DATE),,\
|
||||
sudo crontab -l > crontab.tmp; \
|
||||
sed -i "s/.*unbound-anchor.*# managed by joeac.net//" crontab.tmp; \
|
||||
echo "$(UNBOUND_ANCHOR_CRONTAB_ENTRY)" >> crontab.tmp; \
|
||||
sudo crontab crontab.tmp; \
|
||||
rm crontab.tmp; \
|
||||
)
|
||||
|
||||
/etc/unbound/unbound.conf.d/dnssec.conf: dnssec.conf
|
||||
sudo mkdir -p $(dir $@) && sudo cp $< $@
|
||||
|
||||
DKIM_PRIVATE_KEY_A := ~/mox/config/dkim/2026a._domainkey.mail.joeac.net.20260705T163220.rsa2048.privatekey.pkcs8.pem
|
||||
DKIM_PRIVATE_KEY_B := ~/mox/config/dkim/2026b._domainkey.mail.joeac.net.20260705T163220.rsa2048.privatekey.pkcs8.pem
|
||||
DKIM_PRIVATE_KEYS := $(DKIM_PRIVATE_KEY_A) $(DKIM_PRIVATE_KEY_B)
|
||||
MOX_TLS_CERTS := /etc/letsencrypt/live/mail.joeac.net/fullchain.pem /etc/letsencrypt/live/autoconfig.mail.joeac.net/fullchain.pem /etc/letsencrypt/live/clientsettings.mail.joeac.net/fullchain.pem /etc/letsencrypt/live/mta-sts.mail.joeac.net/fullchain.pem
|
||||
.PHONY: install_mox
|
||||
install_mox: /usr/local/bin/mox ~/mox/config/mox.conf ~/mox/config/domains.conf ~/mox/config/adminpasswd ~/mox/data $(DKIM_PRIVATE_KEYS) $(MOX_TLS_CERTS)
|
||||
|
||||
MOX_PLATFORM := $(if $(filter armv7% arm32%,$(CPU_ARCH)),arm,amd64)
|
||||
MOX_VERSION := 0.0.15
|
||||
MOX_GO_VERSION := 1.26.4
|
||||
MOX_CHECKSUM_amd64_v0.0.15_go1.26.4 := 09OE-1QkNVgmpoRj53mtX9gxoEmY
|
||||
MOX_CHECKSUM_arm_v0.0.15_go1.26.4 := 038X9nQx6hhai60Fk1BhcP3r6Mew
|
||||
MOX_CHECKSUM := $(MOX_CHECKSUM_$(MOX_PLATFORM)_v$(MOX_VERSION)_go$(MOX_GO_VERSION))
|
||||
MOX_URL_BASE := https://beta.gobuilds.org/github.com/mjl-/mox
|
||||
MOX_URL := $(MOX_URL_BASE)@v$(MOX_VERSION)/linux-$(MOX_PLATFORM)-go$(MOX_GO_VERSION)/$(MOX_CHECKSUM)
|
||||
/usr/local/bin/mox:
|
||||
wget -O- $(MOX_URL) | gzip -d > mox
|
||||
chmod +x mox
|
||||
sudo mv mox /usr/local/bin/mox
|
||||
|
||||
~/mox/config/%: config/%
|
||||
mkdir -p $(dir $@) && cp $< $@
|
||||
|
||||
~/mox/data:
|
||||
mkdir -p $@
|
||||
@@ -0,0 +1 @@
|
||||
nameserver: 127.0.0.1
|
||||
@@ -0,0 +1,20 @@
|
||||
server {
|
||||
server_name ${DOMAIN};
|
||||
location / {
|
||||
proxy_pass http://${HOST}:${PORT};
|
||||
}
|
||||
listen 443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/${DOMAIN}/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/${DOMAIN}/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
if ($$host = ${DOMAIN}) {
|
||||
return 301 https://$$host$$request_uri;
|
||||
}
|
||||
server_name ${DOMAIN};
|
||||
listen 80;
|
||||
return 404;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
server {
|
||||
server_name ${DOMAIN};
|
||||
location / {
|
||||
root ${ROOT};
|
||||
try_files $$uri $$uri.html $$uri/index.html =404;
|
||||
error_page 404 /index.html;
|
||||
}
|
||||
listen 443 ssl;
|
||||
ssl_certificate /etc/letsencrypt/live/${CERTNAME}/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/${CERTNAME}/privkey.pem;
|
||||
include /etc/letsencrypt/options-ssl-nginx.conf;
|
||||
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
|
||||
}
|
||||
|
||||
server {
|
||||
if ($$host = ${DOMAIN}) {
|
||||
return 301 https://$$host$$request_uri;
|
||||
} # managed by Certbot
|
||||
server_name ln.joeac.net;
|
||||
listen 80;
|
||||
return 404;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
|
||||
types {
|
||||
text/html html htm shtml;
|
||||
text/css css;
|
||||
text/xml xml;
|
||||
image/gif gif;
|
||||
image/jpeg jpeg jpg;
|
||||
application/javascript js;
|
||||
application/atom+xml atom;
|
||||
application/rss+xml rss;
|
||||
|
||||
text/mathml mml;
|
||||
text/plain txt;
|
||||
text/vnd.sun.j2me.app-descriptor jad;
|
||||
text/vnd.wap.wml wml;
|
||||
text/x-component htc;
|
||||
|
||||
image/avif avif;
|
||||
image/png png;
|
||||
image/svg+xml svg svgz;
|
||||
image/tiff tif tiff;
|
||||
image/vnd.wap.wbmp wbmp;
|
||||
image/webp webp;
|
||||
image/x-icon ico;
|
||||
image/x-jng jng;
|
||||
image/x-ms-bmp bmp;
|
||||
|
||||
font/woff woff;
|
||||
font/woff2 woff2;
|
||||
|
||||
application/java-archive jar war ear;
|
||||
application/json json;
|
||||
application/mac-binhex40 hqx;
|
||||
application/msword doc;
|
||||
application/pdf pdf;
|
||||
application/postscript ps eps ai;
|
||||
application/rtf rtf;
|
||||
application/vnd.apple.mpegurl m3u8;
|
||||
application/vnd.google-earth.kml+xml kml;
|
||||
application/vnd.google-earth.kmz kmz;
|
||||
application/vnd.ms-excel xls;
|
||||
application/vnd.ms-fontobject eot;
|
||||
application/vnd.ms-powerpoint ppt;
|
||||
application/vnd.oasis.opendocument.graphics odg;
|
||||
application/vnd.oasis.opendocument.presentation odp;
|
||||
application/vnd.oasis.opendocument.spreadsheet ods;
|
||||
application/vnd.oasis.opendocument.text odt;
|
||||
application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||
pptx;
|
||||
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||
xlsx;
|
||||
application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||
docx;
|
||||
application/vnd.wap.wmlc wmlc;
|
||||
application/wasm wasm;
|
||||
application/x-7z-compressed 7z;
|
||||
application/x-cocoa cco;
|
||||
application/x-java-archive-diff jardiff;
|
||||
application/x-java-jnlp-file jnlp;
|
||||
application/x-makeself run;
|
||||
application/x-perl pl pm;
|
||||
application/x-pilot prc pdb;
|
||||
application/x-rar-compressed rar;
|
||||
application/x-redhat-package-manager rpm;
|
||||
application/x-sea sea;
|
||||
application/x-shockwave-flash swf;
|
||||
application/x-stuffit sit;
|
||||
application/x-tcl tcl tk;
|
||||
application/x-x509-ca-cert der pem crt;
|
||||
application/x-xpinstall xpi;
|
||||
application/xhtml+xml xhtml;
|
||||
application/xspf+xml xspf;
|
||||
application/zip zip;
|
||||
|
||||
application/octet-stream bin exe dll;
|
||||
application/octet-stream deb;
|
||||
application/octet-stream dmg;
|
||||
application/octet-stream iso img;
|
||||
application/octet-stream msi msp msm;
|
||||
|
||||
audio/midi mid midi kar;
|
||||
audio/mpeg mp3;
|
||||
audio/ogg ogg;
|
||||
audio/x-m4a m4a;
|
||||
audio/x-realaudio ra;
|
||||
|
||||
video/3gpp 3gpp 3gp;
|
||||
video/mp2t ts;
|
||||
video/mp4 mp4;
|
||||
video/mpeg mpeg mpg;
|
||||
video/quicktime mov;
|
||||
video/webm webm;
|
||||
video/x-flv flv;
|
||||
video/x-m4v m4v;
|
||||
video/x-mng mng;
|
||||
video/x-ms-asf asx asf;
|
||||
video/x-ms-wmv wmv;
|
||||
video/x-msvideo avi;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
# /etc/nginx/nginx.conf
|
||||
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
pcre_jit on;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
server_tokens off;
|
||||
client_max_body_size 1m;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:2m;
|
||||
ssl_session_timeout 1h;
|
||||
gzip on;
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
include /etc/nginx/http.d/*.conf;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
UID="$$(id -u ${USER})"
|
||||
if test -z "$${XDG_RUNTIME_DIR}"; then
|
||||
export XDG_RUNTIME_DIR=/tmp/xdg/"$${UID}"-xdg-runtime-dir
|
||||
if ! test -d "$${XDG_RUNTIME_DIR}"; then
|
||||
mkdir -p "$${XDG_RUNTIME_DIR}"
|
||||
chown -R ${USER}:${USER} "$${XDG_RUNTIME_DIR}"
|
||||
chmod -R 0700 "$${XDG_RUNTIME_DIR}"
|
||||
fi
|
||||
fi
|
||||
if ! test -f "$${XDG_RUNTIME_DIR}/openrc/softlevel"; then
|
||||
mkdir -p "$${XDG_RUNTIME_DIR}/openrc"
|
||||
touch "$${XDG_RUNTIME_DIR}/openrc/softlevel"
|
||||
chown -R ${USER}:${USER} "$${XDG_RUNTIME_DIR}"
|
||||
chmod -R 0700 "$${XDG_RUNTIME_DIR}"
|
||||
fi
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/sbin/openrc-run
|
||||
# credits:
|
||||
# https://gist.github.com/itzwam/2069e935385193207f7e5bea7156e21d
|
||||
# https://github.com/0x17de/dockerservice-openrc
|
||||
|
||||
: $${HOME:=/home/joeac.net}
|
||||
: $${MODULENAME:=$${RC_SVCNAME##*.}}
|
||||
: $${SRCDIR:=$$HOME/joeac.net}
|
||||
DOCKER_COMPOSE_UP_ARGS=$${DOCKER_COMPOSE_UP_ARGS-"--no-build --no-recreate --no-deps"}
|
||||
|
||||
[ -z "$${MODULENAME}" ] && exit 1
|
||||
: $${COMPOSEFILE:="$${SRCDIR}/compose.yml"}
|
||||
: $${MAKEFILE:="$${SRCDIR}/Makefile"}
|
||||
export COMPOSE_HTTP_TIMEOUT=300
|
||||
|
||||
description="Manage dockerservices defined in $${COMPOSEFILE}"
|
||||
extra_commands="configtest"
|
||||
description_configtest="Check configuration via \"podman-compose -f $${COMPOSEFILE} config\""
|
||||
|
||||
configtest() {
|
||||
if ! [ -f "$${COMPOSEFILE}" ]; then
|
||||
eerror "The docker-compose file $${COMPOSEFILE} does not exist!"
|
||||
return 1
|
||||
fi
|
||||
if ${COMPOSECMD} -f "$${COMPOSEFILE}" config >&/dev/null; then
|
||||
einfo "config: ok"
|
||||
else
|
||||
eerror "config: error"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
start() {
|
||||
configtest || return 1
|
||||
ebegin "Starting $${MODULENAME}"
|
||||
${COMPOSECMD} -f "$${COMPOSEFILE}" up -d $${DOCKER_COMPOSE_UP_ARGS} $${MODULENAME}
|
||||
eend $$?
|
||||
}
|
||||
|
||||
stop() {
|
||||
ebegin "Stopping $${MODULENAME}"
|
||||
${COMPOSECMD} -f "$${COMPOSEFILE}" down --timeout=5 $${MODULENAME}
|
||||
eend $$?
|
||||
}
|
||||
|
||||
status() {
|
||||
pods=0
|
||||
started=0
|
||||
while read pod; do
|
||||
pod_service="$$(podman inspect $$pod | jq .[0].Config.Labels.[\"com.docker.compose.service\"])"
|
||||
if ! [ "$$pod_service" = "\"$${MODULENAME}\"" ]; then
|
||||
continue
|
||||
fi
|
||||
pods=$$(($$pods+1))
|
||||
if podman top $$pod &>/dev/null; then
|
||||
einfo "status: [$$pod] started"
|
||||
exit 0
|
||||
else
|
||||
einfo "status: [$$pod] stopped"
|
||||
exit 3
|
||||
fi
|
||||
done < <(${COMPOSECMD} -f "$${COMPOSEFILE}" ps --quiet)
|
||||
einfo "status: no container found for $${MODULENAME}"
|
||||
exit 3
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
FROM git.joeac.net/joeac/armv7/msmtp:1.8.32-alpine3.23
|
||||
RUN apk --no-cache add gettext gnutls
|
||||
ARG SMTP_PORT
|
||||
EXPOSE $SMTP_PORT
|
||||
COPY smtp/.msmtprc ./
|
||||
@@ -1,26 +0,0 @@
|
||||
FROM alpine:3.23 AS build
|
||||
|
||||
WORKDIR /
|
||||
RUN mkdir -p /var/msmtp
|
||||
RUN apk --update --no-cache add git autoconf automake build-base gettext-dev gnutls-dev libtool make texinfo && \
|
||||
git clone https://github.com/marlam/msmtp.git --branch msmtp-1.8.32 --single-branch --depth 1
|
||||
WORKDIR /msmtp
|
||||
RUN autoreconf -fi && \
|
||||
./configure && \
|
||||
make && \
|
||||
make install && \
|
||||
apk del git autoconf automake build-base gettext-dev gnutls-dev libtool make texinfo
|
||||
|
||||
FROM alpine:3.23 AS final
|
||||
RUN apk --update --no-cache add gettext
|
||||
COPY --from=build /usr/local/bin/msmtp /usr/local/bin/msmtpd /usr/local/bin/
|
||||
ARG LOCAL_SMTP_PORT
|
||||
EXPOSE $LOCAL_SMTP_PORT
|
||||
COPY .msmtprc ./
|
||||
|
||||
CMD msmtpd \
|
||||
--auth=$LOCAL_SMTP_USER,'echo $LOCAL_SMTP_PASSWORD' \
|
||||
--command='msmtp -C .msmtprc -f %F --' \
|
||||
--interface=0.0.0.0 \
|
||||
--log=/var/msmtp/msmtpd.log \
|
||||
--port=$LOCAL_SMTP_PORT
|
||||
@@ -0,0 +1,7 @@
|
||||
all: Makefile
|
||||
|
||||
Makefile: Makefile
|
||||
echo "Nothing to be done for smtp: this Makefile is a placeholder."
|
||||
|
||||
clean:
|
||||
echo "Nothing to be cleaned up for smtp: this rule is a placeholder."
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user