@typecad/cuttlefish 0.1.0-alpha.2 → 1.0.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -10
- package/dist/api/board-types.d.ts +1 -1
- package/dist/api/config.d.ts +38 -2
- package/dist/api/index.d.ts +1 -1
- package/dist/api/schema/types.d.ts +15 -0
- package/dist/api/shared/async-runtime-static.d.ts +12 -1
- package/dist/api/shared/async-runtime-static.js +69 -24
- package/dist/api/shared/async-symbol-detector.d.ts +11 -0
- package/dist/api/shared/async-symbol-detector.js +140 -0
- package/dist/api/shared/async-types.d.ts +24 -0
- package/dist/api/shared/coop-scheduler.d.ts +60 -0
- package/dist/api/shared/coop-scheduler.js +149 -0
- package/dist/api/shared/display-adapter.d.ts +4 -3
- package/dist/api/shared/display-adapter.js +15 -118
- package/dist/api/shared/display-adapters/sdl.js +38 -31
- package/dist/api/shared/display-profile.d.ts +47 -25
- package/dist/api/shared/display-profile.js +31 -164
- package/dist/api/shared/framework-manifest-registry.d.ts +9 -0
- package/dist/api/shared/framework-manifest-registry.js +27 -0
- package/dist/api/shared/framework-manifest.d.ts +595 -0
- package/dist/api/shared/framework-manifest.js +193 -0
- package/dist/api/shared/glcdfont.d.ts +12 -0
- package/dist/api/shared/glcdfont.js +124 -0
- package/dist/api/shared/graphics-strategy.d.ts +37 -0
- package/dist/api/shared/hal-op-ir.d.ts +764 -1
- package/dist/api/shared/hal-op-ir.js +129 -1
- package/dist/api/shared/index.d.ts +22 -2
- package/dist/api/shared/index.js +26 -1
- package/dist/api/shared/ir-core.d.ts +4 -0
- package/dist/api/shared/ir-declarations.d.ts +6 -0
- package/dist/api/shared/native-display-op-resolver.d.ts +10 -0
- package/dist/api/shared/native-display-op-resolver.js +64 -0
- package/dist/api/shared/platform-strategy.d.ts +119 -2
- package/dist/api/shared/polyfill-helper-registry.js +18 -1
- package/dist/api/shared/promise-runtime.d.ts +1 -1
- package/dist/api/shared/promise-runtime.js +95 -13
- package/dist/api/shared/spdx-licenses.d.ts +139 -0
- package/dist/api/shared/spdx-licenses.js +385 -0
- package/dist/api/shared/toolchain-types.d.ts +17 -0
- package/dist/api/shared/types.d.ts +8 -0
- package/dist/api/shared/validate-framework-manifest.d.ts +28 -0
- package/dist/api/shared/validate-framework-manifest.js +494 -0
- package/dist/api/shared/worker-runtime-polyfill.d.ts +22 -0
- package/dist/api/shared/worker-runtime-polyfill.js +34 -0
- package/dist/api/shared/worker-runtime.d.ts +69 -0
- package/dist/api/shared/worker-runtime.js +163 -0
- package/dist/cli-utils.d.ts +1 -0
- package/dist/cli-utils.js +3 -1
- package/dist/cli.js +249 -32
- package/dist/config-loader.d.ts +7 -2
- package/dist/config-loader.js +23 -3
- package/dist/config-schema.d.ts +112 -70
- package/dist/config-schema.js +14 -0
- package/dist/contract/board-generator.d.ts +17 -0
- package/dist/contract/board-generator.js +57 -0
- package/dist/contract/contract-parser.d.ts +217 -0
- package/dist/contract/contract-parser.js +224 -0
- package/dist/contract/index.d.ts +22 -0
- package/dist/contract/index.js +68 -0
- package/dist/create/board-codegen.js +4 -4
- package/dist/create/board-generators.js +4 -5
- package/dist/create/board-spec.d.ts +72 -75
- package/dist/create/board-spec.js +0 -1
- package/dist/create/framework-catalog.d.ts +64 -0
- package/dist/create/framework-catalog.js +136 -0
- package/dist/create/index.d.ts +5 -1
- package/dist/create/index.js +3 -1
- package/dist/create/init-scaffold.d.ts +12 -3
- package/dist/create/init-scaffold.js +49 -14
- package/dist/create/init-templates.d.ts +4 -0
- package/dist/create/init-templates.js +271 -17
- package/dist/create/init-wizard.js +41 -27
- package/dist/create/install-deps.d.ts +32 -0
- package/dist/create/install-deps.js +46 -0
- package/dist/debug/preprocessor.js +178 -39
- package/dist/debug/types.d.ts +23 -0
- package/dist/diagnostics/mermaid-builder.d.ts +1 -1
- package/dist/diagnostics/mermaid-builder.js +34 -24
- package/dist/emit/compliance/arxml-writer.d.ts +11 -0
- package/dist/emit/compliance/arxml-writer.js +34 -0
- package/dist/emit/compliance/compliance-context.d.ts +57 -0
- package/dist/emit/compliance/compliance-context.js +113 -0
- package/dist/emit/compliance/deviation-ledger.d.ts +27 -0
- package/dist/emit/compliance/deviation-ledger.js +47 -0
- package/dist/emit/compliance/deviation-writer.d.ts +30 -0
- package/dist/emit/compliance/deviation-writer.js +37 -0
- package/dist/emit/compliance/index.d.ts +7 -0
- package/dist/emit/compliance/index.js +6 -0
- package/dist/emit/compliance/rule-engine.d.ts +13 -0
- package/dist/emit/compliance/rule-engine.js +101 -0
- package/dist/emit/compliance/rules.d.ts +16 -0
- package/dist/emit/compliance/rules.js +191 -0
- package/dist/emit/compliance/types.d.ts +59 -0
- package/dist/emit/compliance/types.js +8 -0
- package/dist/emit/cpp-emitter.js +4 -3
- package/dist/emit/emitters/class-emitter.js +6 -1
- package/dist/emit/emitters/emitter-context.d.ts +28 -1
- package/dist/emit/emitters/function-emitter-impl.js +153 -53
- package/dist/emit/emitters/line-appender.js +19 -0
- package/dist/emit/emitters/line-marker.d.ts +38 -0
- package/dist/emit/emitters/line-marker.js +39 -0
- package/dist/emit/emitters/output-finalizer.d.ts +6 -0
- package/dist/emit/emitters/output-finalizer.js +101 -12
- package/dist/emit/emitters/setup.d.ts +17 -0
- package/dist/emit/emitters/setup.js +372 -30
- package/dist/emit/emitters/top-level-prep.js +8 -0
- package/dist/emit/emitters/type-decl-emitter.js +28 -3
- package/dist/emit/emitters/ui-emitter.js +33 -9
- package/dist/emit/expression-renderer.d.ts +1 -1
- package/dist/emit/expression-renderer.js +91 -6
- package/dist/emit/route-hal-op.js +18 -5
- package/dist/emit/snprintf-helpers.js +15 -4
- package/dist/emit/statement-renderer.d.ts +10 -0
- package/dist/emit/statement-renderer.js +34 -5
- package/dist/emit/utils/async-state-machine.js +221 -125
- package/dist/emit/utils/hal-op-cpp-type.d.ts +6 -0
- package/dist/emit/utils/hal-op-cpp-type.js +40 -0
- package/dist/framework-package.js +2 -0
- package/dist/framework-registry.d.ts +17 -0
- package/dist/install/framework-catalog.d.ts +53 -0
- package/dist/install/framework-catalog.js +107 -0
- package/dist/install/handle-install.d.ts +35 -0
- package/dist/install/handle-install.js +177 -0
- package/dist/install/index.d.ts +4 -0
- package/dist/install/index.js +3 -0
- package/dist/ir/adc-range-validation.js +40 -26
- package/dist/ir/build-ir-state.d.ts +1 -0
- package/dist/ir/build-ir-state.js +14 -0
- package/dist/ir/build-ir.js +9 -5
- package/dist/ir/call-graph.js +16 -0
- package/dist/ir/expression-to-ir.js +207 -6
- package/dist/ir/feature-registry.js +7 -25
- package/dist/ir/function-builder.js +22 -0
- package/dist/ir/hal/hal-emitter.d.ts +5 -2
- package/dist/ir/hal/hal-emitter.js +50 -18
- package/dist/ir/hal/hal-parser.d.ts +6 -0
- package/dist/ir/hal/hal-parser.js +81 -0
- package/dist/ir/hal/hal-plugins.js +733 -1
- package/dist/ir/identifier-collector.js +35 -0
- package/dist/ir/interrupt-analysis.d.ts +5 -1
- package/dist/ir/interrupt-analysis.js +16 -19
- package/dist/ir/memory-budget-validation.js +1 -0
- package/dist/ir/network-validation.d.ts +4 -0
- package/dist/ir/network-validation.js +184 -0
- package/dist/ir/ownership-analysis.js +39 -1
- package/dist/ir/peripheral-ownership.js +5 -0
- package/dist/ir/peripheral-validation.d.ts +1 -1
- package/dist/ir/peripheral-validation.js +6 -3
- package/dist/ir/pin-alias-conflict.d.ts +1 -1
- package/dist/ir/pin-alias-conflict.js +2 -1
- package/dist/ir/pin-capability-validation.js +71 -32
- package/dist/ir/pin-mode-validation.d.ts +2 -2
- package/dist/ir/pin-mode-validation.js +54 -18
- package/dist/ir/pin-safety.d.ts +1 -1
- package/dist/ir/pin-safety.js +2 -1
- package/dist/ir/program-analysis.d.ts +81 -0
- package/dist/ir/program-analysis.js +429 -0
- package/dist/ir/pulldown-validation.d.ts +1 -1
- package/dist/ir/pulldown-validation.js +2 -1
- package/dist/ir/pwm-timer-sharing.d.ts +1 -1
- package/dist/ir/pwm-timer-sharing.js +2 -1
- package/dist/ir/render-expr.js +11 -0
- package/dist/ir/resource-analysis.js +2 -0
- package/dist/ir/timer0-pwm-timing-conflict.d.ts +1 -1
- package/dist/ir/timer0-pwm-timing-conflict.js +2 -1
- package/dist/ir/timing-validation.d.ts +6 -1
- package/dist/ir/timing-validation.js +61 -13
- package/dist/ir/transformers/call-statement.js +105 -0
- package/dist/ir/transformers/expressions.js +62 -0
- package/dist/ir/transformers/hal-call-resolver.js +21 -0
- package/dist/ir/transformers/hal-emit-helpers.js +1 -1
- package/dist/ir/transformers/namespace-methods.js +17 -12
- package/dist/ir/transformers/ui-reactive.js +2 -2
- package/dist/ir/transformers/variables.js +137 -19
- package/dist/ir/try-catch-validation.js +2 -0
- package/dist/ir/type-resolution.js +20 -2
- package/dist/ir/ui-element-auto-wire.js +7 -5
- package/dist/ir/unit-suspicion-validation.js +9 -7
- package/dist/ir/utils/map-statements.d.ts +4 -0
- package/dist/ir/utils/map-statements.js +79 -0
- package/dist/ir/validation-orchestrator.js +15 -8
- package/dist/ir/worker-analysis.d.ts +10 -0
- package/dist/ir/worker-analysis.js +261 -0
- package/dist/libdef/c-to-decl.d.ts +27 -0
- package/dist/libdef/c-to-decl.js +397 -0
- package/dist/libdef/component-decls.d.ts +2 -0
- package/dist/libdef/component-decls.js +6 -0
- package/dist/libdef/component-discovery.d.ts +43 -0
- package/dist/libdef/component-discovery.js +83 -0
- package/dist/libdef/cpp-to-decl.d.ts +9 -0
- package/dist/libdef/cpp-to-decl.js +72 -0
- package/dist/libdef/registry.js +5 -2
- package/dist/lint-cache.d.ts +59 -0
- package/dist/lint-cache.js +257 -0
- package/dist/orchestrator/graph-builder.js +14 -9
- package/dist/platform/coop-scheduler-runtime.d.ts +19 -0
- package/dist/platform/coop-scheduler-runtime.js +41 -0
- package/dist/platform/generic-strategy.d.ts +15 -3
- package/dist/platform/generic-strategy.js +49 -4
- package/dist/safety/safety-bridge.d.ts +11 -0
- package/dist/safety/safety-bridge.js +48 -0
- package/dist/safety/sidecar-bridge.d.ts +5 -0
- package/dist/safety/sidecar-bridge.js +22 -0
- package/dist/safety-hook.d.ts +79 -0
- package/dist/safety-hook.js +35 -0
- package/dist/testing.d.ts +8 -2
- package/dist/testing.js +9 -2
- package/dist/transpile.d.ts +3 -0
- package/dist/transpile.js +191 -49
- package/dist/types.d.ts +22 -3
- package/dist/ui-hook.d.ts +17 -3
- package/dist/utils/cli.js +123 -2
- package/dist/utils/fs.d.ts +13 -0
- package/dist/utils/fs.js +50 -0
- package/dist/utils/ui.js +3 -1
- package/package.json +23 -3
- package/dist/api/shared/display-adapters/eink-mono.d.ts +0 -2
- package/dist/api/shared/display-adapters/eink-mono.js +0 -53
- package/dist/api/shared/display-adapters/ssd1309.d.ts +0 -2
- package/dist/api/shared/display-adapters/ssd1309.js +0 -136
- package/dist/api/shared/display-adapters/st7796.d.ts +0 -2
- package/dist/api/shared/display-adapters/st7796.js +0 -132
- package/dist/ir/heap-array-validation.d.ts +0 -24
- package/dist/ir/heap-array-validation.js +0 -29
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
* @param includeWaitForPinEdge Whether to include the `waitForPinEdge` stub
|
|
21
21
|
* (used by Arduino targets).
|
|
22
22
|
*/
|
|
23
|
-
export function generatePromiseRuntime(queueCapacity, includeWaitForPinEdge = false) {
|
|
23
|
+
export function generatePromiseRuntime(queueCapacity, includeWaitForPinEdge = false, strategy) {
|
|
24
|
+
// The current-time expression (millis() on Wiring-derived frameworks,
|
|
25
|
+
// std::chrono on generic). Falling back to millis() preserves the historical
|
|
26
|
+
// behavior when no strategy is supplied.
|
|
27
|
+
const now = strategy?.currentTimeMillis?.() ?? "millis()";
|
|
24
28
|
const waitForPinEdge = includeWaitForPinEdge ? `
|
|
25
29
|
// HAL-level wait for pin edge — polling-based implementation.
|
|
26
30
|
// Detects an actual transition (idle→target), not just the current level.
|
|
@@ -31,18 +35,18 @@ export function generatePromiseRuntime(queueCapacity, includeWaitForPinEdge = fa
|
|
|
31
35
|
return Promise<void>([pin, mode, timeout](std::function<void(const void*)> resolve, std::function<void(const std::string&)> reject) {
|
|
32
36
|
int targetState = (mode == RISING) ? HIGH : LOW;
|
|
33
37
|
int idleState = (mode == RISING) ? LOW : HIGH;
|
|
34
|
-
unsigned long start =
|
|
38
|
+
unsigned long start = ${now};
|
|
35
39
|
// Phase 2 poller: waits for the pin to reach the target state (the edge).
|
|
36
40
|
auto pollTarget = [pin, targetState, timeout, start, resolve]() {
|
|
37
|
-
if (digitalRead(pin) == targetState) {
|
|
41
|
+
if (${strategy?.readDigitalPin?.("pin") ?? "digitalRead(pin)"} == targetState) {
|
|
38
42
|
resolve(nullptr);
|
|
39
|
-
} else if (timeout >= 0 && (
|
|
43
|
+
} else if (timeout >= 0 && (${now} - start >= static_cast<unsigned long>(timeout))) {
|
|
40
44
|
resolve(nullptr);
|
|
41
45
|
} else {
|
|
42
46
|
enqueueMicrotask([pin, targetState, timeout, start, resolve]() {
|
|
43
|
-
if (digitalRead(pin) == targetState) {
|
|
47
|
+
if (${strategy?.readDigitalPin?.("pin") ?? "digitalRead(pin)"} == targetState) {
|
|
44
48
|
resolve(nullptr);
|
|
45
|
-
} else if (timeout >= 0 && (
|
|
49
|
+
} else if (timeout >= 0 && (${now} - start >= static_cast<unsigned long>(timeout))) {
|
|
46
50
|
resolve(nullptr);
|
|
47
51
|
} else {
|
|
48
52
|
enqueueMicrotask([pin, targetState, timeout, start, resolve]() {});
|
|
@@ -52,9 +56,9 @@ export function generatePromiseRuntime(queueCapacity, includeWaitForPinEdge = fa
|
|
|
52
56
|
};
|
|
53
57
|
// Phase 1: wait for idle state before watching for the edge.
|
|
54
58
|
enqueueMicrotask([pin, idleState, timeout, start, resolve, pollTarget]() {
|
|
55
|
-
if (digitalRead(pin) == idleState) {
|
|
59
|
+
if (${strategy?.readDigitalPin?.("pin") ?? "digitalRead(pin)"} == idleState) {
|
|
56
60
|
pollTarget();
|
|
57
|
-
} else if (timeout >= 0 && (
|
|
61
|
+
} else if (timeout >= 0 && (${now} - start >= static_cast<unsigned long>(timeout))) {
|
|
58
62
|
resolve(nullptr);
|
|
59
63
|
} else {
|
|
60
64
|
enqueueMicrotask([pin, idleState, timeout, start, resolve, pollTarget]() {});
|
|
@@ -181,17 +185,95 @@ namespace typecad_async {
|
|
|
181
185
|
std::vector<std::function<void(const std::string&)>> _onRejected;
|
|
182
186
|
};
|
|
183
187
|
|
|
188
|
+
// Promise<void> — T=_value/const T& is illegal for void. Helpers below pass
|
|
189
|
+
// resolve as std::function<void(const void*)> (pointer, not reference).
|
|
190
|
+
template <>
|
|
191
|
+
class Promise<void> {
|
|
192
|
+
public:
|
|
193
|
+
enum class State { Pending, Fulfilled, Rejected };
|
|
194
|
+
|
|
195
|
+
using ResolveFn = std::function<void(const void*)>;
|
|
196
|
+
using RejectFn = std::function<void(const std::string&)>;
|
|
197
|
+
using Executor = std::function<void(ResolveFn, RejectFn)>;
|
|
198
|
+
|
|
199
|
+
Promise() : _state(State::Pending), _error{} {}
|
|
200
|
+
|
|
201
|
+
explicit Promise(Executor executor)
|
|
202
|
+
: _state(State::Pending), _error{} {
|
|
203
|
+
executor(
|
|
204
|
+
[this](const void*) { this->resolve(nullptr); },
|
|
205
|
+
[this](const std::string& error) { this->reject(error); }
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
static Promise<void> resolveValue() {
|
|
210
|
+
Promise<void> promise;
|
|
211
|
+
promise.resolve(nullptr);
|
|
212
|
+
return promise;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
static Promise<void> rejectValue(const std::string& error) {
|
|
216
|
+
Promise<void> promise;
|
|
217
|
+
promise.reject(error);
|
|
218
|
+
return promise;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
void resolve(const void* = nullptr) {
|
|
222
|
+
if (_state != State::Pending) return;
|
|
223
|
+
_state = State::Fulfilled;
|
|
224
|
+
auto callbacks = _onFulfilled;
|
|
225
|
+
enqueueMicrotask([callbacks]() mutable {
|
|
226
|
+
for (auto& callback : callbacks) { callback(nullptr); }
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
void reject(const std::string& error) {
|
|
231
|
+
if (_state != State::Pending) return;
|
|
232
|
+
_state = State::Rejected;
|
|
233
|
+
_error = error;
|
|
234
|
+
auto callbacks = _onRejected;
|
|
235
|
+
enqueueMicrotask([callbacks, error]() mutable {
|
|
236
|
+
for (auto& callback : callbacks) { callback(error); }
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
Promise<void>& then(std::function<void(const void*)> onFulfilled) {
|
|
241
|
+
if (_state == State::Fulfilled) {
|
|
242
|
+
enqueueMicrotask([onFulfilled]() mutable { onFulfilled(nullptr); });
|
|
243
|
+
} else if (_state == State::Pending) {
|
|
244
|
+
_onFulfilled.push_back(std::move(onFulfilled));
|
|
245
|
+
}
|
|
246
|
+
return *this;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
Promise<void>& catchError(std::function<void(const std::string&)> onRejected) {
|
|
250
|
+
if (_state == State::Rejected) {
|
|
251
|
+
const std::string error = _error;
|
|
252
|
+
enqueueMicrotask([onRejected, error]() mutable { onRejected(error); });
|
|
253
|
+
} else if (_state == State::Pending) {
|
|
254
|
+
_onRejected.push_back(std::move(onRejected));
|
|
255
|
+
}
|
|
256
|
+
return *this;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
private:
|
|
260
|
+
State _state;
|
|
261
|
+
std::string _error;
|
|
262
|
+
std::vector<std::function<void(const void*)>> _onFulfilled;
|
|
263
|
+
std::vector<std::function<void(const std::string&)>> _onRejected;
|
|
264
|
+
};
|
|
265
|
+
|
|
184
266
|
// ── HAL-level implementations (inside namespace so they see Promise<T> and enqueueMicrotask) ──
|
|
185
267
|
|
|
186
268
|
// Async.sleep() — cooperative delay using millis polling
|
|
187
269
|
inline Promise<void> __cuttlefish_async_sleep(unsigned long ms) {
|
|
188
270
|
return Promise<void>([ms](std::function<void(const void*)> resolve, std::function<void(const std::string&)> reject) {
|
|
189
|
-
unsigned long start =
|
|
190
|
-
if (
|
|
271
|
+
unsigned long start = ${now};
|
|
272
|
+
if (${now} - start >= ms) {
|
|
191
273
|
resolve(nullptr);
|
|
192
274
|
} else {
|
|
193
275
|
enqueueMicrotask([ms, start, resolve]() {
|
|
194
|
-
if (
|
|
276
|
+
if (${now} - start >= ms) {
|
|
195
277
|
resolve(nullptr);
|
|
196
278
|
} else {
|
|
197
279
|
enqueueMicrotask([ms, start, resolve]() { /* will be re-checked next cycle */ });
|
|
@@ -211,9 +293,9 @@ namespace typecad_async {
|
|
|
211
293
|
// Async.sleepUntil() — poll condition every interval ms
|
|
212
294
|
inline Promise<void> __cuttlefish_async_sleep_until(unsigned long pollIntervalMs) {
|
|
213
295
|
return Promise<void>([pollIntervalMs](std::function<void(const void*)> resolve, std::function<void(const std::string&)> reject) {
|
|
214
|
-
unsigned long start =
|
|
296
|
+
unsigned long start = ${now};
|
|
215
297
|
enqueueMicrotask([pollIntervalMs, start, resolve]() {
|
|
216
|
-
if (
|
|
298
|
+
if (${now} - start >= pollIntervalMs) {
|
|
217
299
|
resolve(nullptr); // caller re-checks condition
|
|
218
300
|
} else {
|
|
219
301
|
enqueueMicrotask([pollIntervalMs, start, resolve]() { /* re-check next cycle */ });
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export type CopyleftRisk = "permissive" | "weak-copyleft" | "strong-copyleft" | "unknown";
|
|
2
|
+
/**
|
|
3
|
+
* Where a license declaration was found.
|
|
4
|
+
*
|
|
5
|
+
* `"library.properties"` is the Arduino library-manifest form (kept verbatim
|
|
6
|
+
* for Arduino parity); `"manifest"` is the framework-neutral form for any
|
|
7
|
+
* other manifest reader a framework supplies (e.g. a west module.yml).
|
|
8
|
+
*/
|
|
9
|
+
export type LicenseSource = "library.properties" | "manifest" | "license-file" | "source-header" | "none";
|
|
10
|
+
/**
|
|
11
|
+
* A resolved license row for a single dependency. Frameworks render this into
|
|
12
|
+
* their own CLI tables.
|
|
13
|
+
*/
|
|
14
|
+
export interface LibraryLicenseEntry {
|
|
15
|
+
name: string;
|
|
16
|
+
version: string | undefined;
|
|
17
|
+
/** Install/root directory of the dependency (where its LICENSE lives). */
|
|
18
|
+
path: string;
|
|
19
|
+
/** Normalized SPDX ID (e.g. "BSD-3-Clause"); undefined if not determined. */
|
|
20
|
+
spdx: string | undefined;
|
|
21
|
+
risk: CopyleftRisk;
|
|
22
|
+
source: LicenseSource;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Neutral dependency shape every framework adapts its enumeration to. Arduino
|
|
26
|
+
* maps `{ name, version, install_dir }` → `{ name, version, installDir }`;
|
|
27
|
+
* Zephyr maps a west-listed project (`{ name, abspath }`) the same way.
|
|
28
|
+
*/
|
|
29
|
+
export interface DiscoveredDependency {
|
|
30
|
+
name: string;
|
|
31
|
+
version?: string;
|
|
32
|
+
/** Absolute path to the dependency's install/root directory. */
|
|
33
|
+
installDir?: string;
|
|
34
|
+
}
|
|
35
|
+
/** Injected file reader (returns undefined on missing/unreadable). */
|
|
36
|
+
export type ReadFile = (p: string) => string | undefined;
|
|
37
|
+
/** Injected directory lister (returns [] on missing/unreadable). */
|
|
38
|
+
export type ReadDir = (d: string) => string[];
|
|
39
|
+
interface SpdxEntry {
|
|
40
|
+
id: string;
|
|
41
|
+
aliases: string[];
|
|
42
|
+
risk: CopyleftRisk;
|
|
43
|
+
/** Substrings, ALL of which must appear in the normalized license text. */
|
|
44
|
+
markers: string[];
|
|
45
|
+
/**
|
|
46
|
+
* Short-form phrases for sparse source-header comments where the full license
|
|
47
|
+
* text is absent (e.g. Adafruit's "BSD license, all text here must be
|
|
48
|
+
* included"). ANY one match is sufficient. Lower-cased on use.
|
|
49
|
+
*/
|
|
50
|
+
shortMarkers: string[];
|
|
51
|
+
}
|
|
52
|
+
export declare const SPDX_TABLE: readonly SpdxEntry[];
|
|
53
|
+
export declare const RISK_RANK: Record<CopyleftRisk, number>;
|
|
54
|
+
/**
|
|
55
|
+
* Candidate LICENSE filenames checked case-insensitively. Includes the British
|
|
56
|
+
* "LICENCE" spelling (e.g. lvgl ships LICENCE.txt) and the `.rst`
|
|
57
|
+
* (reStructuredText) form common in Zephyr/Linux modules (e.g.
|
|
58
|
+
* trusted-firmware-m ships `license.rst`).
|
|
59
|
+
*
|
|
60
|
+
* Deliberately does NOT match per-component `COPYING.<spec>` files (e.g.
|
|
61
|
+
* picolibc's COPYING.GPL2 / COPYING.NEWLIB / COPYING.picolibc): those name a
|
|
62
|
+
* specific license rather than the project license, and matching COPYING.GPL2
|
|
63
|
+
* would falsely flag a BSD project as strong-copyleft.
|
|
64
|
+
*/
|
|
65
|
+
export declare const LICENSE_FILENAMES: readonly string[];
|
|
66
|
+
/**
|
|
67
|
+
* Normalize license input text for matching: trim, lowercase, strip surrounding
|
|
68
|
+
* quotes/whitespace.
|
|
69
|
+
*/
|
|
70
|
+
export declare function normalize(input: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a raw license input (either a short manifest value, the full text of
|
|
73
|
+
* a LICENSE file, or a source-file header comment) to a canonical SPDX ID.
|
|
74
|
+
*
|
|
75
|
+
* Matching priority:
|
|
76
|
+
* 1. SPDX-License-Identifier: <id> marker (authoritative when present)
|
|
77
|
+
* 2. exact alias match (suits the short manifest value)
|
|
78
|
+
* 3. substring markers match, ALL markers required (suits full LICENSE text)
|
|
79
|
+
* 4. shortMarkers match, ANY one sufficient (suits sparse header comments
|
|
80
|
+
* like Adafruit's "BSD license, all text here must be included")
|
|
81
|
+
*
|
|
82
|
+
* Returns the SPDX ID string, or undefined if nothing matched.
|
|
83
|
+
*/
|
|
84
|
+
export declare function identifySpdx(input: string): string | undefined;
|
|
85
|
+
/**
|
|
86
|
+
* Classify the copyleft risk of a known SPDX ID. Returns "unknown" for
|
|
87
|
+
* unrecognized ids.
|
|
88
|
+
*/
|
|
89
|
+
export declare function classifyRisk(spdx: string): CopyleftRisk;
|
|
90
|
+
/**
|
|
91
|
+
* Read the first LICENSE/COPYING file found in a directory and return its text.
|
|
92
|
+
* Checks the directory itself only (no recursion). The match is
|
|
93
|
+
* case-insensitive, but the file is read using its REAL on-disk name (not the
|
|
94
|
+
* canonical candidate) so it works on case-sensitive filesystems where a module
|
|
95
|
+
* ships e.g. `license.rst` rather than `LICENSE.rst`.
|
|
96
|
+
*/
|
|
97
|
+
export declare function readLicenseFileIn(dir: string, readFile: ReadFile, readdir: ReadDir): string | undefined;
|
|
98
|
+
/**
|
|
99
|
+
* Read the first LICENSE/COPYING file found in installDir or one of its common
|
|
100
|
+
* subdirectories (e.g. `src/`, where Arduino's own libraries keep it).
|
|
101
|
+
*/
|
|
102
|
+
export declare function readLicenseFile(installDir: string, readFile: ReadFile, readdir: ReadDir, subdirs?: readonly string[]): string | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Read the leading header comment of each candidate source file in installDir
|
|
105
|
+
* (and its subdirs) and concatenate them, so the SPDX matcher can look for
|
|
106
|
+
* license phrases. The license notice usually lives in the file named after the
|
|
107
|
+
* dependency itself, so such files are scanned first; then a cap of further
|
|
108
|
+
* headers/sources is scanned to keep this cheap.
|
|
109
|
+
*/
|
|
110
|
+
export declare function readSourceHeaders(depName: string, installDir: string, readFile: ReadFile, readdir: ReadDir, subdirs?: readonly string[]): string | undefined;
|
|
111
|
+
export interface ResolveLicenseOptions {
|
|
112
|
+
/** Subdirs searched for a LICENSE file / source headers beyond the root. */
|
|
113
|
+
subdirs?: readonly string[];
|
|
114
|
+
/**
|
|
115
|
+
* Read a manifest file's text for a license field. Given the install dir and
|
|
116
|
+
* the readFile seam, return the raw license value (or undefined). Arduino
|
|
117
|
+
* wires this to read `library.properties` `license=`. Omit to skip the
|
|
118
|
+
* manifest step (e.g. a framework whose manifest carries no license field).
|
|
119
|
+
*/
|
|
120
|
+
readManifestLicense?: (installDir: string, readFile: ReadFile) => string | undefined;
|
|
121
|
+
/**
|
|
122
|
+
* LicenseSource label attached when the license is found via
|
|
123
|
+
* `readManifestLicense`. Defaults to `"manifest"`; Arduino passes
|
|
124
|
+
* `"library.properties"` for parity with its existing output.
|
|
125
|
+
*/
|
|
126
|
+
manifestSourceLabel?: LicenseSource;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Resolve a single dependency's license. Priority: manifest → LICENSE file →
|
|
130
|
+
* source-file header comments → none.
|
|
131
|
+
*
|
|
132
|
+
* Never throws: an unreadable manifest/LICENSE/source simply falls through to
|
|
133
|
+
* the next step, ending at a `"none"` / unknown entry.
|
|
134
|
+
*/
|
|
135
|
+
export declare function resolveLibraryLicense(dep: DiscoveredDependency, readFile: ReadFile, readdir: ReadDir, options?: ResolveLicenseOptions): LibraryLicenseEntry;
|
|
136
|
+
export declare function riskBracket(risk: CopyleftRisk): string;
|
|
137
|
+
export declare function statusMark(risk: CopyleftRisk): string;
|
|
138
|
+
export declare function countByRisk(libs: LibraryLicenseEntry[]): Record<CopyleftRisk, number>;
|
|
139
|
+
export {};
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Shared SPDX license-detection core
|
|
3
|
+
//
|
|
4
|
+
// Framework-agnostic license resolution for the `cuttlefish licenses` command.
|
|
5
|
+
// The pure detection engine (SPDX table, marker matching, copyleft
|
|
6
|
+
// classification) and the file-based license resolver (LICENSE file + source
|
|
7
|
+
// header comments + an injected manifest reader) live here so every framework
|
|
8
|
+
// package can reuse them. Each framework supplies only its own dependency
|
|
9
|
+
// enumeration (arduino-cli lib list, west list, …) and a manifest reader, then
|
|
10
|
+
// calls resolveLibraryLicense.
|
|
11
|
+
//
|
|
12
|
+
// Nothing here imports a framework package or shells out to a toolchain: all
|
|
13
|
+
// filesystem access is through injected readFile/readdir seams, so the core is
|
|
14
|
+
// deterministic and unit-testable without spawning.
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
import path from "node:path";
|
|
17
|
+
export const SPDX_TABLE = [
|
|
18
|
+
{
|
|
19
|
+
id: "MIT",
|
|
20
|
+
risk: "permissive",
|
|
21
|
+
aliases: ["MIT", "MIT-0", "Expat"],
|
|
22
|
+
markers: ["permission is hereby granted, free of charge"],
|
|
23
|
+
shortMarkers: ["mit licence", "mit license"],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "BSD-3-Clause",
|
|
27
|
+
risk: "permissive",
|
|
28
|
+
aliases: ["BSD-3", "BSD-3-Clause", "BSD", "New BSD"],
|
|
29
|
+
markers: [
|
|
30
|
+
"redistribution and use in source and binary forms",
|
|
31
|
+
"neither the name",
|
|
32
|
+
],
|
|
33
|
+
// Adafruit's header convention: "BSD license, all text here/above must be
|
|
34
|
+
// included in any redistribution." Adafruit declares these as BSD-3.
|
|
35
|
+
shortMarkers: ["bsd license, all text", "bsd license. all text"],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "BSD-2-Clause",
|
|
39
|
+
risk: "permissive",
|
|
40
|
+
aliases: ["BSD-2", "BSD-2-Clause", "FreeBSD"],
|
|
41
|
+
markers: [
|
|
42
|
+
"redistribution and use in source and binary forms",
|
|
43
|
+
"redistributions of source code must retain the above copyright notice",
|
|
44
|
+
],
|
|
45
|
+
shortMarkers: [],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "Apache-2.0",
|
|
49
|
+
risk: "permissive",
|
|
50
|
+
aliases: ["Apache-2.0", "Apache 2.0", "Apache-2", "ASL-2.0"],
|
|
51
|
+
markers: ["apache license", "version 2.0"],
|
|
52
|
+
// ArduinoHttpClient header: "Released under Apache License, version 2.0"
|
|
53
|
+
shortMarkers: ["apache license, version 2.0", "under apache license"],
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "LGPL-2.1",
|
|
57
|
+
risk: "weak-copyleft",
|
|
58
|
+
aliases: ["LGPL-2.1", "LGPL-2.1-only", "LGPL-2.1-or-later", "Lesser GPL 2.1"],
|
|
59
|
+
markers: ["gnu lesser general public license", "version 2.1"],
|
|
60
|
+
// ESP32Servo header: "GNU Lesser General Public ... version 2.1"
|
|
61
|
+
shortMarkers: ["gnu lesser general public", "version 2.1"],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "LGPL-3.0",
|
|
65
|
+
risk: "weak-copyleft",
|
|
66
|
+
aliases: ["LGPL-3.0", "LGPL-3", "LGPL-3.0-only", "LGPL-3.0-or-later"],
|
|
67
|
+
markers: ["gnu lesser general public license", "version 3"],
|
|
68
|
+
shortMarkers: [],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "GPL-2.0",
|
|
72
|
+
risk: "strong-copyleft",
|
|
73
|
+
aliases: ["GPL-2.0", "GPL-2", "GPLv2", "GPL-2.0-only", "GPL-2.0-or-later"],
|
|
74
|
+
markers: ["gnu general public license", "version 2"],
|
|
75
|
+
shortMarkers: [],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "GPL-3.0",
|
|
79
|
+
risk: "strong-copyleft",
|
|
80
|
+
aliases: ["GPL-3.0", "GPL-3", "GPLv3", "GPL-3.0-only", "GPL-3.0-or-later"],
|
|
81
|
+
markers: ["gnu general public license", "version 3"],
|
|
82
|
+
shortMarkers: [],
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "AGPL-3.0",
|
|
86
|
+
risk: "strong-copyleft",
|
|
87
|
+
aliases: ["AGPL-3.0", "AGPL-3", "Affero GPL 3", "AGPL-3.0-only", "AGPL-3.0-or-later"],
|
|
88
|
+
markers: ["gnu affero general public license"],
|
|
89
|
+
shortMarkers: [],
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "Unlicense",
|
|
93
|
+
risk: "permissive",
|
|
94
|
+
aliases: ["Unlicense", "The Unlicense"],
|
|
95
|
+
markers: [
|
|
96
|
+
"this is free and unencumbered software released into the public domain",
|
|
97
|
+
],
|
|
98
|
+
shortMarkers: [],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: "CC-BY-4.0",
|
|
102
|
+
risk: "permissive",
|
|
103
|
+
aliases: ["CC-BY-4.0", "Creative Commons Attribution 4.0", "cc by 4.0"],
|
|
104
|
+
markers: ["creative commons attribution 4.0"],
|
|
105
|
+
shortMarkers: [],
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
id: "CC-BY-SA-4.0",
|
|
109
|
+
risk: "strong-copyleft",
|
|
110
|
+
aliases: [
|
|
111
|
+
"CC-BY-SA-4.0",
|
|
112
|
+
"Creative Commons Attribution-ShareAlike 4.0",
|
|
113
|
+
"cc by-sa 4.0",
|
|
114
|
+
],
|
|
115
|
+
markers: ["creative commons attribution-sharealike 4.0"],
|
|
116
|
+
shortMarkers: [],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: "CC-BY-NC-4.0",
|
|
120
|
+
risk: "strong-copyleft",
|
|
121
|
+
aliases: [
|
|
122
|
+
"CC-BY-NC-4.0",
|
|
123
|
+
"Creative Commons Attribution-NonCommercial 4.0",
|
|
124
|
+
"cc by-nc 4.0",
|
|
125
|
+
],
|
|
126
|
+
markers: ["creative commons attribution-noncommercial 4.0"],
|
|
127
|
+
shortMarkers: [],
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
export const RISK_RANK = {
|
|
131
|
+
"strong-copyleft": 0,
|
|
132
|
+
"weak-copyleft": 1,
|
|
133
|
+
permissive: 2,
|
|
134
|
+
unknown: 3,
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* Candidate LICENSE filenames checked case-insensitively. Includes the British
|
|
138
|
+
* "LICENCE" spelling (e.g. lvgl ships LICENCE.txt) and the `.rst`
|
|
139
|
+
* (reStructuredText) form common in Zephyr/Linux modules (e.g.
|
|
140
|
+
* trusted-firmware-m ships `license.rst`).
|
|
141
|
+
*
|
|
142
|
+
* Deliberately does NOT match per-component `COPYING.<spec>` files (e.g.
|
|
143
|
+
* picolibc's COPYING.GPL2 / COPYING.NEWLIB / COPYING.picolibc): those name a
|
|
144
|
+
* specific license rather than the project license, and matching COPYING.GPL2
|
|
145
|
+
* would falsely flag a BSD project as strong-copyleft.
|
|
146
|
+
*/
|
|
147
|
+
export const LICENSE_FILENAMES = [
|
|
148
|
+
"LICENSE",
|
|
149
|
+
"LICENSE.md",
|
|
150
|
+
"LICENSE.txt",
|
|
151
|
+
"LICENSE.markdown",
|
|
152
|
+
"LICENSE.rst",
|
|
153
|
+
"LICENCE",
|
|
154
|
+
"LICENCE.md",
|
|
155
|
+
"LICENCE.txt",
|
|
156
|
+
"LICENCE.rst",
|
|
157
|
+
"COPYING",
|
|
158
|
+
"COPYING.txt",
|
|
159
|
+
"COPYING.rst",
|
|
160
|
+
];
|
|
161
|
+
/**
|
|
162
|
+
* Extensions whose header comments may carry a license notice (Adafruit and
|
|
163
|
+
* many libraries embed the license at the top of the primary source file
|
|
164
|
+
* rather than in a standalone LICENSE file).
|
|
165
|
+
*/
|
|
166
|
+
const HEADER_EXTENSIONS = [".h", ".hpp", ".cpp", ".c"];
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Pure SPDX detection
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
/**
|
|
171
|
+
* Normalize license input text for matching: trim, lowercase, strip surrounding
|
|
172
|
+
* quotes/whitespace.
|
|
173
|
+
*/
|
|
174
|
+
export function normalize(input) {
|
|
175
|
+
return input.trim().toLowerCase().replace(/^["']|["']$/g, "");
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Resolve a raw license input (either a short manifest value, the full text of
|
|
179
|
+
* a LICENSE file, or a source-file header comment) to a canonical SPDX ID.
|
|
180
|
+
*
|
|
181
|
+
* Matching priority:
|
|
182
|
+
* 1. SPDX-License-Identifier: <id> marker (authoritative when present)
|
|
183
|
+
* 2. exact alias match (suits the short manifest value)
|
|
184
|
+
* 3. substring markers match, ALL markers required (suits full LICENSE text)
|
|
185
|
+
* 4. shortMarkers match, ANY one sufficient (suits sparse header comments
|
|
186
|
+
* like Adafruit's "BSD license, all text here must be included")
|
|
187
|
+
*
|
|
188
|
+
* Returns the SPDX ID string, or undefined if nothing matched.
|
|
189
|
+
*/
|
|
190
|
+
export function identifySpdx(input) {
|
|
191
|
+
const norm = normalize(input);
|
|
192
|
+
// 1. SPDX-License-Identifier marker — extract the id and alias-match it.
|
|
193
|
+
// Aliases are stored in their canonical case; compare lowercased.
|
|
194
|
+
const marker = norm.match(/spdx-license-identifier:\s*([^\s\n]+)/);
|
|
195
|
+
if (marker) {
|
|
196
|
+
const id = marker[1].toLowerCase();
|
|
197
|
+
for (const entry of SPDX_TABLE) {
|
|
198
|
+
if (entry.aliases.some((a) => a.toLowerCase() === id)) {
|
|
199
|
+
return entry.id;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
// 2. exact alias match (case-insensitive; `norm` is already lowercased).
|
|
204
|
+
for (const entry of SPDX_TABLE) {
|
|
205
|
+
if (entry.aliases.some((a) => a.toLowerCase() === norm)) {
|
|
206
|
+
return entry.id;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// 3. substring markers — every marker phrase must appear.
|
|
210
|
+
// BSD-3 is listed before BSD-2 so its superset clauses (which contain
|
|
211
|
+
// "neither the name") win over BSD-2's subset.
|
|
212
|
+
for (const entry of SPDX_TABLE) {
|
|
213
|
+
if (entry.markers.every((m) => norm.includes(m))) {
|
|
214
|
+
return entry.id;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// 4. shortMarkers — ANY one match is sufficient. Used for sparse header
|
|
218
|
+
// comments where the full license text is absent.
|
|
219
|
+
for (const entry of SPDX_TABLE) {
|
|
220
|
+
if (entry.shortMarkers.some((m) => norm.includes(m))) {
|
|
221
|
+
return entry.id;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Classify the copyleft risk of a known SPDX ID. Returns "unknown" for
|
|
228
|
+
* unrecognized ids.
|
|
229
|
+
*/
|
|
230
|
+
export function classifyRisk(spdx) {
|
|
231
|
+
const entry = SPDX_TABLE.find((e) => e.id === spdx || e.aliases.includes(spdx));
|
|
232
|
+
return entry ? entry.risk : "unknown";
|
|
233
|
+
}
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
// License-file / source-header readers (pure I/O via injected seams)
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
/**
|
|
238
|
+
* Read the first LICENSE/COPYING file found in a directory and return its text.
|
|
239
|
+
* Checks the directory itself only (no recursion). The match is
|
|
240
|
+
* case-insensitive, but the file is read using its REAL on-disk name (not the
|
|
241
|
+
* canonical candidate) so it works on case-sensitive filesystems where a module
|
|
242
|
+
* ships e.g. `license.rst` rather than `LICENSE.rst`.
|
|
243
|
+
*/
|
|
244
|
+
export function readLicenseFileIn(dir, readFile, readdir) {
|
|
245
|
+
// Map each entry's lowercased name back to its real (on-disk) casing.
|
|
246
|
+
const lowered = new Map();
|
|
247
|
+
for (const e of readdir(dir))
|
|
248
|
+
lowered.set(e.toLowerCase(), e);
|
|
249
|
+
for (const candidate of LICENSE_FILENAMES) {
|
|
250
|
+
const actual = lowered.get(candidate.toLowerCase());
|
|
251
|
+
if (actual !== undefined) {
|
|
252
|
+
return readFile(path.join(dir, actual));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Read the first LICENSE/COPYING file found in installDir or one of its common
|
|
259
|
+
* subdirectories (e.g. `src/`, where Arduino's own libraries keep it).
|
|
260
|
+
*/
|
|
261
|
+
export function readLicenseFile(installDir, readFile, readdir, subdirs = ["src"]) {
|
|
262
|
+
const rootHit = readLicenseFileIn(installDir, readFile, readdir);
|
|
263
|
+
if (rootHit)
|
|
264
|
+
return rootHit;
|
|
265
|
+
for (const sub of subdirs) {
|
|
266
|
+
const text = readLicenseFileIn(path.join(installDir, sub), readFile, readdir);
|
|
267
|
+
if (text)
|
|
268
|
+
return text;
|
|
269
|
+
}
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Read the leading header comment of each candidate source file in installDir
|
|
274
|
+
* (and its subdirs) and concatenate them, so the SPDX matcher can look for
|
|
275
|
+
* license phrases. The license notice usually lives in the file named after the
|
|
276
|
+
* dependency itself, so such files are scanned first; then a cap of further
|
|
277
|
+
* headers/sources is scanned to keep this cheap.
|
|
278
|
+
*/
|
|
279
|
+
export function readSourceHeaders(depName, installDir, readFile, readdir, subdirs = ["src"]) {
|
|
280
|
+
const dirs = [installDir, ...subdirs.map((s) => path.join(installDir, s))];
|
|
281
|
+
// Normalize the dependency name into the stem its source files likely use:
|
|
282
|
+
// "Adafruit seesaw Library" -> "adafruit_seesaw".
|
|
283
|
+
const stem = depName.toLowerCase().replace(/\s+library$/, "").replace(/\s+/g, "_");
|
|
284
|
+
const chunks = [];
|
|
285
|
+
for (const dir of dirs) {
|
|
286
|
+
let entries;
|
|
287
|
+
try {
|
|
288
|
+
entries = readdir(dir);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const sources = entries
|
|
294
|
+
.filter((e) => HEADER_EXTENSIONS.some((ext) => e.toLowerCase().endsWith(ext)))
|
|
295
|
+
// Files whose basename starts with the dependency stem go first — that is
|
|
296
|
+
// where the license header conventionally lives.
|
|
297
|
+
.sort((a, b) => {
|
|
298
|
+
const aMatch = Number(a.toLowerCase().startsWith(stem));
|
|
299
|
+
const bMatch = Number(b.toLowerCase().startsWith(stem));
|
|
300
|
+
return bMatch - aMatch;
|
|
301
|
+
})
|
|
302
|
+
.slice(0, 6);
|
|
303
|
+
for (const src of sources) {
|
|
304
|
+
const text = readFile(path.join(dir, src));
|
|
305
|
+
if (text) {
|
|
306
|
+
// Take a generous leading window. Most licenses sit in the first few
|
|
307
|
+
// lines, but some .cpp files place the notice after a long copyright
|
|
308
|
+
// preamble (e.g. OneWire.cpp at ~line 99), so 120 lines covers it
|
|
309
|
+
// without reading whole large files.
|
|
310
|
+
chunks.push(text.split(/\r?\n/).slice(0, 120).join("\n"));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return chunks.length > 0 ? chunks.join("\n") : undefined;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Resolve a single dependency's license. Priority: manifest → LICENSE file →
|
|
318
|
+
* source-file header comments → none.
|
|
319
|
+
*
|
|
320
|
+
* Never throws: an unreadable manifest/LICENSE/source simply falls through to
|
|
321
|
+
* the next step, ending at a `"none"` / unknown entry.
|
|
322
|
+
*/
|
|
323
|
+
export function resolveLibraryLicense(dep, readFile, readdir, options = {}) {
|
|
324
|
+
const installDir = dep.installDir ?? "";
|
|
325
|
+
const subdirs = options.subdirs ?? ["src"];
|
|
326
|
+
const manifestLabel = options.manifestSourceLabel ?? "manifest";
|
|
327
|
+
const base = {
|
|
328
|
+
name: dep.name,
|
|
329
|
+
version: dep.version,
|
|
330
|
+
path: installDir,
|
|
331
|
+
};
|
|
332
|
+
// 1. manifest (e.g. Arduino library.properties `license=`).
|
|
333
|
+
if (options.readManifestLicense) {
|
|
334
|
+
const manifestLicense = options.readManifestLicense(installDir, readFile);
|
|
335
|
+
if (manifestLicense) {
|
|
336
|
+
const spdx = identifySpdx(manifestLicense);
|
|
337
|
+
if (spdx) {
|
|
338
|
+
return { ...base, spdx, risk: classifyRisk(spdx), source: manifestLabel };
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// 2. LICENSE file (root or a subdir).
|
|
343
|
+
const fileText = readLicenseFile(installDir, readFile, readdir, subdirs);
|
|
344
|
+
if (fileText) {
|
|
345
|
+
const spdx = identifySpdx(fileText);
|
|
346
|
+
if (spdx) {
|
|
347
|
+
return { ...base, spdx, risk: classifyRisk(spdx), source: "license-file" };
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// 3. source-file header comments (Adafruit/Arduino pattern: license notice
|
|
351
|
+
// embedded at the top of the primary .h/.cpp, no standalone LICENSE file).
|
|
352
|
+
const headerText = readSourceHeaders(dep.name, installDir, readFile, readdir, subdirs);
|
|
353
|
+
if (headerText) {
|
|
354
|
+
const spdx = identifySpdx(headerText);
|
|
355
|
+
if (spdx) {
|
|
356
|
+
return { ...base, spdx, risk: classifyRisk(spdx), source: "source-header" };
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// 4. unknown
|
|
360
|
+
return { ...base, spdx: undefined, risk: "unknown", source: "none" };
|
|
361
|
+
}
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
// Presentation helpers (shared by every framework's presenter)
|
|
364
|
+
// ---------------------------------------------------------------------------
|
|
365
|
+
export function riskBracket(risk) {
|
|
366
|
+
if (risk === "strong-copyleft")
|
|
367
|
+
return " [COPYLEFT]";
|
|
368
|
+
if (risk === "weak-copyleft")
|
|
369
|
+
return " [weak copyleft]";
|
|
370
|
+
return "";
|
|
371
|
+
}
|
|
372
|
+
export function statusMark(risk) {
|
|
373
|
+
return risk === "permissive" ? " ✓" : "";
|
|
374
|
+
}
|
|
375
|
+
export function countByRisk(libs) {
|
|
376
|
+
const counts = {
|
|
377
|
+
permissive: 0,
|
|
378
|
+
"weak-copyleft": 0,
|
|
379
|
+
"strong-copyleft": 0,
|
|
380
|
+
unknown: 0,
|
|
381
|
+
};
|
|
382
|
+
for (const l of libs)
|
|
383
|
+
counts[l.risk] += 1;
|
|
384
|
+
return counts;
|
|
385
|
+
}
|