|
| 1 | +#!/usr/bin/env bash |
| 2 | +# SPDX-License-Identifier: Apache-2.0 |
| 3 | +# |
| 4 | +# spdx-headers.sh — check or add SPDX license headers in a CuOS repository. |
| 5 | +# |
| 6 | +# ./spdx-headers.sh check [REPO_DIR] # list files missing a header; exit 1 if any |
| 7 | +# ./spdx-headers.sh fix [REPO_DIR] # insert the header where it is missing |
| 8 | +# ./spdx-headers.sh list [REPO_DIR] # list every file considered in scope |
| 9 | +# |
| 10 | +# REPO_DIR defaults to the current directory. Only git-tracked files are |
| 11 | +# considered, so build output and untracked scratch files are ignored. |
| 12 | +# |
| 13 | +# Policy: Apache-2.0 for every file CuOS itself authors — see |
| 14 | +# brain/decisions/0007-apache-2-0-everywhere.md. There is still no license map: |
| 15 | +# every in-scope file gets the same identifier. What there now *is* is a short |
| 16 | +# list of vendored third-party files that must never be stamped at all, because |
| 17 | +# they carry someone else's license — see third_party() below. |
| 18 | +# |
| 19 | +# Scope and exclusions are defined in third_party()/in_scope()/comment_style() |
| 20 | +# below, with a reason per exclusion. Keep them there and nowhere else. |
| 21 | +# |
| 22 | +# Run by .github/workflows/ci.yml. The same file is vendored in all four CuOS |
| 23 | +# repositories; change it in one and copy it to the others. |
| 24 | + |
| 25 | +set -euo pipefail |
| 26 | + |
| 27 | +IDENTIFIER="Apache-2.0" |
| 28 | +MODE="${1:-check}" |
| 29 | +REPO_DIR="${2:-.}" |
| 30 | + |
| 31 | +usage() { |
| 32 | + sed -n '3,20p' "${BASH_SOURCE[0]}" | sed 's|^# \?||' |
| 33 | + exit 2 |
| 34 | +} |
| 35 | + |
| 36 | +case "${MODE}" in |
| 37 | + check|fix|list) ;; |
| 38 | + *) usage ;; |
| 39 | +esac |
| 40 | + |
| 41 | +cd "${REPO_DIR}" || { echo "Error: cannot enter ${REPO_DIR}" >&2; exit 2; } |
| 42 | +git rev-parse --git-dir >/dev/null 2>&1 || { |
| 43 | + echo "Error: ${REPO_DIR} is not a git repository." >&2; exit 2; } |
| 44 | + |
| 45 | +# Which comment syntax a file uses — also decides whether it is in scope at all. |
| 46 | +comment_style() { |
| 47 | + local file="$1" |
| 48 | + case "${file}" in |
| 49 | + # ---- in scope ----------------------------------------------------------- |
| 50 | + *.sh|Dockerfile|*/Dockerfile|Dockerfile.*|*/Dockerfile.*) echo hash ;; |
| 51 | + .github/*.yml|.github/*.yaml|*/.github/*.yml|*/.github/*.yaml) echo hash ;; |
| 52 | + docker-compose*.yml|*/docker-compose*.yml) echo hash ;; |
| 53 | + *.js|*.jsx|*.mjs|*.cjs|*.ts|*.tsx) echo slash ;; |
| 54 | + *.css) echo block ;; |
| 55 | + |
| 56 | + # ---- out of scope, with the reason -------------------------------------- |
| 57 | + # *.json — JSON cannot carry comments (schemas, fixtures, package*) |
| 58 | + # *.md — documentation; the license is stated in LICENSE/NOTICE |
| 59 | + # *.handlebars — could use {{!-- --}}; deliberately left out for now |
| 60 | + # *.test.*.yml — test fixtures: a header may change what a test compares |
| 61 | + # *.gpg, *.key — third-party archive keys and binary material |
| 62 | + # *.png — binary |
| 63 | + # LICENSE*, NOTICE, DCO.txt, *.rc, *.service, *.socket, *.env, *.cmd |
| 64 | + # — legal texts and small config files |
| 65 | + # NB cuos/system/armbianEnv.txt carries an Apache-2.0 header anyway (U-Boot |
| 66 | + # skips '#' lines on "env import -t", verified 2026-08-26). It is out of |
| 67 | + # scope here, so the tool neither checks nor maintains it. |
| 68 | + *) echo "" ;; |
| 69 | + esac |
| 70 | +} |
| 71 | + |
| 72 | +# Vendored third-party files. These carry someone else's copyright, so this |
| 73 | +# script must never stamp them with the project identifier — not even if the |
| 74 | +# extension is later brought into scope. They are listed here rather than in |
| 75 | +# comment_style() so the reason survives a change to the scope rules. |
| 76 | +# Attribution for each one lives in the repo's NOTICE. |
| 77 | +third_party() { |
| 78 | + case "$1" in |
| 79 | + # Armbian U-Boot boot script for Allwinner H616 (Orange Pi Zero 3), from |
| 80 | + # armbian/build config/bootscripts/boot-sun50iw9.cmd — GPL-2.0-only. |
| 81 | + system/armbian-boot.cmd|*/system/armbian-boot.cmd) return 0 ;; |
| 82 | + esac |
| 83 | + return 1 |
| 84 | +} |
| 85 | + |
| 86 | +in_scope() { |
| 87 | + local file="$1" |
| 88 | + |
| 89 | + # Someone else's license: never in scope, whatever the extension says. |
| 90 | + third_party "${file}" && return 1 |
| 91 | + |
| 92 | + # The test *scripts* are ordinary shell code and must be matched BEFORE the |
| 93 | + # fixture exclusion below — `api.test.sh` also matches `*.test.*`. |
| 94 | + case "${file}" in |
| 95 | + *.test.sh) return 0 ;; |
| 96 | + esac |
| 97 | + # Test fixtures: never touch, whatever the extension. A header could change |
| 98 | + # what a test compares. |
| 99 | + case "${file}" in |
| 100 | + *.test.*|*/init.test.sys_class_net/*) return 1 ;; |
| 101 | + esac |
| 102 | + |
| 103 | + case "${file}" in |
| 104 | + */node_modules/*|node_modules/*) return 1 ;; |
| 105 | + */dist/*|dist/*) return 1 ;; |
| 106 | + *.min.js) return 1 ;; |
| 107 | + esac |
| 108 | + |
| 109 | + [[ -n "$(comment_style "${file}")" ]] && return 0 |
| 110 | + |
| 111 | + # Extensionless files count when they carry a shebang (dev-container/docker, |
| 112 | + # iac/api/*, initramfs hooks). |
| 113 | + if [[ "$(basename "${file}")" != *.* ]] && [[ -f "${file}" ]]; then |
| 114 | + head -c2 "${file}" 2>/dev/null | grep -q '#!' && return 0 |
| 115 | + fi |
| 116 | + return 1 |
| 117 | +} |
| 118 | + |
| 119 | +style_for() { # extensionless shebang files are shell |
| 120 | + local s |
| 121 | + s="$(comment_style "$1")" |
| 122 | + echo "${s:-hash}" |
| 123 | +} |
| 124 | + |
| 125 | +has_header() { |
| 126 | + head -5 "$1" 2>/dev/null | grep -q "SPDX-License-Identifier" |
| 127 | +} |
| 128 | + |
| 129 | +header_line() { |
| 130 | + case "$1" in |
| 131 | + hash) echo "# SPDX-License-Identifier: ${IDENTIFIER}" ;; |
| 132 | + slash) echo "// SPDX-License-Identifier: ${IDENTIFIER}" ;; |
| 133 | + block) echo "/* SPDX-License-Identifier: ${IDENTIFIER} */" ;; |
| 134 | + esac |
| 135 | +} |
| 136 | + |
| 137 | +# Insert the header. A shebang MUST stay on line 1 — inserting above it breaks |
| 138 | +# the file. This bug shipped twice during the initial sweep; do not "simplify" |
| 139 | +# it away. |
| 140 | +insert_header() { |
| 141 | + local file="$1" line="$2" tmp |
| 142 | + tmp="$(mktemp)" |
| 143 | + if head -1 "${file}" | grep -q '^#!'; then |
| 144 | + { head -1 "${file}"; echo "${line}"; tail -n +2 "${file}"; } >"${tmp}" |
| 145 | + else |
| 146 | + { echo "${line}"; cat "${file}"; } >"${tmp}" |
| 147 | + fi |
| 148 | + cat "${tmp}" >"${file}" # preserves mode and ownership |
| 149 | + rm -f "${tmp}" |
| 150 | +} |
| 151 | + |
| 152 | +missing=0 |
| 153 | +fixed=0 |
| 154 | +scoped=0 |
| 155 | + |
| 156 | +while IFS= read -r file; do |
| 157 | + [[ -f "${file}" ]] || continue # deleted but still tracked |
| 158 | + in_scope "${file}" || continue |
| 159 | + scoped=$((scoped + 1)) |
| 160 | + |
| 161 | + if [[ "${MODE}" == "list" ]]; then |
| 162 | + printf '%s\t%s\n' "$(style_for "${file}")" "${file}" |
| 163 | + continue |
| 164 | + fi |
| 165 | + |
| 166 | + has_header "${file}" && continue |
| 167 | + |
| 168 | + if [[ "${MODE}" == "fix" ]]; then |
| 169 | + insert_header "${file}" "$(header_line "$(style_for "${file}")")" |
| 170 | + echo "added: ${file}" |
| 171 | + fixed=$((fixed + 1)) |
| 172 | + else |
| 173 | + echo "missing header: ${file}" |
| 174 | + missing=$((missing + 1)) |
| 175 | + fi |
| 176 | +done < <(git ls-files) |
| 177 | + |
| 178 | +case "${MODE}" in |
| 179 | + list) |
| 180 | + echo "---" |
| 181 | + echo "${scoped} files in scope" |
| 182 | + ;; |
| 183 | + fix) |
| 184 | + echo "---" |
| 185 | + echo "${fixed} header(s) added, ${scoped} files in scope" |
| 186 | + ;; |
| 187 | + check) |
| 188 | + if (( missing > 0 )); then |
| 189 | + echo "---" >&2 |
| 190 | + echo "FAIL: ${missing} of ${scoped} in-scope files lack an SPDX header." >&2 |
| 191 | + echo "Run: $(basename "${BASH_SOURCE[0]}") fix" >&2 |
| 192 | + exit 1 |
| 193 | + fi |
| 194 | + echo "OK: all ${scoped} in-scope files carry an SPDX header." |
| 195 | + ;; |
| 196 | +esac |
0 commit comments