-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathMakefile
More file actions
455 lines (426 loc) · 17.1 KB
/
Makefile
File metadata and controls
455 lines (426 loc) · 17.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
# SPDX-FileCopyrightText: Bernhard Posselt <dev@bernhard-posselt.com>
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# AutoCurrency — Makefile
# ---------------------------------
# A friendly, batteries-included Makefile for building and packaging a Nextcloud app
# that uses pnpm (JS) and Composer (PHP).
#
# Requirements:
# - make, which, curl, tar
# - pnpm (for JS build/lint/test)
# - composer (optional; will auto-download local composer.phar if missing)
#
# Conventions:
# - If no composer.json → Composer step is skipped.
# - If no package.json (root) and js/package.json missing → pnpm step is skipped.
# - JS build is delegated to your package.json scripts (tool-agnostic).
#
# Common recipes:
# make build → install deps & build
# make dist → build source + appstore tarballs
# make test → run PHP unit tests
# make lint → lint JS & PHP
# make openapi → generate OpenAPI JSON
# make sign → print signature for GitHub tarball
# make release → upload release to Nextcloud App Store
#
app_name=autocurrency
repo_path=chenasraf/nextcloud-$(app_name)
build_tools_directory=$(CURDIR)/build/tools
source_build_directory=$(CURDIR)/build/artifacts/source
source_intermediate_directory=$(CURDIR)/build/artifacts/intermediate-source/$(app_name)
source_package_name=$(source_build_directory)/$(app_name)
app_intermediate_directory=$(CURDIR)/build/artifacts/intermediate/$(app_name)
appstore_build_directory=$(CURDIR)/build/artifacts/appstore
appstore_package_name=$(appstore_build_directory)/$(app_name)
pnpm=$(shell which pnpm 2> /dev/null)
composer=$(shell which composer 2> /dev/null)
composer_phar=$(build_tools_directory)/composer.phar
composer_bin := $(if $(composer),$(composer),php $(composer_phar))
pnpm_wrapper=$(build_tools_directory)/pnpm.sh
pnpm_cmd=$(if $(pnpm),$(pnpm),$(pnpm_wrapper))
# Optional: Set path to Nextcloud installation for local testing
# Can be overridden by environment variable: NEXTCLOUD_ROOT=/path make test
NEXTCLOUD_ROOT ?=
# Default target: install deps & build JS (and PHP if composer.json exists)
all: build
# build:
# - Composer install if composer.json exists (skips if vendor/ exists)
# - pnpm install & build if package.json (root) or js/package.json exists
.PHONY: build
build:
ifneq (,$(wildcard $(CURDIR)/composer.json))
make composer
endif
ifneq (,$(wildcard $(CURDIR)/package.json))
make pnpm
endif
ifneq (,$(wildcard $(CURDIR)/js/package.json))
make pnpm
endif
$(composer_phar):
@echo "No system composer found; installing local composer.phar"
mkdir -p $(build_tools_directory)
curl -sS https://getcomposer.org/installer | php
mv composer.phar $(composer_phar)
# composer:
# - Use system composer if available, else download local composer.phar
# - Skip install if vendor/ already exists
.PHONY: composer
composer: $(if $(composer),, $(composer_phar))
ifneq ("$(wildcard vendor)","")
@echo "Vendor directory already exists, skipping composer install"
else
@echo "Installing composer dependencies..."
$(composer_bin) install --prefer-dist
endif
# Ensure a local pnpm wrapper exists if pnpm is not installed globally.
# The wrapper uses Corepack to activate pnpm, then delegates to pnpm.
$(pnpm_wrapper):
@mkdir -p $(build_tools_directory); \
echo "#!/usr/bin/env bash" > $(pnpm_wrapper); \
echo "set -e" >> $(pnpm_wrapper); \
echo "if ! command -v pnpm >/dev/null 2>&1; then" >> $(pnpm_wrapper); \
echo " if command -v corepack >/dev/null 2>&1; then" >> $(pnpm_wrapper); \
echo " corepack enable >/dev/null 2>&1 || true" >> $(pnpm_wrapper); \
echo " corepack prepare pnpm@latest --activate" >> $(pnpm_wrapper); \
echo " else" >> $(pnpm_wrapper); \
echo " echo 'pnpm not found and corepack not available. Please install pnpm or Node.js (with corepack).'; exit 1" >> $(pnpm_wrapper); \
echo " fi" >> $(pnpm_wrapper); \
echo "fi" >> $(pnpm_wrapper); \
echo "exec pnpm \"\$$@\"" >> $(pnpm_wrapper); \
chmod +x $(pnpm_wrapper)
# pnpm:
# - Install JS deps (frozen lockfile)
# - Run build via root package.json if present, else fallback to js/ subdir
.PHONY: pnpm
pnpm: $(pnpm_wrapper)
$(pnpm_cmd) install --frozen-lockfile
ifeq (,$(wildcard $(CURDIR)/package.json))
cd js && $(pnpm_cmd) build
else
$(pnpm_cmd) build
endif
# clean:
# - Remove build artifacts (but keep dependencies)
.PHONY: clean
clean:
rm -rf ./build
# refresh-autoload:
# - Regenerate Composer autoload files (if composer.json exists)
.PHONY: refresh-autoload
refresh-autoload: composer
$(if $(composer),$(composer),php $(composer_phar)) dump-autoload -o
# distclean:
# - Run clean and also remove PHP/JS dependencies
.PHONY: distclean
distclean: clean
rm -rf vendor
rm -rf node_modules
rm -rf js/vendor
rm -rf js/node_modules
# dist:
# - Build both source and appstore tarballs
.PHONY: dist
dist:
make source
make appstore
# source:
# - Create a source tarball (full source, excludes dev/test artifacts)
# - Output: build/artifacts/source/$(app_name).tar.gz
.PHONY: source
source:
rm -rf $(source_build_directory)
mkdir -p $(source_build_directory)
rm -rf $(appstore_package_name).tar.gz
rsync -vtr \
--exclude="**/.git/**/*" \
--exclude="build" \
--exclude="tests" \
--exclude="/src" \
--exclude="js/node_modules" \
--exclude="node_modules" \
--exclude="*.log" \
--exclude="dist/js/*.log" \
--exclude="rename-template.sh" \
$(CURDIR)/ $(source_intermediate_directory)
cd $(CURDIR)/build/artifacts/intermediate-source && \
tar czf $(source_package_name).tar.gz $(app_name)
# appstore:
# - Create an App Store tarball (strips tests, dotfiles, dev configs)
# - Output: build/artifacts/appstore/$(app_name).tar.gz
.PHONY: appstore
appstore:
rm -rf $(appstore_build_directory)
mkdir -p $(app_intermediate_directory)
mkdir -p $(appstore_build_directory)
rm -rf $(appstore_package_name).tar.gz
rsync -vtr \
--exclude="**/.git/**/*" \
--exclude="**/.github/**/*" \
--exclude="build" \
--exclude="tests" \
--exclude="Makefile" \
--exclude="*.log" \
--exclude="phpunit*xml" \
--exclude="composer.*" \
--exclude="composer" \
--exclude="vendor" \
--exclude="vendor-bin" \
--exclude="node_modules" \
--exclude="dist/js/node_modules" \
--exclude="dist/js/tests" \
--exclude="dist/js/test" \
--exclude="dist/js/*.log" \
--exclude="dist/js/package.json" \
--exclude="dist/js/bower.json" \
--exclude="dist/js/karma.*" \
--exclude="dist/js/protractor.*" \
--exclude="package.json" \
--exclude="bower.json" \
--exclude="karma.*" \
--exclude="protractor\.*" \
--exclude="/gen" \
--exclude="/.*" \
--exclude="dist/js/.*" \
--exclude="/src" \
--exclude="rename-template.sh" \
$(CURDIR)/ $(app_intermediate_directory)
cd $(CURDIR)/build/artifacts/intermediate && \
tar czf $(appstore_package_name).tar.gz $(app_name)
# test:
# - Run PHP unit tests locally with a configured Nextcloud installation
# - Requires: A fully configured and installed Nextcloud instance with database
# - Auto-detects Nextcloud installation or uses NEXTCLOUD_ROOT (Makefile var or env var)
# - RECOMMENDED: Use 'make test-docker' instead (works in any environment)
.PHONY: test
test: composer
@NC_ROOT="$(NEXTCLOUD_ROOT)"; \
if [ -n "$$NC_ROOT" ]; then \
NC_ROOT=$$(echo "$$NC_ROOT" | sed "s|^\\\~|$$HOME|" | sed "s|^~|$$HOME|"); \
fi; \
if [ -z "$$NC_ROOT" ]; then \
if [ -d "$(CURDIR)/../../../tests/bootstrap.php" ]; then \
NC_ROOT="$(CURDIR)/../../.."; \
fi; \
fi; \
if [ -z "$$NC_ROOT" ]; then \
echo "\x1b[33mCould not find Nextcloud installation.\x1b[0m"; \
echo ""; \
echo "Local testing requires a fully configured Nextcloud instance."; \
echo ""; \
echo "Options:"; \
echo " 1. Use Docker tests (recommended): \x1b[32mmake test-docker\x1b[0m"; \
echo " 2. Set NEXTCLOUD_ROOT in Makefile (line 47) or as env var:"; \
echo " \x1b[32mNEXTCLOUD_ROOT=/path/to/nextcloud make test\x1b[0m"; \
echo ""; \
exit 1; \
fi; \
echo "\x1b[32mUsing Nextcloud root: $$NC_ROOT\x1b[0m"; \
NEXTCLOUD_ROOT="$$NC_ROOT" $(CURDIR)/vendor/phpunit/phpunit/phpunit -c tests/phpunit.xml; \
UNIT_EXIT=$$?; \
INTEG_EXIT=0; \
if [ -f tests/phpunit.integration.xml ]; then \
NEXTCLOUD_ROOT="$$NC_ROOT" $(CURDIR)/vendor/phpunit/phpunit/phpunit -c tests/phpunit.integration.xml; \
INTEG_EXIT=$$?; \
fi; \
exit $$((UNIT_EXIT + INTEG_EXIT))
# test-docker:
# - Run PHP unit tests inside a Nextcloud Docker container
# - Automatically finds the running Nextcloud container and app directory
# - Works with various Nextcloud dev environments (nextcloud-dev, custom setups, etc.)
.PHONY: test-docker
test-docker:
@echo "\x1b[33mSearching for Nextcloud container...\x1b[0m"; \
CONTAINER_ID=$$(docker ps --format "{{.ID}}\t{{.Image}}" | grep -iE 'nextcloud.*php|php.*nextcloud' | head -1 | cut -f1); \
if [ -z "$$CONTAINER_ID" ]; then \
CONTAINER_ID=$$(docker ps --format "{{.ID}}\t{{.Image}}" | grep -i nextcloud | head -1 | cut -f1); \
fi; \
if [ -z "$$CONTAINER_ID" ]; then \
CONTAINER_ID=$$(docker ps --format "{{.ID}}\t{{.Names}}" | grep -i nextcloud | head -1 | cut -f1); \
fi; \
if [ -z "$$CONTAINER_ID" ]; then \
echo "\x1b[31mError: No running Nextcloud container found\x1b[0m"; \
echo "Looking for containers with 'nextcloud' in image or container name"; \
exit 1; \
fi; \
if ! docker exec $$CONTAINER_ID which phpunit >/dev/null 2>&1; then \
echo "\x1b[31mError: Container $$CONTAINER_ID does not have phpunit installed\x1b[0m"; \
echo "Found container but it may not be the Nextcloud PHP container"; \
exit 1; \
fi; \
APP_DIR=$$(basename $(CURDIR)); \
if ! docker exec $$CONTAINER_ID test -d "apps-shared/$$APP_DIR/tests"; then \
echo "\x1b[31mError: App directory apps-shared/$$APP_DIR not found in container\x1b[0m"; \
exit 1; \
fi; \
echo "\x1b[33mRunning tests in container $$CONTAINER_ID for app $$APP_DIR\x1b[0m"; \
docker exec $$CONTAINER_ID phpunit -c apps-shared/$$APP_DIR/tests/phpunit.docker.xml
info_xsd_url=https://apps.nextcloud.com/schema/apps/info.xsd
info_xsd=$(build_tools_directory)/info.xsd
$(info_xsd):
@mkdir -p $(build_tools_directory)
curl -sS -o $(info_xsd) $(info_xsd_url)
# lint-appinfo:
# - Validate appinfo/info.xml against the Nextcloud App Store XSD schema
.PHONY: lint-appinfo
lint-appinfo: $(info_xsd)
@echo "\x1b[33mValidating appinfo/info.xml against Nextcloud schema...\x1b[0m"
@xmllint --noout --schema $(info_xsd) appinfo/info.xml
@echo "\x1b[32mappinfo/info.xml is valid.\x1b[0m"
# lint:
# - Lint JS via pnpm and PHP via composer script "lint"
.PHONY: lint
lint:
pnpm lint
$(composer_bin) run lint
# php-cs-fixer:
# - Fix staged PHP files with PHP-CS-Fixer shim (checks syntax first)
.PHONY: php-cs-fixer
php-cs-fixer:
@echo "\x1b[33mFixing PHP files...\x1b[0m"
@FILES=$$(git diff --cached --name-only --diff-filter=ACM | grep '\.php$$' | grep -v '^gen/'); \
if [ -z "$$FILES" ]; then \
echo "No PHP files staged."; \
else \
echo "Running CS fixer on:" $$FILES; \
php -l $$FILES || exit 1; \
PHP_CS_FIXER_IGNORE_ENV=true php vendor-bin/cs-fixer/vendor/php-cs-fixer/shim/php-cs-fixer.phar --config=.php-cs-fixer.dist.php fix $$FILES || exit 1; \
fi
# format:
# - Format JS and PHP (composer script "cs:fix")
.PHONY: format
format:
pnpm format
PHP_CS_FIXER_IGNORE_ENV=true $(composer_bin) run cs:fix
# openapi:
# - Generate OpenAPI spec via composer script "openapi"
# - Output: build/openapi/openapi.json
.PHONY: openapi
openapi:
@echo "\x1b[33mGenerating OpenAPI documentation...\x1b[0m"
$(composer_bin) run openapi
@echo "\x1b[32mOpenAPI documentation generated at build/openapi/openapi.json\x1b[0m"
update-pnpm-deps:
@echo "\x1b[33mUpdating pnpm dependencies and lockfile...\x1b[0m"
$(pnpm_cmd) update
@echo "\x1b[32mDependencies updated and lockfile refreshed.\x1b[0m"
update-composer-deps:
@echo "\x1b[33mUpdating Composer dependencies and lockfile...\x1b[0m"
$(composer_bin) update
@echo "\x1b[32mDependencies updated and lockfile refreshed.\x1b[0m"
update-deps: update-pnpm-deps update-composer-deps
@echo "\x1b[36mAll dependencies updated.\x1b[0m"
@echo "\x1b[36mPush changes? [Y/n]\x1b[0m"
@read ans; \
if [ "$$ans" != "n" ] && [ "$$ans" != "N" ]; then \
git add package.json pnpm-lock.yaml composer.lock vendor-bin/*/composer.lock; \
git commit -m "chore(deps): update dependencies"; \
git push; \
else \
echo "Changes not pushed."; \
fi
# csr:
# - Generate a new private key and self-signed certificate for signing releases
# and place them in ~/.nextcloud/certificates/$(app_name).{key,csr}
.PHONY: csr
csr:
@if [ -f "$$HOME/.nextcloud/certificates/$(app_name).key" ] && [ -f "$$HOME/.nextcloud/certificates/$(app_name).csr" ]; then \
echo "\x1b[31mPrivate key & CSR already exists at ~/.nextcloud/certificates/$(app_name).{key,csr}\x1b[0m"; \
else \
echo "\x1b[33mGenerating a new private key and self-signed certificate...\x1b[0m"; \
openssl req -nodes -newkey rsa:4096 -keyout $(app_name).key -out $(app_name).csr -subj "/CN=$(app_name)"; \
mkdir -p "$$HOME/.nextcloud/certificates" && \
mv "$(app_name).key" "$$HOME/.nextcloud/certificates/$(app_name).key" && \
mv "$(app_name).csr" "$$HOME/.nextcloud/certificates/$(app_name).csr" || \
echo "\x1b[31mError: Could not move key & CSR to ~/.nextcloud/certificates/\x1b[0m"; \
echo "\x1b[32mPrivate key saved to ~/.nextcloud/certificates/$(app_name).key"; \
echo "\x1b[32mCerticate signing request saved to ~/.nextcloud/certificates/$(app_name).csr"; \
echo ""; \
echo "Follow the instructions at:"; \
echo "https://nextcloudappstore.readthedocs.io/en/latest/developer.html#obtaining-a-certificate"; \
echo "to get your app registered and obtain a proper public certificate .crt file.\x1b[0m"; \
fi
# sign:
# - Print a base64 SHA-512 signature for the release tarball from GitHub.
# - Requires a private key at ~/.nextcloud/certificates/$(app_name).key
# - Reads version from version.txt
.PHONY: sign
sign:
@VERSION="$$(cat version.txt)"; \
TMPF="$$(mktemp)"; \
KEY_FILE=~/.nextcloud/certificates/$(app_name).key; \
if [ ! -f "$$KEY_FILE" ]; then \
echo "\x1b[31m❌ Error: Private key not found at $$KEY_FILE\x1b[0m"; \
exit 1; \
fi; \
echo "\x1b[33mSigning version $${VERSION}\x1b[0m"; \
echo "\x1b[33mDownloading archive...\x1b[0m"; \
curl -L https://github.com/$(repo_path)/releases/download/v$${VERSION}/$(app_name)-v$${VERSION}.tar.gz -o "$${TMPF}"; \
FILESIZE=$$(stat -f%z "$${TMPF}" 2>/dev/null || stat -c%s "$${TMPF}"); \
if [ "$${FILESIZE}" -lt 10240 ]; then \
echo "\x1b[31mError: Downloaded file is too small (<10KB, actual: $${FILESIZE} bytes)\x1b[0m"; \
rm -rf "$${TMPF}"; \
exit 1; \
fi; \
echo "\x1b[33mSigning with key $$KEY_FILE\x1b[0m"; \
echo; \
echo "\x1b[32mDownload URL:\x1b[0m https://github.com/$(repo_path)/releases/download/v$${VERSION}/$(app_name)-v$${VERSION}.tar.gz"; \
echo "\x1b[32mSignature:\x1b[0m"; \
openssl dgst -sha512 -sign "$$KEY_FILE" "$${TMPF}" | openssl base64; \
rm -rf "$${TMPF}"
# release:
# - Upload release to Nextcloud App Store using NEXTCLOUD_API_TOKEN
# - Downloads tarball from GitHub, signs it, and POSTs to App Store
.PHONY: release
release:
@VERSION="$$(cat version.txt)"; \
if [ -z "$$NEXTCLOUD_API_TOKEN" ]; then \
printf "\x1b[33mNEXTCLOUD_API_TOKEN not set. Enter token: \x1b[0m"; \
read -r NEXTCLOUD_API_TOKEN; \
fi; \
if [ -z "$$NEXTCLOUD_API_TOKEN" ]; then \
echo "\x1b[31m❌ Error: NEXTCLOUD_API_TOKEN is missing\x1b[0m"; \
exit 1; \
else \
echo "\x1b[32m✅ Using provided NEXTCLOUD_API_TOKEN\x1b[0m"; \
fi; \
TMPF="$$(mktemp)"; \
DOWNLOAD_URL="https://github.com/$(repo_path)/releases/download/v$${VERSION}/$(app_name)-v$${VERSION}.tar.gz"; \
KEY_FILE=~/.nextcloud/certificates/$(app_name).key; \
if [ ! -f "$$KEY_FILE" ]; then \
echo "\x1b[31m❌ Error: Private key not found at $$KEY_FILE\x1b[0m"; \
exit 1; \
fi; \
echo "\x1b[33mDownloading archive for version $${VERSION}...\x1b[0m"; \
curl -L "$${DOWNLOAD_URL}" -o "$${TMPF}"; \
FILESIZE=$$(stat -f%z "$${TMPF}" 2>/dev/null || stat -c%s "$${TMPF}"); \
if [ "$${FILESIZE}" -lt 10240 ]; then \
echo "\x1b[31mError: Downloaded file is too small (<10KB, actual: $${FILESIZE} bytes)\x1b[0m"; \
rm -f "$${TMPF}"; \
exit 1; \
fi; \
echo "\x1b[33mSigning with key $$KEY_FILE\x1b[0m"; \
echo; \
SIGNATURE="$$(openssl dgst -sha512 -sign "$$KEY_FILE" "$${TMPF}" | openssl base64 | tr -d '\n')"; \
rm -f "$${TMPF}"; \
echo "\x1b[32mReleasing to Nextcloud App Store...\x1b[0m"; \
RESPONSE="$$(mktemp)"; \
HTTP_CODE=$$(curl -s -w "%{http_code}" -o "$${RESPONSE}" -X POST \
-H "Authorization: Token $$NEXTCLOUD_API_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"download\":\"$${DOWNLOAD_URL}\", \"signature\":\"$${SIGNATURE}\"}" \
https://apps.nextcloud.com/api/v1/apps/releases); \
cat "$$RESPONSE"; echo; \
if [ "$$HTTP_CODE" = "400" ]; then \
echo "\x1b[31m❌ Error 400: Invalid data, app too large, signature/cert issue, or not registered\x1b[0m"; exit 1; \
elif [ "$$HTTP_CODE" = "401" ]; then \
echo "\x1b[31m❌ Error 401: Not authenticated\x1b[0m"; exit 1; \
elif [ "$$HTTP_CODE" = "403" ]; then \
echo "\x1b[31m❌ Error 403: Not authorized\x1b[0m"; exit 1; \
elif [ "$$HTTP_CODE" -ge 300 ]; then \
echo "\x1b[31m❌ Unexpected error (HTTP $$HTTP_CODE)\x1b[0m"; exit 1; \
fi; \
rm -f "$$RESPONSE"; \
echo "\x1b[32m🎉 Release successful!\x1b[0m";