mirror of
https://github.com/alexhopeoconnor/DFTE.git
synced 2026-10-10 01:41:01 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbbb29484a | ||
|
|
a09f9e331a | ||
|
|
ff33de6293 |
@@ -18,6 +18,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: bash -n scripts/*.sh
|
||||
- run: ./scripts/check-docs.sh
|
||||
compile-tests:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -32,3 +33,4 @@ jobs:
|
||||
python-version: '3.11'
|
||||
- run: python -m pip install --upgrade platformio==6.1.19
|
||||
- run: ./scripts/test.sh compile --platform ${{ matrix.platform }}
|
||||
- run: ./scripts/test.sh examples --platform ${{ matrix.platform }}
|
||||
|
||||
@@ -18,7 +18,9 @@ jobs:
|
||||
python-version: '3.11'
|
||||
- run: python -m pip install --upgrade platformio==6.1.19
|
||||
- run: ./scripts/test.sh compile --platform esp8266
|
||||
- run: ./scripts/test.sh examples --platform esp8266
|
||||
- run: ./scripts/test.sh compile --platform esp32
|
||||
- run: ./scripts/test.sh examples --platform esp32
|
||||
- run: ./scripts/check-docs.sh
|
||||
- run: ./scripts/prepare-release.sh "$GITHUB_REF_NAME"
|
||||
- run: ./scripts/release-notes.sh "$GITHUB_REF_NAME" > "$RUNNER_TEMP/release-notes.md"
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## 1.2.0
|
||||
|
||||
- Add a borrowed async-response helper for fixed, caller-owned response slots. It avoids a per-request `shared_ptr` control allocation while releasing the slot safely on completion or disconnect.
|
||||
- Add callback-enabled safe shared response helpers so dynamic contexts can return a bounded owner permit at the same time as their shared state is released.
|
||||
|
||||
## 1.1.0
|
||||
|
||||
- Replace layout-affecting compile-time storage overrides with per-context,
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
# Device Framework Template Engine
|
||||
|
||||
DFTE streams HTML and text for ESP8266 and ESP32 without constructing a giant response in RAM. It combines PROGMEM templates, live getters, nested layouts, conditions, and iterators with request-safe asynchronous HTTP rendering.
|
||||
DFTE streams HTML and text from ESP8266 and ESP32 firmware without constructing a complete response in RAM. Store layouts in PROGMEM, resolve changing values through getters, and render fixed-size chunks directly to serial or an asynchronous HTTP response.
|
||||
|
||||
## Why use it
|
||||
|
||||
- **Predictable memory:** render fixed-size chunks instead of one large `String`.
|
||||
- **Reusable templates:** keep layouts, partials, CSS, and HTML in flash.
|
||||
- **Live device data:** resolve values from getters only when a chunk is rendered.
|
||||
- **Safe async responses:** use one render context per HTTP request while sharing one prepared registry.
|
||||
|
||||
## Try it
|
||||
## Render a first template
|
||||
|
||||
```cpp
|
||||
#include <TemplateEngine.h>
|
||||
|
||||
static const char PAGE[] PROGMEM = "<h1>%TITLE%</h1>";
|
||||
Serial.begin(115200);
|
||||
PlaceholderRegistry registry;
|
||||
TemplateContext context;
|
||||
|
||||
void setup() {
|
||||
Serial.begin(115200);
|
||||
registry.registerProgmemData(PSTR("%TITLE%"), PSTR("Hello DFTE"));
|
||||
registry.registerProgmemTemplate(PSTR("%PAGE%"), PAGE);
|
||||
context.setRegistry(®istry);
|
||||
@@ -34,37 +27,31 @@ void loop() {
|
||||
}
|
||||
```
|
||||
|
||||
## Featured examples
|
||||
Build [Hello Placeholder](examples/HelloPlaceholder/) to see the rendered result over serial.
|
||||
|
||||
| Goal | Example |
|
||||
## Choose an example
|
||||
|
||||
| Example | What you will build |
|
||||
| --- | --- |
|
||||
| Smallest placeholder render | [HelloPlaceholder](examples/HelloPlaceholder/) |
|
||||
| Layouts, partials, and conditions | [NestedLayouts](examples/NestedLayouts/) |
|
||||
| Async HTTP streaming | [StreamingAsync](examples/StreamingAsync/) |
|
||||
| Dashboard, iterators, and telemetry | [AsyncDashboardDemo](examples/AsyncDashboardDemo/) |
|
||||
| [Hello Placeholder](examples/HelloPlaceholder/) | the smallest registry, context, and serial-rendering flow |
|
||||
| [Nested Layouts](examples/NestedLayouts/) | reusable partials, conditions, and iterator sections |
|
||||
| [Streaming Async](examples/StreamingAsync/) | a streamed ESPAsyncWebServer response over a SoftAP |
|
||||
| [Async Dashboard Demo](examples/AsyncDashboardDemo/) | a live dashboard with iterator rows and captive-portal access |
|
||||
|
||||
## Why DFTE
|
||||
|
||||
- **Bounded response memory:** render fixed-size chunks instead of allocating one large `String`.
|
||||
- **Flash-resident layouts:** keep templates, CSS, and shared fragments in PROGMEM.
|
||||
- **Live values:** resolve dynamic information only as the active response needs it.
|
||||
- **Async-safe rendering:** each HTTP request owns its rendering context while sharing the prepared registry.
|
||||
|
||||
## Install
|
||||
|
||||
```ini
|
||||
lib_deps =
|
||||
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.1.0
|
||||
DeviceFrameworkTemplateEngine=https://github.com/alexhopeoconnor/DFTE.git#v1.2.0
|
||||
```
|
||||
|
||||
PlatformIO checks out the Git ref after `#`; GitHub Release assets are unrelated. DFTE’s supported release targets are ESP8266 and ESP32.
|
||||
PlatformIO checks out the Git ref after `#`; GitHub Release assets are unrelated. DFTE supports ESP8266 and ESP32 Arduino projects.
|
||||
|
||||
## Documentation
|
||||
|
||||
Read the [documentation index](docs/README.md) for template syntax, async web responses, per-context memory sizing, examples, tests, and releases.
|
||||
|
||||
## Development and releases
|
||||
|
||||
```bash
|
||||
./scripts/test.sh compile --platform esp8266
|
||||
./scripts/test.sh compile --platform esp32
|
||||
./scripts/check-docs.sh
|
||||
./scripts/prepare-release.sh vMAJOR.MINOR.PATCH --tag
|
||||
```
|
||||
|
||||
Tagging repeats the board-free compile checks, validates the package, and creates a GitHub Release from the matching changelog section. It does not publish to the PlatformIO Registry or deploy firmware.
|
||||
|
||||
See the [changelog](CHANGELOG.md) and [licence](LICENSE).
|
||||
See [getting started](docs/GETTING_STARTED.md), the [documentation index](docs/README.md), [examples](examples/README.md), [changelog](CHANGELOG.md), and [licence](LICENSE).
|
||||
|
||||
@@ -24,6 +24,54 @@ void sendTemplate(AsyncWebServerRequest* request, const char* root) {
|
||||
|
||||
The response retains the context while it streams. Releasing the request-owned `shared_ptr` on disconnect prevents state from leaking into later requests.
|
||||
|
||||
## Bounded fixed slots
|
||||
|
||||
Use `beginBorrowedChunkedResponse()` when the application already owns a small, fixed response pool. It does not allocate a `shared_ptr` control block for the request. The release callback must be idempotent because it can run once when rendering finishes and again if the connection later disconnects.
|
||||
|
||||
```cpp
|
||||
struct ResponseSlot {
|
||||
bool busy = false;
|
||||
TemplateContext context;
|
||||
};
|
||||
|
||||
ResponseSlot slots[2];
|
||||
|
||||
void releaseSlot(ResponseSlot& slot) {
|
||||
slot.context.reset();
|
||||
slot.busy = false;
|
||||
}
|
||||
|
||||
void sendBoundedTemplate(AsyncWebServerRequest* request, const char* root) {
|
||||
ResponseSlot* slot = nullptr;
|
||||
for (auto& candidate : slots) {
|
||||
if (!candidate.busy) {
|
||||
slot = &candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (slot == nullptr) {
|
||||
request->send(503, "text/plain", "Busy");
|
||||
return;
|
||||
}
|
||||
|
||||
slot->busy = true;
|
||||
slot->context.setRegistry(registry.get());
|
||||
TemplateRenderer::initializeContext(slot->context, root);
|
||||
request->send(TemplateEngineAsyncWeb::beginBorrowedChunkedResponse(
|
||||
request, "text/html; charset=utf-8", slot,
|
||||
[](ResponseSlot& state, uint8_t* out, size_t size, size_t) {
|
||||
return TemplateEngineAsyncWeb::renderTemplateChunkWithRetries(
|
||||
state.context, out, size, 128);
|
||||
},
|
||||
[](const ResponseSlot& state) {
|
||||
return TemplateEngineAsyncWeb::isTemplateTerminal(state.context);
|
||||
},
|
||||
releaseSlot));
|
||||
}
|
||||
```
|
||||
|
||||
Use this form only while the slot itself has static or otherwise guaranteed lifetime. Use the `shared_ptr` form above for a request-owned dynamic context.
|
||||
|
||||
Use [StreamingAsync](../examples/StreamingAsync/) for a complete SoftAP/captive-portal project.
|
||||
|
||||
Back to [documentation](README.md) · [project overview](../README.md).
|
||||
|
||||
+3
-1
@@ -2,9 +2,11 @@
|
||||
|
||||
Released applications should use a public Git tag. While changing DFTE with a sibling library, select a local `symlink://` or `file://` dependency from an ignored PlatformIO override rather than changing tracked application dependencies.
|
||||
|
||||
Before a release, update `library.json`, `CHANGELOG.md`, and the relevant guides, then run:
|
||||
Start a release with `bump-version.sh`. It updates package metadata and canonical installation snippets, then creates the changelog section. Replace its generated TODO with the release summary and update any behavioural documentation before running:
|
||||
|
||||
```bash
|
||||
./scripts/bump-version.sh vMAJOR.MINOR.PATCH
|
||||
# Replace the generated CHANGELOG TODO with the release summary.
|
||||
./scripts/check-docs.sh
|
||||
./scripts/test.sh compile --platform esp8266
|
||||
./scripts/test.sh compile --platform esp32
|
||||
|
||||
+1
-9
@@ -1,20 +1,12 @@
|
||||
# DFTE documentation
|
||||
|
||||
DFTE documentation is organised by the rendering problem being solved.
|
||||
|
||||
| I want to… | Read |
|
||||
| --- | --- |
|
||||
| Render a first template | [Getting started](GETTING_STARTED.md) |
|
||||
| Use placeholders, templates, conditions, dynamic fragments, or iterators | [Template language](TEMPLATE_LANGUAGE.md) |
|
||||
| Stream a response through ESPAsyncWebServer | [Async web responses](ASYNC_WEB.md) |
|
||||
| Change buffer, stack, or placeholder limits safely | [Configuration](CONFIGURATION.md) |
|
||||
| Build the standalone examples | [Examples](../examples/README.md) |
|
||||
| Build, flash, and observe a complete example | [Examples](../examples/README.md) |
|
||||
| Run checks or prepare a release | [Testing](TESTING.md) · [Development](DEVELOPMENT.md) |
|
||||
|
||||
## Documentation rules
|
||||
|
||||
- The template-language guide owns DFTE semantics and lifecycle contracts.
|
||||
- The configuration guide owns ABI-sensitive build flags.
|
||||
- The project README is an entry point and example chooser, not a full API manual.
|
||||
|
||||
Back to the [project overview](../README.md).
|
||||
|
||||
+9
-9
@@ -1,22 +1,22 @@
|
||||
# DFTE examples
|
||||
|
||||
Every example is a standalone PlatformIO project. Build, upload, and monitor from its directory:
|
||||
Every example is a standalone PlatformIO project. Build, upload, and monitor from the repository root:
|
||||
|
||||
```bash
|
||||
pio run -d examples/HelloPlaceholder -e example_esp8266
|
||||
pio run -d examples/HelloPlaceholder -e example_esp8266 -t upload
|
||||
pio run -d examples/HelloPlaceholder -e example_esp8266 -t monitor
|
||||
pio device monitor -d examples/HelloPlaceholder -e example_esp8266
|
||||
```
|
||||
|
||||
Choose the ESP32 environment where provided.
|
||||
Choose the corresponding ESP32 environment where provided. The examples use the checked-out DFTE source, so they double as practical integration checks for this repository.
|
||||
|
||||
| Example | What it demonstrates |
|
||||
| Example | Start here when you want to… |
|
||||
| --- | --- |
|
||||
| [HelloPlaceholder](HelloPlaceholder/) | Smallest registry/context/chunk flow over serial |
|
||||
| [NestedLayouts](NestedLayouts/) | Templates, partials, conditionals, and iterators without a web server |
|
||||
| [StreamingAsync](StreamingAsync/) | Request-scoped streaming HTTP response through a SoftAP portal |
|
||||
| [AsyncDashboardDemo](AsyncDashboardDemo/) | Dashboard telemetry, iterator rows, and captive-portal flow |
|
||||
| [HelloPlaceholder](HelloPlaceholder/) | understand the smallest registry/context/chunk flow over serial |
|
||||
| [NestedLayouts](NestedLayouts/) | compose a page with partials, conditions, and repeating data |
|
||||
| [StreamingAsync](StreamingAsync/) | serve one streamed page through an asynchronous web server |
|
||||
| [AsyncDashboardDemo](AsyncDashboardDemo/) | inspect a richer browser-facing dashboard without buffering the response |
|
||||
|
||||
The SoftAP examples print their network names and passwords to serial. They are demonstrations, not production provisioning implementations.
|
||||
The SoftAP examples print their network names and passwords to serial. They are self-contained browser demonstrations, not Wi-Fi provisioning implementations.
|
||||
|
||||
Back to [DFTE documentation](../docs/README.md) · [project overview](../README.md).
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
# Streaming Async
|
||||
|
||||
Small ESPAsyncWebServer demo that exposes a captive portal over a SoftAP and renders a DFTE template directly to the HTTP response stream. It highlights:
|
||||
This ESPAsyncWebServer example exposes a captive portal over a SoftAP and streams a DFTE template directly to each HTTP response. It demonstrates:
|
||||
|
||||
- Per-request `TemplateContext` ownership to avoid concurrent request clashes
|
||||
- PROGMEM template with shared CSS/header/footer snippets
|
||||
- Runtime data from RAM getters (uptime, connected station count)
|
||||
- Captive portal DNS redirect so clients automatically receive the dashboard
|
||||
- one request-scoped `TemplateContext` per response;
|
||||
- a PROGMEM page with shared CSS/header/footer snippets;
|
||||
- runtime getters for uptime and connected-station count;
|
||||
- a captive-portal DNS redirect for browsers that support it.
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
```bash
|
||||
pio run -d examples/StreamingAsync -e example_esp8266
|
||||
|
||||
See the shared [examples guide](../README.md) and [async web guidance](../../docs/ASYNC_WEB.md).
|
||||
pio run -d examples/StreamingAsync -e example_esp32
|
||||
```
|
||||
|
||||
Flash the board, connect to the SoftAP announced by the device (`DFTE-Portal-8266` or `DFTE-Portal-ESP32`, password `dfte-demo`), and your browser should automatically present the streamed dashboard. If it does not, navigate manually to `http://192.168.4.1/`.
|
||||
## Run
|
||||
|
||||
1. Flash your target with `pio run -d examples/StreamingAsync -e <env> -t upload --upload-port <port>`.
|
||||
2. Open the serial monitor at 115200 baud and confirm the SoftAP credentials.
|
||||
3. Connect to `DFTE-Portal-8266` or `DFTE-Portal-ESP32` using password `dfte-demo`.
|
||||
4. Browse to `http://192.168.4.1/` if the captive portal does not open automatically.
|
||||
|
||||
See the shared [examples guide](../README.md) and [async web guidance](../../docs/ASYNC_WEB.md).
|
||||
|
||||
@@ -85,6 +85,103 @@ AsyncWebServerResponse* beginSafeChunkedResponse(AsyncWebServerRequest* request,
|
||||
});
|
||||
}
|
||||
|
||||
// A bounded embedded server can own response state in a fixed slot instead of
|
||||
// allocating a shared control block for every request. The caller guarantees
|
||||
// that state remains valid until release() is called. release() must be safe
|
||||
// to call more than once because a completed request can later disconnect.
|
||||
template <typename StateT, typename FillFn, typename IsDoneFn, typename ReleaseFn>
|
||||
AsyncWebServerResponse* beginBorrowedChunkedResponse(AsyncWebServerRequest* request,
|
||||
const char* contentType,
|
||||
StateT* state,
|
||||
FillFn fill,
|
||||
IsDoneFn isDone,
|
||||
ReleaseFn release) {
|
||||
request->onDisconnect([state, release]() mutable {
|
||||
if (state != nullptr) {
|
||||
release(*state);
|
||||
}
|
||||
});
|
||||
|
||||
return request->beginChunkedResponse(contentType,
|
||||
[state, fill, isDone, release](uint8_t* buffer, size_t maxLen, size_t index) mutable -> size_t {
|
||||
if (state == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (isDone(*state)) {
|
||||
release(*state);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (maxLen == 0) {
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
|
||||
size_t written = fill(*state, buffer, maxLen, index);
|
||||
if (written == RESPONSE_TRY_AGAIN) {
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
|
||||
if (written > 0) {
|
||||
return written;
|
||||
}
|
||||
|
||||
if (isDone(*state)) {
|
||||
release(*state);
|
||||
return 0;
|
||||
}
|
||||
|
||||
yieldForChunkRetry();
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
});
|
||||
}
|
||||
|
||||
// Variant for dynamically allocated state when the owner also needs an
|
||||
// idempotent completion notification (for example, returning a bounded web
|
||||
// response permit). The original overload intentionally remains available for
|
||||
// callers that only need shared ownership.
|
||||
template <typename StateT, typename FillFn, typename IsDoneFn, typename ReleaseFn>
|
||||
AsyncWebServerResponse* beginSafeChunkedResponse(AsyncWebServerRequest* request,
|
||||
const char* contentType,
|
||||
const std::shared_ptr<StateT>& sharedState,
|
||||
FillFn fill,
|
||||
IsDoneFn isDone,
|
||||
ReleaseFn release) {
|
||||
request->onDisconnect([state = sharedState, release]() mutable {
|
||||
release();
|
||||
state.reset();
|
||||
});
|
||||
|
||||
return request->beginChunkedResponse(contentType,
|
||||
[state = sharedState, fill, isDone, release](uint8_t* buffer, size_t maxLen, size_t index) mutable -> size_t {
|
||||
if (!state) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (maxLen == 0) {
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
|
||||
size_t written = fill(*state, buffer, maxLen, index);
|
||||
if (written == RESPONSE_TRY_AGAIN) {
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
}
|
||||
|
||||
if (written > 0) {
|
||||
return written;
|
||||
}
|
||||
|
||||
if (isDone(*state)) {
|
||||
release();
|
||||
state.reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
yieldForChunkRetry();
|
||||
return RESPONSE_TRY_AGAIN;
|
||||
});
|
||||
}
|
||||
|
||||
template <typename StateT, typename FillFn, typename IsDoneFn>
|
||||
AsyncWebServerResponse* beginSafeChunkedResponse(AsyncWebServerRequest* request,
|
||||
const String& contentType,
|
||||
@@ -128,6 +225,25 @@ AsyncWebServerResponse* beginSafeChunkedResponse(AsyncWebServerRequest* request,
|
||||
});
|
||||
}
|
||||
|
||||
template <typename ContextT, typename ReleaseFn>
|
||||
AsyncWebServerResponse* beginSafeTemplateResponse(AsyncWebServerRequest* request,
|
||||
const char* contentType,
|
||||
const std::shared_ptr<ContextT>& sharedContext,
|
||||
unsigned maxNoProgressRetries,
|
||||
ReleaseFn release) {
|
||||
return beginSafeChunkedResponse(
|
||||
request,
|
||||
contentType,
|
||||
sharedContext,
|
||||
[maxNoProgressRetries](ContextT& context, uint8_t* buffer, size_t maxLen, size_t /*index*/) -> size_t {
|
||||
return renderTemplateChunkWithRetries(context, buffer, maxLen, maxNoProgressRetries);
|
||||
},
|
||||
[](const ContextT& context) -> bool {
|
||||
return isTemplateTerminal(context);
|
||||
},
|
||||
release);
|
||||
}
|
||||
|
||||
template <typename ContextT>
|
||||
AsyncWebServerResponse* beginSafeTemplateResponse(AsyncWebServerRequest* request,
|
||||
const char* contentType,
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "DeviceFrameworkTemplateEngine",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"description": "Memory-efficient streaming template engine for ESP8266/ESP32 with chunked rendering support. Designed for embedded web interfaces with PROGMEM template support.",
|
||||
"keywords": [
|
||||
"template",
|
||||
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 vMAJOR.MINOR.PATCH" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
tag="${1:-}"
|
||||
[[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || usage
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
version="${tag#v}"
|
||||
repo_url="https://github.com/alexhopeoconnor/DFTE.git"
|
||||
reference_files=(README.md)
|
||||
|
||||
current_version="$(sed -n 's/.*"version": "\([^"]*\)".*/\1/p' "$root/library.json" | head -n 1)"
|
||||
[[ "$current_version" != "$version" ]] || {
|
||||
echo "library.json already declares $version; choose a new version." >&2
|
||||
exit 1
|
||||
}
|
||||
grep -q "^## $version$" "$root/CHANGELOG.md" && {
|
||||
echo "CHANGELOG.md already has a $version section; choose a new version." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
sed -i -E '0,/"version": "[0-9]+\.[0-9]+\.[0-9]+"/s//"version": "'"$version"'"/' "$root/library.json"
|
||||
for file in "${reference_files[@]}"; do
|
||||
sed -i -E "s|${repo_url}#v[0-9]+\.[0-9]+\.[0-9]+|${repo_url}#v${version}|g" "$root/$file"
|
||||
done
|
||||
|
||||
temp_file="$(mktemp)"
|
||||
trap 'rm -f "$temp_file"' EXIT
|
||||
{
|
||||
IFS= read -r changelog_heading < "$root/CHANGELOG.md"
|
||||
[[ "$changelog_heading" == "# Changelog" ]] || {
|
||||
echo "CHANGELOG.md must begin with # Changelog" >&2
|
||||
exit 1
|
||||
}
|
||||
printf '%s\n\n## %s\n\n- TODO: Describe this release.\n' "$changelog_heading" "$version"
|
||||
tail -n +2 "$root/CHANGELOG.md"
|
||||
} > "$temp_file"
|
||||
mv "$temp_file" "$root/CHANGELOG.md"
|
||||
|
||||
echo "Updated DFTE declarations and canonical install references to $tag."
|
||||
echo "Replace the generated changelog TODO with the release summary, then run scripts/check-docs.sh and scripts/prepare-release.sh $tag."
|
||||
@@ -25,4 +25,21 @@ while IFS= read -r -d '' markdown; do
|
||||
done < <(sed -nE 's/.*\]\(([^ )]+)( "[^"]*")?\).*/\1/p' "$markdown")
|
||||
done < <(find "$root" -path "$root/.git" -prune -o -path '*/.pio' -prune -o -name '*.md' -type f -print0)
|
||||
|
||||
while IFS= read -r example; do
|
||||
for required in README.md platformio.ini; do
|
||||
[[ -f "$example/$required" ]] || { echo "Incomplete example: ${example#$root/} is missing $required" >&2; exit 1; }
|
||||
done
|
||||
find "$example/src" -type f \( -name '*.ino' -o -name '*.cpp' \) -print -quit | grep -q . || {
|
||||
echo "Incomplete example: ${example#$root/} has no source" >&2
|
||||
exit 1
|
||||
}
|
||||
done < <(find "$root/examples" -mindepth 2 -maxdepth 2 -type f -name platformio.ini -printf '%h\n' | sort)
|
||||
|
||||
while IFS= read -r markdown; do
|
||||
(( $(grep -Ec '^[[:space:]]*```' "$markdown") % 2 == 0 )) || {
|
||||
echo "Unclosed code fence in ${markdown#$root/}" >&2
|
||||
exit 1
|
||||
}
|
||||
done < <(find "$root" -path "$root/.git" -prune -o -path '*/.pio' -prune -o -name '*.md' -type f -print)
|
||||
|
||||
echo "Documentation links and required files passed"
|
||||
|
||||
@@ -32,12 +32,30 @@ if ! grep -q "^## ${version}$" "$root/CHANGELOG.md"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if awk -v heading="## $version" '
|
||||
$0 == heading { found = 1; next }
|
||||
found && /^## / { exit }
|
||||
found { print }
|
||||
' "$root/CHANGELOG.md" | grep -Fq 'TODO: Describe this release.'; then
|
||||
echo "CHANGELOG.md still has the generated TODO for $version" >&2
|
||||
exit 1
|
||||
fi
|
||||
repo_url="https://github.com/alexhopeoconnor/DFTE.git"
|
||||
validate_reference() {
|
||||
local file="$1"
|
||||
local reference_count
|
||||
reference_count="$(grep -F "$repo_url#v" "$root/$file" | wc -l)"
|
||||
[[ "$reference_count" -eq 1 ]] || { echo "$file must contain exactly one canonical release reference" >&2; exit 1; }
|
||||
grep -Fq "$repo_url#$tag" "$root/$file" || { echo "$file does not reference $tag" >&2; exit 1; }
|
||||
}
|
||||
validate_reference README.md
|
||||
|
||||
git -C "$root" diff --check
|
||||
|
||||
package_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$package_dir"' EXIT
|
||||
pio pkg pack "$root" --output "$package_dir/package.tar.gz" >/dev/null
|
||||
echo "Validated PlatformIO package for $tag"
|
||||
echo "Validated release metadata and PlatformIO package for $tag"
|
||||
|
||||
if [[ "${2:-}" == "--tag" ]]; then
|
||||
git -C "$root" diff --quiet
|
||||
|
||||
+25
-7
@@ -2,18 +2,36 @@
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 compile --platform esp8266|esp32" >&2
|
||||
echo "Usage: $0 compile|examples --platform esp8266|esp32" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
[[ "${1:-}" == "compile" && "${2:-}" == "--platform" && $# -eq 3 ]] || usage
|
||||
|
||||
[[ $# -eq 3 && ( "${1:-}" == "compile" || "${1:-}" == "examples" ) && "${2:-}" == "--platform" ]] || usage
|
||||
case "${3:-}" in
|
||||
esp8266) environment="test_template_engine_8266" ;;
|
||||
esp32) environment="test_template_engine_esp32" ;;
|
||||
esp8266|esp32) platform="$3" ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
pio test -d "$root" -e "$environment" --without-uploading --without-testing
|
||||
echo "DFTE compile check passed for ${3}"
|
||||
if [[ "$1" == "compile" ]]; then
|
||||
case "$platform" in
|
||||
esp8266) test_environment="test_template_engine_8266" ;;
|
||||
esp32) test_environment="test_template_engine_esp32" ;;
|
||||
esac
|
||||
pio test -d "$root" -e "$test_environment" --without-uploading --without-testing
|
||||
echo "DFTE compile check passed for $platform"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
suffix="$platform"
|
||||
mapfile -t examples < <(find "$root/examples" -mindepth 2 -maxdepth 2 -type f -name platformio.ini -printf '%h\n' | sort)
|
||||
if (( ${#examples[@]} == 0 )); then
|
||||
echo "No example projects found" >&2
|
||||
exit 1
|
||||
fi
|
||||
for example in "${examples[@]}"; do
|
||||
env_name="example_$suffix"
|
||||
[[ "$(basename "$example")" == "AsyncDashboardDemo" ]] && env_name="dashboard_$suffix"
|
||||
pio run -d "$example" -e "$env_name" </dev/null
|
||||
done
|
||||
echo "DFTE examples compile check passed for $platform"
|
||||
|
||||
Reference in New Issue
Block a user