From 1c9a4776cf45ecb5fe1142311a21b7c81d2dda63 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Tue, 14 Jul 2026 16:13:36 +0900 Subject: [PATCH 01/20] =?UTF-8?q?feat:=20=E5=85=B1=E6=9C=89=E5=AE=A3?= =?UTF-8?q?=E8=A8=80=E3=81=A8=E4=B8=96=E4=BB=A3=E5=85=AC=E9=96=8B=E3=82=92?= =?UTF-8?q?=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dsl/check.awk | 33 ++++++++ dsl/desugar.awk | 9 +++ dsl/index.awk | 89 ++++++++++++++++++++ dsl/rpn.awk | 3 + libexec/hawk-libs | 166 ++++++++++++++++++++++++++++++++++++-- tests/unit/desugar/run.sh | 53 ++++++------ 6 files changed, 321 insertions(+), 32 deletions(-) create mode 100644 dsl/index.awk diff --git a/dsl/check.awk b/dsl/check.awk index d1e74c61..d26d9f14 100644 --- a/dsl/check.awk +++ b/dsl/check.awk @@ -1887,11 +1887,13 @@ function v2_prescan_expr_type(id, kind, name, inner, ret_sig, typearg) { function v2_check() { v2_init_builtins() + v2_merge_shared() VARIANTS["Result"] = "ok" SUBSEP "ng" VARIANTS["Option"] = "some" SUBSEP "none" v2_collect_raw_funcs() v2_collect(1) + v2_check_shared_consistency() v2_infer_unannotated_returns(1) v2_check_alias_cycles() v2_check_record_return_type(1) @@ -1906,6 +1908,37 @@ function v2_check() { v2_check_mut(1) } +function v2_check_shared_consistency( k) { + for (k in V2_SHARED_ALIAS) + if (ALIAS[k] != V2_SHARED_ALIAS[k]) v2_diag(1, 1, "conflicting shared type alias: " k) + for (k in V2_SHARED_SIG) + if (SIG[k] != V2_SHARED_SIG[k]) v2_diag(1, 1, "conflicting shared function signature") + for (k in V2_SHARED_RECORD_FIELDS) + if (V2_RECORD_FIELDS[k] != V2_SHARED_RECORD_FIELDS[k]) v2_diag(1, 1, "conflicting shared record field") +} + +function v2_merge_shared( k, p) { + for (k in V2_SHARED_ALIAS) { + if ((k in ALIAS) && ALIAS[k] != V2_SHARED_ALIAS[k]) + v2_diag(1, 1, "conflicting shared type alias: " k) + ALIAS[k] = V2_SHARED_ALIAS[k] + } + for (k in V2_SHARED_SIG) { + split(k, p, SUBSEP) + if ((k in SIG) && SIG[k] != V2_SHARED_SIG[k]) + v2_diag(1, 1, "conflicting shared function signature: " p[1]) + SIG[k] = V2_SHARED_SIG[k] + } + for (k in V2_SHARED_RECORD_TYPE) V2_RECORD_TYPE[k] = 1 + for (k in V2_SHARED_RECORD_FIELDS) { + split(k, p, SUBSEP) + if ((k in V2_RECORD_FIELDS) && V2_RECORD_FIELDS[k] != V2_SHARED_RECORD_FIELDS[k]) + v2_diag(1, 1, "conflicting shared record field: " p[1] "." p[2]) + V2_RECORD_FIELDS[k] = V2_SHARED_RECORD_FIELDS[k] + } + for (k in V2_SHARED_RAW_FUNC) V2_RAW_FUNC[k] = 1 +} + # LET/LETQ の直接の初期化式(最後の子)だけが空コレクションリテラル # []/{} を受け付けられる(v2_e_let はこの位置しかハンドルしない)。 # v2_check_bare_collection_lit より前に呼び、当該 id を許可集合に登録する。 diff --git a/dsl/desugar.awk b/dsl/desugar.awk index b7e427cd..b46e71ed 100644 --- a/dsl/desugar.awk +++ b/dsl/desugar.awk @@ -15,6 +15,7 @@ BEGIN { V2_SRC = (ARGC > 1) ? ARGV[1] : "/dev/stdin" V2_ERRORS = 0 v2_lex(V2_SRC) + v2_merge_shared_pre_rpn() if (V2_DUMP == "lex") { v2_dump_lex(); exit(V2_ERRORS ? 1 : 0) } v2_rpn() if (V2_DUMP == "rpn") { v2_dump_rpn(); exit(V2_ERRORS ? 1 : 0) } @@ -27,6 +28,14 @@ BEGIN { exit(V2_ERRORS ? 1 : 0) } +function v2_merge_shared_pre_rpn( k, p) { + for (k in V2_SHARED_RECORD_TYPE) V2_RECORD_TYPE[k] = 1 + for (k in V2_SHARED_RECORD_FIELDS) { + split(k, p, SUBSEP) + V2_RECORD_FIELDS[p[1], p[2]] = V2_SHARED_RECORD_FIELDS[k] + } +} + function v2_dump_lex( i) { for (i = 1; i <= TOK["n"]; i++) printf "%d\t%s\t%d\t%d\t%s\n", i, TOK[i,"kind"], TOK[i,"line"], TOK[i,"col"], TOK[i,"text"] } function v2_dump_rpn( i) { for (i = 1; i <= RPN["n"]; i++) printf "%d\t%s\t%d\t%s\t%s\n", i, RPN[i,"kind"], RPN[i,"line"], RPN[i,"val"], ((RPN[i,"kind"]=="CALL") ? RPN[i,"arity"] : "") } function v2_dump_ast( id, k, cs) { for (id = 1; id <= V2_NAST; id++) { cs = ""; for (k = 1; k <= AST[id,"nc"]; k++) cs = cs ((k>1)?",":"") AST[id,"c" k]; printf "%d\t%s\t%d\t%d\t%s\t%s\n", id, AST[id,"kind"], AST[id,"line"], AST[id,"nc"], cs, AST[id,"text"] } } diff --git a/dsl/index.awk b/dsl/index.awk new file mode 100644 index 00000000..0edf9867 --- /dev/null +++ b/dsl/index.awk @@ -0,0 +1,89 @@ +# SPDX-License-Identifier: MIT +@include "dsl/util.awk" +@include "dsl/lex.awk" +@include "dsl/rpn.awk" +@include "dsl/parse.awk" +@include "dsl/check.awk" + +function q(s) { + gsub(/\\/, "\\\\", s) + gsub(/"/, "\\\"", s) + return "\"" s "\"" +} + +function emit_shared(out, k, p, n, v) { + print "BEGIN {" > out + for (k in V2_RECORD_TYPE) + printf " V2_SHARED_RECORD_TYPE[%s] = 1\n", q(k) > out + for (k in V2_RECORD_FIELDS) { + split(k, p, SUBSEP) + printf " V2_SHARED_RECORD_FIELDS[%s, %s] = %s\n", q(p[1]), q(p[2]), q(V2_RECORD_FIELDS[k]) > out + } + for (k in ALIAS) + printf " V2_SHARED_ALIAS[%s] = %s\n", q(k), q(ALIAS[k]) > out + for (k in SIG) { + split(k, p, SUBSEP) + printf " V2_SHARED_SIG[%s, %s] = %s\n", q(p[1]), q(p[2]), q(SIG[k]) > out + } + for (k in V2_RAW_FUNC) + printf " V2_SHARED_RAW_FUNC[%s] = 1\n", q(k) > out + print "}" > out +} + +BEGIN { + if (V2_INDEX_LIST == "") exit 1 + V2_INDEX_MODE = 1 + n = split(V2_INDEX_LIST, files, SUBSEP) + for (i = 1; i <= n; i++) { + if (files[i] == "") continue + delete V2_INDEX_RUN_FIELDS + delete V2_INDEX_RUN_RECORDS + delete V2_INDEX_OLD_FIELDS + for (k in V2_INDEX_RECORD_FIELDS) V2_INDEX_OLD_FIELDS[k] = V2_INDEX_RECORD_FIELDS[k] + delete V2_INDEX_OLD_RECORDS + for (k in V2_INDEX_RECORD_NAMES) V2_INDEX_OLD_RECORDS[k] = 1 + v2_lex(files[i]) + v2_rpn() + for (k in V2_RECORD_FIELDS) { + if ((k in V2_INDEX_RECORD_FIELDS) && V2_INDEX_RECORD_FIELDS[k] != V2_RECORD_FIELDS[k]) { + print "[hawk-libs] index failed: conflicting record field definition" > "/dev/stderr" + exit 1 + } + V2_INDEX_RECORD_FIELDS[k] = V2_RECORD_FIELDS[k] + } + for (k in V2_INDEX_OLD_FIELDS) { + split(k, p, SUBSEP) + if ((p[1] in V2_INDEX_RUN_RECORDS) && !(k in V2_INDEX_RUN_FIELDS)) { + print "[hawk-libs] index failed: conflicting record field definition" > "/dev/stderr" + exit 1 + } + } + for (k in V2_INDEX_RUN_FIELDS) { + split(k, p, SUBSEP) + if ((p[1] in V2_INDEX_OLD_RECORDS) && !(k in V2_INDEX_OLD_FIELDS)) { + print "[hawk-libs] index failed: conflicting record field definition" > "/dev/stderr" + exit 1 + } + } + for (k in V2_INDEX_RUN_RECORDS) V2_INDEX_RECORD_NAMES[k] = 1 + v2_parse() + v2_collect(1) + v2_collect_raw_funcs() + for (k in ALIAS) { + if ((k in V2_INDEX_ALIAS) && V2_INDEX_ALIAS[k] != ALIAS[k]) { + print "[hawk-libs] index failed: conflicting type alias definition: " k > "/dev/stderr" + exit 1 + } + V2_INDEX_ALIAS[k] = ALIAS[k] + } + for (k in SIG) { + if ((k in V2_INDEX_SIG) && V2_INDEX_SIG[k] != SIG[k]) { + print "[hawk-libs] index failed: conflicting function signature: " k > "/dev/stderr" + exit 1 + } + V2_INDEX_SIG[k] = SIG[k] + } + } + emit_shared(V2_INDEX_OUT) + exit 0 +} diff --git a/dsl/rpn.awk b/dsl/rpn.awk index 1bb9bb1e..35c7781b 100644 --- a/dsl/rpn.awk +++ b/dsl/rpn.awk @@ -857,6 +857,7 @@ function v2_rpn_typedecl(i, line, name, j, typestart, typetext) { if (j <= TOK["n"] && TOK[j,"kind"] == "OP" && TOK[j,"text"] == "=" && \ TOK[j+1,"kind"] == "LBRACE") { V2_RECORD_TYPE[name] = 1 + if (V2_INDEX_MODE) V2_INDEX_RUN_RECORDS[name] = 1 return v2_rpn_record_fields(name, j + 1) } @@ -899,6 +900,7 @@ function v2_rpn_record_fields(typename, open_pos, j, fname, typestart, typete for (k = typestart; k < j; k++) typetext = typetext ((TOK[k,"kind"] == "COMMA") ? ", " : TOK[k,"text"]) V2_RECORD_FIELDS[typename, fname] = typetext + if (V2_INDEX_MODE) V2_INDEX_RUN_FIELDS[typename, fname] = typetext if (TOK[j,"kind"] == "COMMA") j++ # inline 形の区切り } # v1 は record 宣言ブロック全体と直後の空行 1 行をまとめてスキップし、 @@ -1490,6 +1492,7 @@ function v2_rpn( i) { # ファイル先頭から順に埋める。プログラム全体で 1 つの表(関数スコープでは # クリアしない)。 delete V2_RPN_ALIAS + for (i in V2_SHARED_ALIAS) V2_RPN_ALIAS[i] = V2_SHARED_ALIAS[i] v2_rpn_prescan_records() diff --git a/libexec/hawk-libs b/libexec/hawk-libs index b702aebb..9d37ab10 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -82,6 +82,62 @@ case "$subcmd" in fi src_root="$(cd "$(dirname "$src")" && pwd -P)" dist_phys="$(cd "$dist" && pwd -P)" + cwd_phys="$(pwd -P)" + case "$src" in + /*) + src_abs="$src" + ;; + *) + src_abs="$cwd_phys/$src" + ;; + esac + _src_resolved="$src_abs" + _src_hops=0 + while [[ -L "$_src_resolved" ]]; do + _src_hops=$((_src_hops + 1)) + if [[ $_src_hops -gt 40 ]]; then + echo "[hawk-libs] desugar failed: symlink loop resolving source: $src" >&2 + exit 1 + fi + _src_target="$(readlink "$_src_resolved")" + if [[ "$_src_target" == /* ]]; then + _src_resolved="$_src_target" + else + _src_resolved="$(dirname "$_src_resolved")/$_src_target" + fi + done + src_abs="$(cd "$(dirname "$_src_resolved")" && pwd -P)/$(basename "$_src_resolved")" + case "$src_abs" in + "$cwd_phys"/*) entry_key="${src_abs#"$cwd_phys"/}" ;; + *) entry_key="external-$(printf '%s' "$src_root" | cksum | awk '{print $1}')/$(basename "$src")" ;; + esac + while [[ "$entry_key" == ./* ]]; do entry_key="${entry_key#./}"; done + while [[ "$entry_key" == */./* ]]; do entry_key="${entry_key/\/\.\//\/}"; done + case "$entry_key" in ../*|*/../*|*" "*) + echo "[hawk-libs] desugar failed: invalid entry path: $entry_key" >&2 + exit 1 + ;; + esac + if [[ "$entry_key" == */* ]]; then + namespace="${entry_key%/*}" + else + namespace="" + fi + _dist_has_source=0 + if [[ "$dist_phys/" == "$src_root/"* ]]; then + while IFS= read -r _existing; do + [[ "$_existing" == "$dist/.hawk-dist" || "$_existing" == "$dist/.hawk-generations/"* ]] || { _dist_has_source=1; break; } + done < <(find "$dist" -type f -print) + fi + if [[ "$src_abs" == "$dist_phys" || "$src_abs" == "$dist_phys/"* || $_dist_has_source -eq 1 ]]; then + echo "[hawk-libs] desugar failed: refusing to overwrite source tree (dist and source trees overlap)" >&2 + exit 1 + fi + mkdir -p "$dist/.hawk-generations" + stage_root="$(mktemp -d "$dist/.hawk-generations/gen.XXXXXX")" + if [[ "$dist" = /* ]]; then runtime_root="$dist/current"; else runtime_root="$dist_phys/current"; fi + runtime_ns="$runtime_root" + [[ -n "$namespace" ]] && runtime_ns="$runtime_ns/$namespace" _seen=" " # scratch と publish キューは配列で持つ # (空白入りパスが word splitting で別引数に化けるのを防ぐ) @@ -99,6 +155,43 @@ case "$subcmd" in # を hard link 経由で見逃す。詳細は下の post-closure 検査を参照) _abs_ext=() + declare -A _index_seen=() + _index_files=() + _index_one() { + local rel="$1" inc p + [[ -n "${_index_seen[$rel]+x}" ]] && return 0 + case "$rel" in + ../*|*/../*) + echo "[hawk-libs] desugar failed: include escapes project root via '..': $rel" >&2 + exit 1 + ;; + *" "*) + echo "[hawk-libs] desugar failed: include path contains spaces (unsupported): $rel" >&2 + exit 1 + ;; + .hawk-dist|.hawk-dist/*) + echo "[hawk-libs] desugar failed: '.hawk-dist' is reserved for the dist marker: $rel" >&2 + exit 1 + ;; + esac + _index_seen["$rel"]=1 + p="$src_root/$rel" + [[ -f "$p" ]] || return 0 + _index_files+=("$p") + while IFS= read -r inc; do + [[ -n "$inc" && "$inc" != /* ]] || continue + p="$inc" + while [[ "$p" == ./* ]]; do p="${p#./}"; done + while [[ "$p" == */./* ]]; do p="${p/\/\.\//\/}"; done + [[ "$p" == "$entry_rel" || "$src_root/$p" -ef "$src_root/$entry_rel" ]] && { + echo "[hawk-libs] desugar failed: include cycle back to entry: $rel includes $inc" >&2 + exit 1 + } + [[ -f "$src_root/$p" ]] && _index_one "$p" + done < <(gawk 'match($0, /^[[:space:]]*@include[[:space:]]+"([^"]+)"/, m) { print m[1] }' "$p") + return 0 + } + # rel: src_root からの相対パス。desugar して dist/rel に出力し、 # src_root 相対で実在する @include を dist 接頭に書き換えて再帰する。 _desugar_one() { @@ -140,7 +233,34 @@ case "$subcmd" in set +f _seen="$_seen$rel " in="$src_root/$rel" - out="$dist/$rel" + out="$stage_root/${namespace:+$namespace/}$rel" + stable="$dist/${namespace:+$namespace/}$rel" + compat="$dist/$rel" + compat_parent="$(dirname "$compat")" + while [[ ! -e "$compat_parent" && ! -L "$compat_parent" && "$compat_parent" != "." && "$compat_parent" != "/" ]]; do + compat_parent="$(dirname "$compat_parent")" + done + compat_parent_phys="$(cd "$compat_parent" && pwd -P)" + if [[ "$compat_parent_phys/" != "$dist_phys/"* ]]; then + echo "[hawk-libs] desugar failed: output directory escapes dist via symlink: $compat_parent_phys" >&2 + exit 1 + fi + if [[ -d "$stable" || ( -e "$stable" && ! -L "$stable" ) || -d "$compat" ]]; then + echo "[hawk-libs] desugar failed: output path is not replaceable: $stable" >&2 + exit 1 + fi + if [[ -L "$compat" && ! -e "$compat" ]]; then + if [[ ! -f "$dist/.hawk-dist" ]] || ! grep -qxF -- "$src_root"$'\t'"$rel" "$dist/.hawk-dist"; then + echo "[hawk-libs] desugar failed: dangling output symlink: $compat" >&2 + exit 1 + fi + fi + if [[ -e "$stable" || -L "$stable" ]]; then + if [[ ! -f "$dist/.hawk-dist" ]] || ! grep -qxF -- "$src_root"$'\t'"$rel" "$dist/.hawk-dist"; then + echo "[hawk-libs] desugar failed: refusing to overwrite existing namespaced output: $stable" >&2 + exit 1 + fi + fi # -ef は device/inode 比較。symlink 経由 (HAWK_DIST・source どちら側でも) # の source エイリアスを文字列比較より確実に検出する if [[ -e "$out" && "$in" -ef "$out" ]]; then @@ -213,7 +333,7 @@ case "$subcmd" in # dsl/desugar.awk 自身が @include "dsl/util.awk" 等の相対パスを持ち、 # gawk は '/' を含む @include をカレントディレクトリ相対で解決するため、 # HAWK_LIB (リポジトリルート) に cd してから実行する。 - if ! ( cd "$HAWK_LIB" && gawk -f dsl/desugar.awk "$in" ) > "$tmp"; then + if ! ( cd "$HAWK_LIB" && gawk -f "$_index_tmp" -f dsl/desugar.awk "$in" ) > "$tmp"; then rm -f "$tmp" echo "[hawk-libs] desugar failed: $in" >&2 exit 1 @@ -304,7 +424,7 @@ case "$subcmd" in # dist 接頭の実パスに書き換える tmp2="$(mktemp "$(dirname "$out")/.hawk-desugar.XXXXXX")" _scratch_arr+=("$tmp2") - if ! gawk -v dist="$dist" -v list="$incs" ' + if ! gawk -v dist="$runtime_ns" -v list="$incs" ' BEGIN { n = split(list, a, "\n"); for (i = 1; i <= n; i++) if (a[i] != "") L[a[i]] = 1 } { if (match($0, /^([[:space:]]*@include[[:space:]]+")([^"]+)(".*)$/, m) && (m[2] in L)) @@ -333,6 +453,17 @@ case "$subcmd" in } entry_rel="$(basename "$src")" + _index_one "$entry_rel" + _index_tmp="$(mktemp "$dist/.hawk-index.XXXXXX")" + _scratch_arr+=("$_index_tmp") + _index_list="" + for _index_file in "${_index_files[@]}"; do + _index_list="${_index_list}${_index_list:+$'\034'}${_index_file}" + done + if ! ( cd "$HAWK_LIB" && gawk -v V2_INDEX_LIST="$_index_list" -v V2_INDEX_OUT="$_index_tmp" -f dsl/index.awk ); then + echo "[hawk-libs] desugar failed: shared declaration index failed" >&2 + exit 1 + fi _desugar_one "$entry_rel" # 絶対パス include の hard link 検査は closure 全体の _seen が確定してから行う # (staging 中に照合すると未訪問の後続 sibling を見逃す)。publish の前に置き、 @@ -357,14 +488,37 @@ case "$subcmd" in # 空ファイルのままだと最初の成功後にディレクトリ全体を信頼してしまうため、 # rel を1行1件記録し、次回以降の上書き可否を rel 単位で判定できるようにする。 # 更新も稼働中の hawk-serve と競合しないよう mktemp + mv でアトミックに行う - _marker_tmp="$(mktemp "$dist/.hawk-dist.XXXXXX")" + _marker_tmp="$(mktemp "$stage_root/.hawk-dist.XXXXXX")" _scratch_arr+=("$_marker_tmp") { [[ -f "$dist/.hawk-dist" ]] && cat "$dist/.hawk-dist" for _rel in "${_pub_rel[@]}"; do printf '%s\t%s\n' "$src_root" "$_rel"; done } | sort -u > "$_marker_tmp" - mv "$_marker_tmp" "$dist/.hawk-dist" - echo "$dist/$entry_rel" + mv "$_marker_tmp" "$stage_root/.hawk-dist" + _current_tmp="$(mktemp "$dist/.current.XXXXXX")" + rm -f "$_current_tmp" + ln -s "${stage_root#"$dist/"}" "$_current_tmp" + mv -f -h "$_current_tmp" "$dist/current" + _root_marker_tmp="$(mktemp "$dist/.hawk-dist.XXXXXX")" + cp "$stage_root/.hawk-dist" "$_root_marker_tmp" + mv -f "$_root_marker_tmp" "$dist/.hawk-dist" + for ((i = 0; i < ${#_pub_rel[@]}; i++)); do + _rel="${_pub_rel[$i]}" + _stable="$dist/${namespace:+$namespace/}$_rel" + _compat="$dist/$_rel" + mkdir -p "$(dirname "$_stable")" "$(dirname "$_compat")" + if [[ -e "$_stable" && ! -L "$_stable" ]]; then + echo "[hawk-libs] desugar failed: stable output is not a symlink: $_stable" >&2 + exit 1 + fi + if [[ ! -e "$_stable" && ! -L "$_stable" ]]; then + ln -s "$runtime_ns/$_rel" "$_stable" + fi + if [[ "$_compat" != "$_stable" && ! -e "$_compat" && ! -L "$_compat" ]]; then + ln -s "$runtime_ns/$_rel" "$_compat" + fi + done + echo "$dist/${namespace:+$namespace/}$entry_rel" ;; *) echo "[hawk-libs] unknown subcommand: $subcmd" >&2 diff --git a/tests/unit/desugar/run.sh b/tests/unit/desugar/run.sh index 3c115cb6..3885381f 100755 --- a/tests/unit/desugar/run.sh +++ b/tests/unit/desugar/run.sh @@ -22,15 +22,28 @@ trap 'rm -rf "$TMP"' EXIT # --- 再帰 desugar + @include 書き換え + 実行 --- dist="$TMP/dist1" out=$(HAWK_DIST="$dist" "$LIBS" desugar "$FIX/proj/main.awk") -if [[ "$out" == "$dist/main.awk" ]]; then ok "entry_path_echoed"; else ng "entry_path_echoed" "$out"; fi +if [[ "$out" == "$dist/tests/unit/desugar/fixtures/proj/main.awk" ]]; then ok "entry_path_echoed"; else ng "entry_path_echoed" "$out"; fi if [[ -f "$dist/app/page.awk" ]]; then ok "included_file_mirrored"; else ng "included_file_mirrored"; fi if [[ -f "$dist/app/sub/util.awk" ]]; then ok "nested_include_mirrored"; else ng "nested_include_mirrored"; fi -if grep -qF "@include \"$dist/app/page.awk\"" "$dist/main.awk"; then ok "include_rewritten"; else ng "include_rewritten"; fi -if grep -qF "@include \"$dist/app/sub/util.awk\"" "$dist/app/page.awk"; then ok "nested_include_rewritten"; else ng "nested_include_rewritten"; fi +if grep -qF "@include \"$dist/current/" "$dist/main.awk" && grep -qF "app/page.awk\"" "$dist/main.awk"; then ok "include_rewritten"; else ng "include_rewritten"; fi +if grep -qF "@include \"$dist/current/" "$dist/app/page.awk" && grep -qF "app/sub/util.awk\"" "$dist/app/page.awk"; then ok "nested_include_rewritten"; else ng "nested_include_rewritten"; fi if ! grep -q "let " "$dist/app/page.awk"; then ok "included_file_desugared"; else ng "included_file_desugared"; fi run_out=$(gawk -f "$dist/main.awk" Str { return "shared" }\n' > "$shared/types.awk" +printf '@include "types.awk"\nfunction main() -> Response { let u: User = { name: get_name() }; return ctx.res.text(get_name()) }\n' > "$shared/main.awk" +if HAWK_DIST="$TMP/distshared" "$LIBS" desugar "$shared/main.awk" >/dev/null; then ok "shared_declarations_compile"; else ng "shared_declarations_compile"; fi + +gen="$TMP/generation" +HAWK_DIST="$gen" "$LIBS" desugar "$FIX/solo.awk" >/dev/null +old_gen=$(readlink "$gen/current") +HAWK_DIST="$gen" "$LIBS" desugar "$FIX/proj/main.awk" >/dev/null +new_gen=$(readlink "$gen/current") +if [[ "$old_gen" != "$new_gen" && $(find -L "$gen/current" -type f -name main.awk | wc -l | tr -d ' ') -eq 1 && $(find "$gen/$old_gen" -type f -name solo.awk | wc -l | tr -d ' ') -eq 1 ]]; then ok "generation_switch_is_atomic"; else ng "generation_switch_is_atomic"; fi + # --- entry に戻る循環は exit 1 (gawk が実行時 fatal になるため desugar 時点で拒否) --- dist="$TMP/dist2" set +e @@ -71,7 +84,7 @@ fi # --- "./x.awk" 形式の include は正規化されて dist に書き換わる --- dist="$TMP/distds" if HAWK_DIST="$dist" "$LIBS" desugar "$FIX/dotslash/main.awk" >/dev/null; then - if grep -qF "@include \"$dist/x.awk\"" "$dist/main.awk"; then ok "dotslash_include_normalized"; else ng "dotslash_include_normalized"; fi + if grep -qF "@include \"$dist/current/" "$dist/main.awk" && grep -qF "x.awk\"" "$dist/main.awk"; then ok "dotslash_include_normalized"; else ng "dotslash_include_normalized"; fi run_out=$(gawk -f "$dist/main.awk" /dev/null if HAWK_DIST="$dist" "$LIBS" desugar "$FIX/proj/main.awk" >/dev/null; then ok "rerun_over_marker_ok"; else ng "rerun_over_marker_ok" "exit != 0"; fi -# --- 異なる source の同名 basename entry (dist/main.awk が衝突) は marker に別 source -# として上書き拒否され、先に publish した成果物が生き残る --- +# --- 異なる source の同名 basename entry は namespace で分離される --- appsA="$TMP/appsA" appsB="$TMP/appsB" mkdir -p "$appsA" "$appsB" @@ -375,16 +387,12 @@ printf 'BEGIN { print "from-a" }\n' > "$appsA/main.awk" printf 'BEGIN { print "from-b" }\n' > "$appsB/main.awk" dist="$TMP/distcs" HAWK_DIST="$dist" "$LIBS" desugar "$appsA/main.awk" >/dev/null -before_hash=$(cksum < "$dist/main.awk") -set +e -err=$(HAWK_DIST="$dist" "$LIBS" desugar "$appsB/main.awk" 2>&1 >/dev/null) -st=$? -set -e -after_hash=$(cksum < "$dist/main.awk") -if [[ "$st" -eq 1 && "$err" == *"refusing to overwrite"* && "$before_hash" == "$after_hash" ]]; then - ok "cross_source_same_basename_rejected" +HAWK_DIST="$dist" "$LIBS" desugar "$appsB/main.awk" >/dev/null +entries=$(find "$dist/.hawk-generations" -type f -name main.awk | wc -l | tr -d ' ') +if [[ "$entries" -eq 2 ]] && grep -R -q "from-a" "$dist/.hawk-generations" && grep -R -q "from-b" "$dist/.hawk-generations"; then + ok "cross_source_same_basename_isolated" else - ng "cross_source_same_basename_rejected" "exit=$st: $err" + ng "cross_source_same_basename_isolated" "entries=$entries" fi # --- marker は publish 済み rel のみ信頼し、同じ dist 配下の無関係な既存ファイルは @@ -393,17 +401,10 @@ fi srcsub2="$TMP/srcsub2" mkdir -p "$srcsub2/sub" cp "$FIX/solo.awk" "$srcsub2/main.awk" -HAWK_DIST="$srcsub2/sub" "$LIBS" desugar "$srcsub2/main.awk" >/dev/null -printf 'BEGIN { print "other-source" }\n' > "$srcsub2/sub/other.awk" -before_hash=$(cksum < "$srcsub2/sub/other.awk") -cp "$FIX/solo.awk" "$srcsub2/other.awk" -set +e -err=$(HAWK_DIST="$srcsub2/sub" "$LIBS" desugar "$srcsub2/other.awk" 2>&1 >/dev/null) -st=$? -set -e -if [[ "$st" -eq 1 && "$err" == *"refusing to overwrite"* ]]; then ok "marker_does_not_bless_unrelated_files"; else ng "marker_does_not_bless_unrelated_files" "exit=$st: $err"; fi -after_hash=$(cksum < "$srcsub2/sub/other.awk") -if [[ "$before_hash" == "$after_hash" ]]; then ok "marker_does_not_bless_unrelated_files_untouched"; else ng "marker_does_not_bless_unrelated_files_untouched" "source file was modified"; fi +before_hash=$(cksum < "$srcsub2/main.awk") +if HAWK_DIST="$srcsub2/sub" "$LIBS" desugar "$srcsub2/main.awk" >/dev/null; then ok "source_tree_dist_published"; else ng "source_tree_dist_published"; fi +after_hash=$(cksum < "$srcsub2/main.awk") +if [[ "$before_hash" == "$after_hash" && $(find -L "$srcsub2/sub/current" -type f -name main.awk | wc -l | tr -d ' ') -eq 1 ]]; then ok "source_tree_dist_source_untouched"; else ng "source_tree_dist_source_untouched"; fi # --- include 名に glob 文字が含まれても、_seen の未クォート展開で # カレントディレクトリの同名ファイルと誤って alias 判定されない --- From 773cecbc30c3d9301e9bfd62563dc11001865ad3 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Wed, 15 Jul 2026 10:55:20 +0900 Subject: [PATCH 02/20] =?UTF-8?q?fix:=20=E4=B8=96=E4=BB=A3=E5=88=87?= =?UTF-8?q?=E6=9B=BF=E3=82=92=E7=A7=BB=E6=A4=8D=E5=8F=AF=E8=83=BD=E3=81=AB?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BSD 専用の mv -h に依存すると GNU CI で current symlink を切り替えられないため、実行時に mv -h を確認して未対応環境では mv -T に切り替える。相対 dist の index 出力も物理パスへ固定し、worktree の cwd 移動で失敗しないようにする。 --- libexec/hawk-libs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/libexec/hawk-libs b/libexec/hawk-libs index 9d37ab10..5b66259d 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -454,7 +454,9 @@ case "$subcmd" in entry_rel="$(basename "$src")" _index_one "$entry_rel" - _index_tmp="$(mktemp "$dist/.hawk-index.XXXXXX")" + # dsl/index.awk は HAWK_LIB へ cd して実行するため、相対 dist をそのまま渡すと + # work/dist ではなくリポジトリ直下へ出力しようとして失敗する。 + _index_tmp="$(mktemp "$dist_phys/.hawk-index.XXXXXX")" _scratch_arr+=("$_index_tmp") _index_list="" for _index_file in "${_index_files[@]}"; do @@ -498,7 +500,16 @@ case "$subcmd" in _current_tmp="$(mktemp "$dist/.current.XXXXXX")" rm -f "$_current_tmp" ln -s "${stage_root#"$dist/"}" "$_current_tmp" - mv -f -h "$_current_tmp" "$dist/current" + # BSD mv の -h と GNU mv の -T は同じ「symlink 自体を置換」だが、片方しかない。 + # 実行時に -h の実移動を確認し、非対応なら -T へ切り替える(Why not: rm+mv は非 atomic)。 + if mv -f -h "$_current_tmp" "$dist/current" 2>/dev/null && [[ ! -e "$_current_tmp" && ! -L "$_current_tmp" ]]; then + : + elif mv -f -T "$_current_tmp" "$dist/current"; then + : + else + echo "[hawk-libs] desugar failed: no portable symlink replacement command (mv -h or mv -T)" >&2 + exit 1 + fi _root_marker_tmp="$(mktemp "$dist/.hawk-dist.XXXXXX")" cp "$stage_root/.hawk-dist" "$_root_marker_tmp" mv -f "$_root_marker_tmp" "$dist/.hawk-dist" From 97688e631a4c7a931fb20d54cdeeb177259c3ed0 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Wed, 15 Jul 2026 16:07:25 +0900 Subject: [PATCH 03/20] =?UTF-8?q?fix:=20=E4=B8=96=E4=BB=A3=E5=88=87?= =?UTF-8?q?=E6=9B=BF=E5=BE=8C=E3=82=82=20namespace=20=E3=82=92=E4=BF=9D?= =?UTF-8?q?=E6=8C=81=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit current の全体切替で既存 entry を欠落させないため前世代を hardlink 継承し、共通の親検査で stable/compat symlink 作成時の dist 外書込みも防ぐ。 --- libexec/hawk-libs | 126 +++++++++++++++++++++++++------------- tests/unit/desugar/run.sh | 42 ++++++++++--- 2 files changed, 117 insertions(+), 51 deletions(-) diff --git a/libexec/hawk-libs b/libexec/hawk-libs index 5b66259d..0c3bcdb9 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -133,20 +133,89 @@ case "$subcmd" in echo "[hawk-libs] desugar failed: refusing to overwrite source tree (dist and source trees overlap)" >&2 exit 1 fi - mkdir -p "$dist/.hawk-generations" - stage_root="$(mktemp -d "$dist/.hawk-generations/gen.XXXXXX")" - if [[ "$dist" = /* ]]; then runtime_root="$dist/current"; else runtime_root="$dist_phys/current"; fi - runtime_ns="$runtime_root" - [[ -n "$namespace" ]] && runtime_ns="$runtime_ns/$namespace" - _seen=" " - # scratch と publish キューは配列で持つ - # (空白入りパスが word splitting で別引数に化けるのを防ぐ) + _scratch_arr=() _cleanup_scratches() { local f for f in "${_scratch_arr[@]}"; do rm -f "$f"; done } trap _cleanup_scratches EXIT + + _prepare_dist_parent() { + local parent="$1" rel probe part parent_phys + probe="$dist" + if [[ "$parent" == "$dist"/* ]]; then + rel="${parent#"$dist"/}" + while [[ -n "$rel" ]]; do + if [[ "$rel" == */* ]]; then + part="${rel%%/*}" + rel="${rel#*/}" + else + part="$rel" + rel= + fi + [[ -n "$part" ]] || continue + probe="$probe/$part" + if [[ -L "$probe" ]]; then + echo "[hawk-libs] desugar failed: output directory escapes dist via symlink: $probe" >&2 + exit 1 + fi + [[ -e "$probe" ]] || break + done + elif [[ "$parent" != "$dist" ]]; then + echo "[hawk-libs] desugar failed: output directory escapes dist: $parent" >&2 + exit 1 + fi + mkdir -p "$parent" + parent_phys="$(cd "$parent" && pwd -P)" + if [[ "$parent_phys/" != "$dist_phys/"* ]]; then + echo "[hawk-libs] desugar failed: output directory escapes dist via symlink: $parent_phys" >&2 + exit 1 + fi + } + + _prepare_dist_parent "$dist/.hawk-generations" + stage_root="$(mktemp -d "$dist/.hawk-generations/gen.XXXXXX")" + if [[ -e "$dist/current" || -L "$dist/current" ]]; then + if [[ ! -L "$dist/current" ]]; then + echo "[hawk-libs] desugar failed: $dist/current is not a symlink" >&2 + exit 1 + fi + previous_rel="$(readlink "$dist/current")" + case "$previous_rel" in + .hawk-generations/gen.*/*|/*|../*|*/../*|"") + echo "[hawk-libs] desugar failed: invalid current generation: $previous_rel" >&2 + exit 1 + ;; + .hawk-generations/gen.*) ;; + *) + echo "[hawk-libs] desugar failed: invalid current generation: $previous_rel" >&2 + exit 1 + ;; + esac + previous_root="$dist/$previous_rel" + if [[ -L "$previous_root" || ! -d "$previous_root" ]]; then + echo "[hawk-libs] desugar failed: current generation not found: $previous_root" >&2 + exit 1 + fi + previous_list="$(mktemp "$dist/.hawk-carry.XXXXXX")" + _scratch_arr+=("$previous_list") + if ! find "$previous_root" -type f -print0 > "$previous_list"; then + echo "[hawk-libs] desugar failed: cannot list current generation: $previous_root" >&2 + exit 1 + fi + while IFS= read -r -d '' previous_file; do + previous_file_rel="${previous_file#"$previous_root"/}" + carried_file="$stage_root/$previous_file_rel" + _prepare_dist_parent "$(dirname "$carried_file")" + ln "$previous_file" "$carried_file" + done < "$previous_list" + rm -f "$previous_list" + fi + if [[ "$dist" = /* ]]; then runtime_root="$dist/current"; else runtime_root="$dist_phys/current"; fi + runtime_ns="$runtime_root" + [[ -n "$namespace" ]] && runtime_ns="$runtime_ns/$namespace" + _seen=" " _pub_tmp=() _pub_out=() _pub_rel=() @@ -195,7 +264,7 @@ case "$subcmd" in # rel: src_root からの相対パス。desugar して dist/rel に出力し、 # src_root 相対で実在する @include を dist 接頭に書き換えて再帰する。 _desugar_one() { - local rel="$1" in out out_parent out_rel probe part tmp inc incs prior + local rel="$1" in out out_parent tmp inc incs prior case "$_seen" in *" $rel "*) return 0 ;; esac case "$rel" in ../*|*/../*) @@ -290,39 +359,8 @@ case "$subcmd" in exit 1 fi fi - # mkdir -p は既存の symlink を辿るため、先に dist 配下の既存コンポーネントを - # 検査する。例えば dist/app -> 外部 の状態で app/sub を作ると、dist 外に - # sub/ が作られてから下の実パス検査に到達してしまう。 out_parent="$(dirname "$out")" - probe="$dist" - if [[ "$out_parent" == "$dist"/* ]]; then - out_rel="${out_parent#"$dist"/}" - while [[ -n "$out_rel" ]]; do - if [[ "$out_rel" == */* ]]; then - part="${out_rel%%/*}" - out_rel="${out_rel#*/}" - else - part="$out_rel" - out_rel= - fi - [[ -n "$part" ]] || continue - probe="$probe/$part" - if [[ -L "$probe" ]]; then - echo "[hawk-libs] desugar failed: output directory escapes dist via symlink: $probe" >&2 - exit 1 - fi - [[ -e "$probe" ]] || break - done - fi - mkdir -p "$out_parent" - # dist 配下のディレクトリが外部への symlink に差し替わっていると、mkdir -p はそれを - # 素通りし、後続の mv は symlink 先 (dist の外) に publish してしまう。 - # 出力先の親を実パスまで解決して dist の実パス配下であることを都度確認する - out_parent_phys="$(cd "$(dirname "$out")" && pwd -P)" - if [[ "$out_parent_phys/" != "$dist_phys/"* ]]; then - echo "[hawk-libs] desugar failed: output directory escapes dist via symlink: $out_parent_phys" >&2 - exit 1 - fi + _prepare_dist_parent "$out_parent" # 稼働中の hawk-serve が dist の成果物を再ロードしうるため、 # scratch (mktemp) 上で desugar と書き換えを済ませてから # mv でアトミックに配置する。scratch を mktemp にすることで @@ -497,6 +535,11 @@ case "$subcmd" in for _rel in "${_pub_rel[@]}"; do printf '%s\t%s\n' "$src_root" "$_rel"; done } | sort -u > "$_marker_tmp" mv "$_marker_tmp" "$stage_root/.hawk-dist" + for ((i = 0; i < ${#_pub_rel[@]}; i++)); do + _rel="${_pub_rel[$i]}" + _prepare_dist_parent "$(dirname "$dist/${namespace:+$namespace/}$_rel")" + _prepare_dist_parent "$(dirname "$dist/$_rel")" + done _current_tmp="$(mktemp "$dist/.current.XXXXXX")" rm -f "$_current_tmp" ln -s "${stage_root#"$dist/"}" "$_current_tmp" @@ -517,7 +560,6 @@ case "$subcmd" in _rel="${_pub_rel[$i]}" _stable="$dist/${namespace:+$namespace/}$_rel" _compat="$dist/$_rel" - mkdir -p "$(dirname "$_stable")" "$(dirname "$_compat")" if [[ -e "$_stable" && ! -L "$_stable" ]]; then echo "[hawk-libs] desugar failed: stable output is not a symlink: $_stable" >&2 exit 1 diff --git a/tests/unit/desugar/run.sh b/tests/unit/desugar/run.sh index 3885381f..8f7cb181 100755 --- a/tests/unit/desugar/run.sh +++ b/tests/unit/desugar/run.sh @@ -380,20 +380,27 @@ HAWK_DIST="$dist" "$LIBS" desugar "$FIX/proj/main.awk" >/dev/null if HAWK_DIST="$dist" "$LIBS" desugar "$FIX/proj/main.awk" >/dev/null; then ok "rerun_over_marker_ok"; else ng "rerun_over_marker_ok" "exit != 0"; fi # --- 異なる source の同名 basename entry は namespace で分離される --- -appsA="$TMP/appsA" -appsB="$TMP/appsB" -mkdir -p "$appsA" "$appsB" -printf 'BEGIN { print "from-a" }\n' > "$appsA/main.awk" -printf 'BEGIN { print "from-b" }\n' > "$appsB/main.awk" +multi_entry="$TMP/multi-entry" +mkdir -p "$multi_entry/apps/a" "$multi_entry/apps/b" +printf 'BEGIN { print "from-a" }\n' > "$multi_entry/apps/a/main.awk" +printf 'BEGIN { print "from-b" }\n' > "$multi_entry/apps/b/main.awk" dist="$TMP/distcs" -HAWK_DIST="$dist" "$LIBS" desugar "$appsA/main.awk" >/dev/null -HAWK_DIST="$dist" "$LIBS" desugar "$appsB/main.awk" >/dev/null -entries=$(find "$dist/.hawk-generations" -type f -name main.awk | wc -l | tr -d ' ') -if [[ "$entries" -eq 2 ]] && grep -R -q "from-a" "$dist/.hawk-generations" && grep -R -q "from-b" "$dist/.hawk-generations"; then +( cd "$multi_entry" && HAWK_DIST="$dist" "$LIBS_ABS" desugar apps/a/main.awk >/dev/null ) +first_a="$dist/$(readlink "$dist/current")/apps/a/main.awk" +( cd "$multi_entry" && HAWK_DIST="$dist" "$LIBS_ABS" desugar apps/b/main.awk >/dev/null ) +entries=$(find -L "$dist/current" -type f -name main.awk | wc -l | tr -d ' ') +if [[ "$entries" -eq 2 ]] && grep -q "from-a" "$dist/apps/a/main.awk" && grep -q "from-b" "$dist/apps/b/main.awk"; then ok "cross_source_same_basename_isolated" else ng "cross_source_same_basename_isolated" "entries=$entries" fi +run_a=$(gawk -f "$dist/apps/a/main.awk" "$stable_parent_src/app/main.awk" +ln -s "$stable_parent_outside" "$stable_parent_dist/app" +set +e +err=$(cd "$stable_parent_src" && HAWK_DIST="$stable_parent_dist" "$LIBS_ABS" desugar app/main.awk 2>&1 >/dev/null) +st=$? +set -e +if [[ "$st" -eq 1 && ! -e "$stable_parent_outside/main.awk" && ! -e "$stable_parent_dist/current" ]]; then + ok "stable_symlink_parent_escape_rejected" +else + ng "stable_symlink_parent_escape_rejected" "exit=$st, external=$([[ -e "$stable_parent_outside/main.awk" ]] && echo yes || echo no): $err" +fi + printf "\n%d passed, %d failed\n" "$PASS" "$FAIL" [[ $FAIL -eq 0 ]] From ae84f92bdc6668db59aebdf8ec217e415d977a0e Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Wed, 15 Jul 2026 16:27:33 +0900 Subject: [PATCH 04/20] =?UTF-8?q?fix:=20desugar=20=E3=81=AE=20Bash=203.2?= =?UTF-8?q?=20=E4=BA=92=E6=8F=9B=E6=80=A7=E3=82=92=E6=88=BB=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit check・emit・serve の共通経路に Bash 4 要件を波及させないため、index の訪問済み集合を連想配列から既存の文字列照合へ戻す。 --- libexec/hawk-libs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libexec/hawk-libs b/libexec/hawk-libs index 0c3bcdb9..2af88b78 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -224,11 +224,11 @@ case "$subcmd" in # を hard link 経由で見逃す。詳細は下の post-closure 検査を参照) _abs_ext=() - declare -A _index_seen=() + _index_seen=" " _index_files=() _index_one() { local rel="$1" inc p - [[ -n "${_index_seen[$rel]+x}" ]] && return 0 + case "$_index_seen" in *" $rel "*) return 0 ;; esac case "$rel" in ../*|*/../*) echo "[hawk-libs] desugar failed: include escapes project root via '..': $rel" >&2 @@ -243,7 +243,7 @@ case "$subcmd" in exit 1 ;; esac - _index_seen["$rel"]=1 + _index_seen="$_index_seen$rel " p="$src_root/$rel" [[ -f "$p" ]] || return 0 _index_files+=("$p") From 75d11690342e6ac71f4447888b91cde1e6833229 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Wed, 15 Jul 2026 17:04:17 +0900 Subject: [PATCH 05/20] =?UTF-8?q?fix:=20=E5=85=B1=E6=9C=89=E5=9E=8B?= =?UTF-8?q?=E6=A4=9C=E6=9F=BB=E3=81=A8=20dist=20=E6=AD=A3=E8=A6=8F?= =?UTF-8?q?=E5=8C=96=E3=82=92=E7=9B=B4=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit include 先の関数シグネチャを raw AWK 呼び出しでも検査し、末尾スラッシュ付き HAWK_DIST を正常に扱うため。 --- dsl/desugar.awk | 5 ++++- dsl/lex.awk | 15 +++++++++++---- libexec/hawk-libs | 1 + tests/unit/desugar/run.sh | 14 ++++++++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) diff --git a/dsl/desugar.awk b/dsl/desugar.awk index b46e71ed..cfb9d3ca 100644 --- a/dsl/desugar.awk +++ b/dsl/desugar.awk @@ -14,8 +14,8 @@ BEGIN { V2_SRC = (ARGC > 1) ? ARGV[1] : "/dev/stdin" V2_ERRORS = 0 - v2_lex(V2_SRC) v2_merge_shared_pre_rpn() + v2_lex(V2_SRC) if (V2_DUMP == "lex") { v2_dump_lex(); exit(V2_ERRORS ? 1 : 0) } v2_rpn() if (V2_DUMP == "rpn") { v2_dump_rpn(); exit(V2_ERRORS ? 1 : 0) } @@ -29,11 +29,14 @@ BEGIN { } function v2_merge_shared_pre_rpn( k, p) { + for (k in V2_SHARED_ALIAS) ALIAS[k] = V2_SHARED_ALIAS[k] + for (k in V2_SHARED_SIG) SIG[k] = V2_SHARED_SIG[k] for (k in V2_SHARED_RECORD_TYPE) V2_RECORD_TYPE[k] = 1 for (k in V2_SHARED_RECORD_FIELDS) { split(k, p, SUBSEP) V2_RECORD_FIELDS[p[1], p[2]] = V2_SHARED_RECORD_FIELDS[k] } + for (k in V2_SHARED_RAW_FUNC) V2_RAW_FUNC[k] = 1 } function v2_dump_lex( i) { for (i = 1; i <= TOK["n"]; i++) printf "%d\t%s\t%d\t%d\t%s\n", i, TOK[i,"kind"], TOK[i,"line"], TOK[i,"col"], TOK[i,"text"] } diff --git a/dsl/lex.awk b/dsl/lex.awk index 102e256b..ad96e829 100644 --- a/dsl/lex.awk +++ b/dsl/lex.awk @@ -63,7 +63,7 @@ function v2_strip_str_comment(line, i, ch, out, instr) { } # DSL 構文を含む行か判定する述語 -# type NAME = ... 宣言をファイル全体から事前収集し、型注釈検出(v2_is_dsl の +# type NAME = ... 宣言を共有 index とファイル全体から事前収集し、型注釈検出(v2_is_dsl の # `: TYPE` 判定)に使う代替パターンを組み立てる(CE)。組込み型集合に無い # ユーザー定義エイリアスだけが注釈に使われている関数(`type Status = Int|Str` # + `function f(x: Status) {...}`)は、この事前収集なしでは検出できず素の awk @@ -72,8 +72,11 @@ function v2_strip_str_comment(line, i, ch, out, instr) { # 誤検出リスク: エイリアス名が偶然コメント・文字列中の識別子と一致するケースは # 元々の固定型リストと同じ理由で許容する(v2_strip_str_comment 済みの行に対して # のみ判定するため文字列内は対象外)。 -function v2_collect_type_aliases(nlines, i, s, m, names, n) { +function v2_collect_type_aliases(nlines, i, s, m, names, n, k) { names = ""; n = 0 + for (k in V2_SHARED_ALIAS) { + if (!(k in V2_DSL_ALIAS)) { V2_DSL_ALIAS[k] = 1; names = names (n++ ? "|" : "") k } + } for (i = 1; i <= nlines; i++) { s = v2_strip_str_comment(V2_RAWLINE[i]) if (match(s, /(^|[^[:alnum:]_])type[[:space:]]+([A-Za-z_][A-Za-z0-9_]*)[[:space:]]*=/, m)) { @@ -83,14 +86,18 @@ function v2_collect_type_aliases(nlines, i, s, m, names, n) { V2_ALIAS_TYPEANN_RE = names } -# 戻り値型注釈(`-> RETTYPE`)を持つユーザー定義関数名をファイル全体から +# 戻り値型注釈(`-> RETTYPE`)を持つユーザー定義関数名を共有 index とファイル全体から # 事前収集する(review EQ)。DSL マーカーを一切持たない関数本体 # (`function handler() { normalize(123) }`)でも、本体が既知の注釈付き # 関数を呼び出しているだけなら v1 は引数型検査を行う。v2 は関数ヘッダに # DSL 構文が無いと本体全体を素通し(RAWLINE)扱いにするため、この呼び出し # だけが v2_check_calls の CALL 検査を経由せず引数型不一致を見逃していた。 -function v2_collect_annotated_funcs(nlines, i, s, m, names, n) { +function v2_collect_annotated_funcs(nlines, i, s, m, names, n, k, p) { names = ""; n = 0 + for (k in V2_SHARED_SIG) { + split(k, p, SUBSEP) + if (!(p[1] in V2_DSL_FUNCNAME)) { V2_DSL_FUNCNAME[p[1]] = 1; names = names (n++ ? "|" : "") p[1] } + } for (i = 1; i <= nlines; i++) { s = v2_strip_str_comment(V2_RAWLINE[i]) # 戻り値注釈 (-> RETTYPE) を持つ関数 diff --git a/libexec/hawk-libs b/libexec/hawk-libs index 2af88b78..2b2281f0 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -53,6 +53,7 @@ case "$subcmd" in exit 1 fi dist="${HAWK_DIST:-dist}" + while [[ "$dist" == */ ]]; do dist="${dist%/}"; done mkdir -p "$dist" # marker の異常 (symlink / 非 regular file / 書込不可) は publish 後の # 作成・更新で失敗し「exit 1 なのに成果物は更新済み」になるため、 diff --git a/tests/unit/desugar/run.sh b/tests/unit/desugar/run.sh index 8f7cb181..bec3612d 100755 --- a/tests/unit/desugar/run.sh +++ b/tests/unit/desugar/run.sh @@ -37,6 +37,16 @@ printf 'type User = { name: Str }\nfunction get_name() -> Str { return "shared" printf '@include "types.awk"\nfunction main() -> Response { let u: User = { name: get_name() }; return ctx.res.text(get_name()) }\n' > "$shared/main.awk" if HAWK_DIST="$TMP/distshared" "$LIBS" desugar "$shared/main.awk" >/dev/null; then ok "shared_declarations_compile"; else ng "shared_declarations_compile"; fi +shared_sig="$TMP/shared-sig" +mkdir -p "$shared_sig" +printf 'function normalize(x: Str) -> Str { return x }\n' > "$shared_sig/lib.awk" +printf '@include "lib.awk"\nfunction caller() { return normalize(123) }\nBEGIN { caller() }\n' > "$shared_sig/main.awk" +set +e +err=$(HAWK_DIST="$TMP/distsharedsig" "$LIBS" desugar "$shared_sig/main.awk" 2>&1 >/dev/null) +st=$? +set -e +if [[ "$st" -ne 0 && "$err" == *"normalize argument 1 expects Str, got Int"* ]]; then ok "shared_signature_checks_raw_caller"; else ng "shared_signature_checks_raw_caller" "exit=$st: $err"; fi + gen="$TMP/generation" HAWK_DIST="$gen" "$LIBS" desugar "$FIX/solo.awk" >/dev/null old_gen=$(readlink "$gen/current") @@ -441,6 +451,10 @@ cp "$FIX/solo.awk" "$work/main.awk" out=$(cd "$work" && "$LIBS_ABS" desugar main.awk) if [[ "$out" == "dist/main.awk" && -f "$work/dist/main.awk" ]]; then ok "default_dist_dir"; else ng "default_dist_dir" "$out"; fi +# --- HAWK_DIST の末尾 / は親ディレクトリ検査前に正規化する --- +dist="$TMP/dist-trailing/" +if HAWK_DIST="$dist" "$LIBS" desugar "$FIX/solo.awk" >/dev/null && [[ -f "${dist%/}/solo.awk" ]]; then ok "hawk_dist_trailing_slash_normalized"; else ng "hawk_dist_trailing_slash_normalized"; fi + # --- 入力ファイル不在は従来どおり exit 1 --- set +e "$LIBS" desugar "$TMP/nope.awk" >/dev/null 2>&1 From 5a9e1612a745b9b92a6576de97e271c88a67d196 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Wed, 15 Jul 2026 17:48:07 +0900 Subject: [PATCH 06/20] =?UTF-8?q?fix:=20desugar=20=E3=81=AE=E5=85=AC?= =?UTF-8?q?=E9=96=8B=E6=95=B4=E5=90=88=E6=80=A7=E3=82=92=E5=AE=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 予約 namespace・改ざん済み stable link・record/alias 型名衝突で壊れた成果物を公開しないため。 --- dsl/index.awk | 12 ++++++++- libexec/hawk-libs | 56 ++++++++++++++++++++++++++------------- tests/unit/desugar/run.sh | 39 +++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 20 deletions(-) diff --git a/dsl/index.awk b/dsl/index.awk index 0edf9867..8874be36 100644 --- a/dsl/index.awk +++ b/dsl/index.awk @@ -65,11 +65,21 @@ BEGIN { exit 1 } } - for (k in V2_INDEX_RUN_RECORDS) V2_INDEX_RECORD_NAMES[k] = 1 + for (k in V2_INDEX_RUN_RECORDS) { + if (k in V2_INDEX_ALIAS) { + print "[hawk-libs] index failed: type name is both record and alias: " k > "/dev/stderr" + exit 1 + } + V2_INDEX_RECORD_NAMES[k] = 1 + } v2_parse() v2_collect(1) v2_collect_raw_funcs() for (k in ALIAS) { + if (k in V2_INDEX_RECORD_NAMES) { + print "[hawk-libs] index failed: type name is both record and alias: " k > "/dev/stderr" + exit 1 + } if ((k in V2_INDEX_ALIAS) && V2_INDEX_ALIAS[k] != ALIAS[k]) { print "[hawk-libs] index failed: conflicting type alias definition: " k > "/dev/stderr" exit 1 diff --git a/libexec/hawk-libs b/libexec/hawk-libs index 2b2281f0..294eee86 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -119,6 +119,12 @@ case "$subcmd" in exit 1 ;; esac + case "$entry_key" in + .hawk-dist|.hawk-dist/*|.hawk-generations|.hawk-generations/*|current|current/*) + echo "[hawk-libs] desugar failed: reserved entry namespace: $entry_key" >&2 + exit 1 + ;; + esac if [[ "$entry_key" == */* ]]; then namespace="${entry_key%/*}" else @@ -142,6 +148,33 @@ case "$subcmd" in } trap _cleanup_scratches EXIT + _replace_symlink_atomically() { + local tmp="$1" link="$2" + if mv -f -h "$tmp" "$link" 2>/dev/null && [[ ! -e "$tmp" && ! -L "$tmp" ]]; then + return 0 + fi + if mv -f -T "$tmp" "$link"; then return 0; fi + echo "[hawk-libs] desugar failed: no portable symlink replacement command (mv -h or mv -T)" >&2 + exit 1 + } + + _refresh_output_link() { + local link="$1" target="$2" actual tmp + if [[ -e "$link" && ! -L "$link" ]]; then + echo "[hawk-libs] desugar failed: stable output is not a symlink: $link" >&2 + exit 1 + fi + if [[ -L "$link" ]]; then + actual="$(readlink "$link")" + [[ "$actual" == "$target" ]] && return 0 + fi + tmp="$(mktemp "$(dirname "$link")/.hawk-link.XXXXXX")" + _scratch_arr+=("$tmp") + rm -f "$tmp" + ln -s "$target" "$tmp" + _replace_symlink_atomically "$tmp" "$link" + } + _prepare_dist_parent() { local parent="$1" rel probe part parent_phys probe="$dist" @@ -315,7 +348,7 @@ case "$subcmd" in echo "[hawk-libs] desugar failed: output directory escapes dist via symlink: $compat_parent_phys" >&2 exit 1 fi - if [[ -d "$stable" || ( -e "$stable" && ! -L "$stable" ) || -d "$compat" ]]; then + if [[ -d "$stable" || ( -e "$stable" && ! -L "$stable" ) || -d "$compat" || ( -e "$compat" && ! -L "$compat" ) ]]; then echo "[hawk-libs] desugar failed: output path is not replaceable: $stable" >&2 exit 1 fi @@ -546,14 +579,7 @@ case "$subcmd" in ln -s "${stage_root#"$dist/"}" "$_current_tmp" # BSD mv の -h と GNU mv の -T は同じ「symlink 自体を置換」だが、片方しかない。 # 実行時に -h の実移動を確認し、非対応なら -T へ切り替える(Why not: rm+mv は非 atomic)。 - if mv -f -h "$_current_tmp" "$dist/current" 2>/dev/null && [[ ! -e "$_current_tmp" && ! -L "$_current_tmp" ]]; then - : - elif mv -f -T "$_current_tmp" "$dist/current"; then - : - else - echo "[hawk-libs] desugar failed: no portable symlink replacement command (mv -h or mv -T)" >&2 - exit 1 - fi + _replace_symlink_atomically "$_current_tmp" "$dist/current" _root_marker_tmp="$(mktemp "$dist/.hawk-dist.XXXXXX")" cp "$stage_root/.hawk-dist" "$_root_marker_tmp" mv -f "$_root_marker_tmp" "$dist/.hawk-dist" @@ -561,16 +587,8 @@ case "$subcmd" in _rel="${_pub_rel[$i]}" _stable="$dist/${namespace:+$namespace/}$_rel" _compat="$dist/$_rel" - if [[ -e "$_stable" && ! -L "$_stable" ]]; then - echo "[hawk-libs] desugar failed: stable output is not a symlink: $_stable" >&2 - exit 1 - fi - if [[ ! -e "$_stable" && ! -L "$_stable" ]]; then - ln -s "$runtime_ns/$_rel" "$_stable" - fi - if [[ "$_compat" != "$_stable" && ! -e "$_compat" && ! -L "$_compat" ]]; then - ln -s "$runtime_ns/$_rel" "$_compat" - fi + _refresh_output_link "$_stable" "$runtime_ns/$_rel" + [[ "$_compat" == "$_stable" ]] || _refresh_output_link "$_compat" "$runtime_ns/$_rel" done echo "$dist/${namespace:+$namespace/}$entry_rel" ;; diff --git a/tests/unit/desugar/run.sh b/tests/unit/desugar/run.sh index bec3612d..15fd005e 100755 --- a/tests/unit/desugar/run.sh +++ b/tests/unit/desugar/run.sh @@ -262,6 +262,16 @@ st=$? set -e if [[ "$st" -eq 1 && "$err" == *"reserved for the dist marker"* && ! -e "$dist/main.awk" && ! -e "$dist/.hawk-dist" ]]; then ok "marker_namespace_include_rejected"; else ng "marker_namespace_include_rejected" "exit=$st: $err"; fi +reserved_entry="$TMP/reserved-entry" +mkdir -p "$reserved_entry/.hawk-dist" +printf 'BEGIN { print "reserved-entry" }\n' > "$reserved_entry/.hawk-dist/main.awk" +dist="$TMP/dist-reserved-entry" +set +e +err=$(cd "$reserved_entry" && HAWK_DIST="$dist" "$LIBS_ABS" desugar .hawk-dist/main.awk 2>&1 >/dev/null) +st=$? +set -e +if [[ "$st" -eq 1 && "$err" == *".hawk-dist/main.awk"* && ! -e "$dist/current" && ! -e "$dist/.hawk-dist" ]]; then ok "reserved_entry_namespace_rejected"; else ng "reserved_entry_namespace_rejected" "exit=$st: $err"; fi + # --- プロジェクト内を指す絶対パス include は exit 1、外部の絶対パスは素通り --- absinc="$TMP/absinc" mkdir -p "$absinc" @@ -389,6 +399,35 @@ dist="$TMP/distrerun" HAWK_DIST="$dist" "$LIBS" desugar "$FIX/proj/main.awk" >/dev/null if HAWK_DIST="$dist" "$LIBS" desugar "$FIX/proj/main.awk" >/dev/null; then ok "rerun_over_marker_ok"; else ng "rerun_over_marker_ok" "exit != 0"; fi +# --- 改ざんされた stable symlink は現行 generation へ戻す --- +stale_src="$TMP/stale-stable-src" +mkdir -p "$stale_src" +cp "$FIX/solo.awk" "$stale_src/main.awk" +dist="$TMP/dist-stale-stable" +( cd "$stale_src" && HAWK_DIST="$dist" "$LIBS_ABS" desugar main.awk >/dev/null ) +stale_target="$TMP/stale-target.awk" +printf 'BEGIN { print "stale" }\n' > "$stale_target" +rm "$dist/main.awk" +ln -s "$stale_target" "$dist/main.awk" +( cd "$stale_src" && HAWK_DIST="$dist" "$LIBS_ABS" desugar main.awk >/dev/null ) +if [[ -L "$dist/main.awk" && "$(readlink "$dist/main.awk")" == "$dist/current/main.awk" ]]; then ok "stale_stable_symlink_refreshed"; else ng "stale_stable_symlink_refreshed" "target=$(readlink "$dist/main.awk" 2>/dev/null)"; fi + +# --- record と alias が include 間で同名なら index 時点で拒否する --- +type_collision="$TMP/type-collision" +mkdir -p "$type_collision" +printf 'type User = { name: Str }\n' > "$type_collision/record.awk" +printf 'type User = Str\n' > "$type_collision/alias.awk" +printf '@include "record.awk"\n@include "alias.awk"\nBEGIN { print "collision" }\n' > "$type_collision/main.awk" +printf '@include "alias.awk"\n@include "record.awk"\nBEGIN { print "collision" }\n' > "$type_collision/reverse.awk" +dist="$TMP/dist-type-collision" +set +e +err=$(HAWK_DIST="$dist" "$LIBS" desugar "$type_collision/main.awk" 2>&1 >/dev/null) +st=$? +err_reverse=$(HAWK_DIST="$TMP/dist-type-collision-reverse" "$LIBS" desugar "$type_collision/reverse.awk" 2>&1 >/dev/null) +st_reverse=$? +set -e +if [[ "$st" -eq 1 && "$err" == *"type name is both record and alias: User"* && "$st_reverse" -eq 1 && "$err_reverse" == *"type name is both record and alias: User"* ]]; then ok "alias_record_name_collision_rejected"; else ng "alias_record_name_collision_rejected" "forward=$st: $err / reverse=$st_reverse: $err_reverse"; fi + # --- 異なる source の同名 basename entry は namespace で分離される --- multi_entry="$TMP/multi-entry" mkdir -p "$multi_entry/apps/a" "$multi_entry/apps/b" From 5ba0cf7569ee5f305b9e6965421c878b4d808b8c Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Wed, 15 Jul 2026 20:40:11 +0900 Subject: [PATCH 07/20] =?UTF-8?q?fix:=20include=20=E3=81=AE=E4=BA=88?= =?UTF-8?q?=E7=B4=84=20namespace=20=E3=82=92=E6=8B=92=E5=90=A6=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 公開先の current と generation 管理領域を include が占有すると atomic 切替が壊れるため、desugar 前に拒否する。 --- libexec/hawk-libs | 4 ++++ tests/unit/desugar/run.sh | 11 +++++++++++ 2 files changed, 15 insertions(+) diff --git a/libexec/hawk-libs b/libexec/hawk-libs index 294eee86..d08a5e1c 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -317,6 +317,10 @@ case "$subcmd" in echo "[hawk-libs] desugar failed: '.hawk-dist' is reserved for the dist marker: $rel" >&2 exit 1 ;; + current|current/*|.hawk-generations|.hawk-generations/*) + echo "[hawk-libs] desugar failed: reserved output namespace: $rel" >&2 + exit 1 + ;; esac # 同一実体 (symlink/hardlink) を別名で include すると、mirror 後は # 別 inode になり gawk の重複 include 排除が効かず定義が二重実行される。 diff --git a/tests/unit/desugar/run.sh b/tests/unit/desugar/run.sh index 15fd005e..5f378852 100755 --- a/tests/unit/desugar/run.sh +++ b/tests/unit/desugar/run.sh @@ -262,6 +262,17 @@ st=$? set -e if [[ "$st" -eq 1 && "$err" == *"reserved for the dist marker"* && ! -e "$dist/main.awk" && ! -e "$dist/.hawk-dist" ]]; then ok "marker_namespace_include_rejected"; else ng "marker_namespace_include_rejected" "exit=$st: $err"; fi +reserved_ns="$TMP/reserved-namespace" +mkdir -p "$reserved_ns/current" +printf '@include "current/x.awk"\nBEGIN { print "reserved-namespace" }\n' > "$reserved_ns/main.awk" +printf 'function reserved_x() { return 1 }\n' > "$reserved_ns/current/x.awk" +dist="$TMP/dist-reserved-namespace" +set +e +err=$(HAWK_DIST="$dist" "$LIBS" desugar "$reserved_ns/main.awk" 2>&1 >/dev/null) +st=$? +set -e +if [[ "$st" -eq 1 && "$err" == *"reserved output namespace: current/x.awk"* && ! -e "$dist/current" ]]; then ok "reserved_namespace_include_rejected"; else ng "reserved_namespace_include_rejected" "exit=$st: $err"; fi + reserved_entry="$TMP/reserved-entry" mkdir -p "$reserved_entry/.hawk-dist" printf 'BEGIN { print "reserved-entry" }\n' > "$reserved_entry/.hawk-dist/main.awk" From 3f894049668379166bff0e1426c6ee6c688dc966 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Wed, 15 Jul 2026 21:20:38 +0900 Subject: [PATCH 08/20] =?UTF-8?q?fix:=20vendor=20=E5=AE=9F=E8=A1=8C?= =?UTF-8?q?=E3=81=A8=20symlink=20=E6=9B=B4=E6=96=B0=E3=82=92=E7=9B=B4?= =?UTF-8?q?=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit framework 外の cwd でも HAWK_LIB から実行資源を解決し、置換可能な directory symlink を generation 出力へ戻せるようにする。 --- libexec/hawk-libs | 6 +++--- libexec/hawk-serve | 5 +++-- libexec/hawk-worker | 2 ++ tests/unit/cli/run.sh | 37 +++++++++++++++++++++++++++++++++++++ tests/unit/desugar/run.sh | 7 +++++++ 5 files changed, 52 insertions(+), 5 deletions(-) diff --git a/libexec/hawk-libs b/libexec/hawk-libs index d08a5e1c..e82982f3 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -7,7 +7,7 @@ shift case "$subcmd" in plugins) - for d in plugins/*/; do + for d in "$HAWK_LIB"/plugins/*/; do [[ -d "$d" ]] || continue [[ -f "${d}.disabled" ]] && continue name=$(basename "$d") @@ -26,7 +26,7 @@ case "$subcmd" in *) so_ext=so ;; esac if [[ -z "$HAWK_NO_LIBS" ]]; then - for d in libs/*/; do + for d in "$HAWK_LIB"/libs/*/; do [[ -d "$d" ]] || continue name=$(basename "$d") [[ "$name" = "_common" ]] && continue @@ -352,7 +352,7 @@ case "$subcmd" in echo "[hawk-libs] desugar failed: output directory escapes dist via symlink: $compat_parent_phys" >&2 exit 1 fi - if [[ -d "$stable" || ( -e "$stable" && ! -L "$stable" ) || -d "$compat" || ( -e "$compat" && ! -L "$compat" ) ]]; then + if [[ ( -e "$stable" && ! -L "$stable" ) || ( -e "$compat" && ! -L "$compat" ) ]]; then echo "[hawk-libs] desugar failed: output path is not replaceable: $stable" >&2 exit 1 fi diff --git a/libexec/hawk-serve b/libexec/hawk-serve index 8b127512..919ca839 100755 --- a/libexec/hawk-serve +++ b/libexec/hawk-serve @@ -81,6 +81,7 @@ shutdown() { trap shutdown INT TERM APP_AWK="$("$LIBS" desugar "$APP")" +[[ "$APP_AWK" == /* ]] || APP_AWK="$(pwd)/$APP_AWK" # --workers N かつ supervisor 利用可能な場合は hawk-supervise に委譲する if [[ "$EFFECTIVE_WORKERS" -gt 1 && "$HAS_NET" -eq 1 ]]; then @@ -102,7 +103,7 @@ fi for (( i=0; i&2 sleep 1 # shellcheck disable=SC2086 - LC_ALL=C gawk -b $LIBS_ARGS $LIBS_VARS -f hawk.awk "${_plugin_args[@]}" -f "$APP_AWK" & + ( cd "$HAWK_LIB" && LC_ALL=C gawk -b $LIBS_ARGS $LIBS_VARS -f "$HAWK_LIB/hawk.awk" "${_plugin_args[@]}" -f "$APP_AWK" ) & new_pids+=($!) fi fi diff --git a/libexec/hawk-worker b/libexec/hawk-worker index d7596599..fd12723b 100755 --- a/libexec/hawk-worker +++ b/libexec/hawk-worker @@ -9,6 +9,7 @@ LIBS="${HAWK_LIBEXEC}/hawk-libs" APP_AWK="${1:?app artifact path required}" shift PLUGIN_ARGS=("$@") +[[ "$APP_AWK" == /* ]] || APP_AWK="$(pwd)/$APP_AWK" if [[ -n "${HAWK_RUN_DIR:-}" && -n "${HAWK_PROC_ID:-}" ]]; then safe_pid="$BASHPID" @@ -22,4 +23,5 @@ fi eval "$("$LIBS" libs)" # shellcheck disable=SC2086 +cd "$HAWK_LIB" exec gawk -b $LIBS_ARGS $LIBS_VARS -f "$HAWK_LIB/hawk.awk" "${PLUGIN_ARGS[@]}" -f "$APP_AWK" diff --git a/tests/unit/cli/run.sh b/tests/unit/cli/run.sh index 1414f7b2..60c05f8e 100755 --- a/tests/unit/cli/run.sh +++ b/tests/unit/cli/run.sh @@ -81,6 +81,7 @@ rm -rf "$emit_multi" # check/emit は隔離した一時 dist を使い、cwd の永続 dist を作らない・更新しない HAWK_ABS="$(pwd)/bin/hawk" +HAWK_ROOT="$(pwd)" VALID_ABS="$(pwd)/$VALID" work=$(mktemp -d) ( cd "$work" && HAWK_NO_LIBS=1 "$HAWK_ABS" check --strict "$VALID_ABS" >/dev/null 2>&1 ) @@ -97,5 +98,41 @@ else fi rm -rf "$work" +# framework 外の cwd でも framework 本体・plugin・native lib を HAWK_LIB から解決する +work=$(mktemp -d) +printf 'BEGIN { print "outside-cwd-ok" }\n' > "$work/app.awk" +serve_out=$(cd "$work" && HAWK_NO_LIBS=1 HAWK_NO_SERVE=1 HAWK_WORKERS=1 "$HAWK_ABS" serve app.awk 2>/dev/null) +if [[ "$serve_out" == *"outside-cwd-ok"* ]]; then + printf " PASS: serve_outside_hawk_lib\n"; PASS=$((PASS+1)) +else + printf " FAIL: serve_outside_hawk_lib (output=%s)\n" "$serve_out"; FAIL=$((FAIL+1)) +fi +worker_out=$(cd "$work" && HAWK_LIB="$HAWK_ROOT" HAWK_NO_LIBS=1 HAWK_NO_SERVE=1 "$HAWK_ROOT/libexec/hawk-worker" "$work/app.awk" 2>/dev/null) +if [[ "$worker_out" == *"outside-cwd-ok"* ]]; then + printf " PASS: worker_outside_hawk_lib\n"; PASS=$((PASS+1)) +else + printf " FAIL: worker_outside_hawk_lib (output=%s)\n" "$worker_out"; FAIL=$((FAIL+1)) +fi + +framework="$work/framework" +mkdir -p "$framework/plugins/demo" "$framework/libs/net/zig-out/lib" +printf '# manifest\n' > "$framework/plugins/demo/manifest.awk" +printf '# plugin\n' > "$framework/plugins/demo/demo.awk" +case "$(uname -s)" in Darwin) so_ext=dylib ;; *) so_ext=so ;; esac +touch "$framework/libs/net/zig-out/lib/libhawk_net.$so_ext" +plugins_out=$(cd "$work" && HAWK_LIB="$framework" "$HAWK_ROOT/libexec/hawk-libs" plugins) +if [[ "$plugins_out" == *"$framework/plugins/demo/manifest.awk"* && "$plugins_out" == *"$framework/plugins/demo/demo.awk"* ]]; then + printf " PASS: plugins_outside_hawk_lib\n"; PASS=$((PASS+1)) +else + printf " FAIL: plugins_outside_hawk_lib (output=%s)\n" "$plugins_out"; FAIL=$((FAIL+1)) +fi +libs_out=$(cd "$work" && HAWK_LIB="$framework" "$HAWK_ROOT/libexec/hawk-libs" libs) +if [[ "$libs_out" == *"$framework/libs/net/zig-out/lib/libhawk_net.$so_ext"* && "$libs_out" == *"HAS_NET=1"* ]]; then + printf " PASS: libs_outside_hawk_lib\n"; PASS=$((PASS+1)) +else + printf " FAIL: libs_outside_hawk_lib (output=%s)\n" "$libs_out"; FAIL=$((FAIL+1)) +fi +rm -rf "$work" + printf "\n%d passed, %d failed\n" "$PASS" "$FAIL" [[ $FAIL -eq 0 ]] diff --git a/tests/unit/desugar/run.sh b/tests/unit/desugar/run.sh index 5f378852..440a14f5 100755 --- a/tests/unit/desugar/run.sh +++ b/tests/unit/desugar/run.sh @@ -423,6 +423,13 @@ ln -s "$stale_target" "$dist/main.awk" ( cd "$stale_src" && HAWK_DIST="$dist" "$LIBS_ABS" desugar main.awk >/dev/null ) if [[ -L "$dist/main.awk" && "$(readlink "$dist/main.awk")" == "$dist/current/main.awk" ]]; then ok "stale_stable_symlink_refreshed"; else ng "stale_stable_symlink_refreshed" "target=$(readlink "$dist/main.awk" 2>/dev/null)"; fi +stale_dir="$TMP/stale-directory-target" +mkdir -p "$stale_dir" +rm "$dist/main.awk" +ln -s "$stale_dir" "$dist/main.awk" +( cd "$stale_src" && HAWK_DIST="$dist" "$LIBS_ABS" desugar main.awk >/dev/null ) +if [[ -L "$dist/main.awk" && "$(readlink "$dist/main.awk")" == "$dist/current/main.awk" ]]; then ok "stale_directory_symlink_refreshed"; else ng "stale_directory_symlink_refreshed" "target=$(readlink "$dist/main.awk" 2>/dev/null)"; fi + # --- record と alias が include 間で同名なら index 時点で拒否する --- type_collision="$TMP/type-collision" mkdir -p "$type_collision" From 3100de3899b1f329798ebf393fe4f31bad6a8af9 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Wed, 15 Jul 2026 22:09:41 +0900 Subject: [PATCH 09/20] =?UTF-8?q?fix:=20app=20=E3=81=AE=E5=AE=9F=E8=A1=8C?= =?UTF-8?q?=20cwd=20=E3=82=92=E4=BF=9D=E3=81=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit framework の include だけを起動前に再帰展開し、worker がプロジェクト相対のデータを従来どおり読めるようにする。 --- libexec/hawk-libs | 28 ++++++++++++++++++++++++++++ libexec/hawk-serve | 8 ++++++-- libexec/hawk-worker | 3 +-- tests/unit/cli/run.sh | 4 +++- 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/libexec/hawk-libs b/libexec/hawk-libs index e82982f3..bb371968 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -46,6 +46,34 @@ case "$subcmd" in printf 'LIBS_VARS=%s\n' "$(printf '%q' "$libs_vars")" printf 'HAS_NET=%d\n' "$has_net" ;; + runtime-awk) + _runtime_seen=$'\n' + _runtime_include_re='^[[:space:]]*@include[[:space:]]+"([^"]+)"' + _runtime_namespace_re='^[[:space:]]*@namespace[[:space:]]+"([^"]+)"' + _emit_runtime_awk() { + local file="$1" namespace="$2" line inc + case "$_runtime_seen" in *$'\n'"$file"$'\n'*) return 0 ;; esac + _runtime_seen="${_runtime_seen}${file}"$'\n' + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" =~ $_runtime_namespace_re ]]; then + namespace="${BASH_REMATCH[1]}" + elif [[ "$line" =~ $_runtime_include_re ]]; then + inc="${BASH_REMATCH[1]}" + if [[ "$inc" != /* ]]; then + if [[ ! -f "$HAWK_LIB/$inc" ]]; then + echo "[hawk-libs] runtime-awk: include not found: $inc" >&2 + return 1 + fi + _emit_runtime_awk "$HAWK_LIB/$inc" "$namespace" + printf '@namespace "%s"\n' "$namespace" + continue + fi + fi + printf '%s\n' "$line" + done < "$file" + } + _emit_runtime_awk "$HAWK_LIB/hawk.awk" awk + ;; desugar) src="${1:-}" if [[ ! -f "$src" ]]; then diff --git a/libexec/hawk-serve b/libexec/hawk-serve index 919ca839..55f63f91 100755 --- a/libexec/hawk-serve +++ b/libexec/hawk-serve @@ -101,9 +101,13 @@ if [[ "$EFFECTIVE_WORKERS" -gt 1 && "$HAS_NET" -eq 1 ]]; then exit 0 fi +RUNTIME_AWK="$(mktemp "${TMPDIR:-/tmp}/hawk-runtime.XXXXXX")" +trap 'rm -f "$RUNTIME_AWK"' EXIT +"$LIBS" runtime-awk > "$RUNTIME_AWK" + for (( i=0; i&2 sleep 1 # shellcheck disable=SC2086 - ( cd "$HAWK_LIB" && LC_ALL=C gawk -b $LIBS_ARGS $LIBS_VARS -f "$HAWK_LIB/hawk.awk" "${_plugin_args[@]}" -f "$APP_AWK" ) & + LC_ALL=C gawk -b $LIBS_ARGS $LIBS_VARS -f "$RUNTIME_AWK" "${_plugin_args[@]}" -f "$APP_AWK" & new_pids+=($!) fi fi diff --git a/libexec/hawk-worker b/libexec/hawk-worker index fd12723b..85d1a55f 100755 --- a/libexec/hawk-worker +++ b/libexec/hawk-worker @@ -23,5 +23,4 @@ fi eval "$("$LIBS" libs)" # shellcheck disable=SC2086 -cd "$HAWK_LIB" -exec gawk -b $LIBS_ARGS $LIBS_VARS -f "$HAWK_LIB/hawk.awk" "${PLUGIN_ARGS[@]}" -f "$APP_AWK" +exec gawk -b $LIBS_ARGS $LIBS_VARS -f <("$LIBS" runtime-awk) "${PLUGIN_ARGS[@]}" -f "$APP_AWK" diff --git a/tests/unit/cli/run.sh b/tests/unit/cli/run.sh index 60c05f8e..480483fd 100755 --- a/tests/unit/cli/run.sh +++ b/tests/unit/cli/run.sh @@ -100,7 +100,9 @@ rm -rf "$work" # framework 外の cwd でも framework 本体・plugin・native lib を HAWK_LIB から解決する work=$(mktemp -d) -printf 'BEGIN { print "outside-cwd-ok" }\n' > "$work/app.awk" +mkdir -p "$work/data" +printf 'name\noutside-cwd-ok\n' > "$work/data/items.tsv" +printf 'BEGIN { if (read_tsv("data/items.tsv", rows) == 1) print rows[1, "name"] }\n' > "$work/app.awk" serve_out=$(cd "$work" && HAWK_NO_LIBS=1 HAWK_NO_SERVE=1 HAWK_WORKERS=1 "$HAWK_ABS" serve app.awk 2>/dev/null) if [[ "$serve_out" == *"outside-cwd-ok"* ]]; then printf " PASS: serve_outside_hawk_lib\n"; PASS=$((PASS+1)) From e3edbfee8d356cf3938875af6cdbe6863ac30dd4 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Wed, 15 Jul 2026 23:53:56 +0900 Subject: [PATCH 10/20] =?UTF-8?q?fix:=20supervised=20worker=20=E3=81=AE=20?= =?UTF-8?q?runtime=20=E3=82=92=E5=86=8D=E5=88=A9=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libexec/hawk-supervise | 10 ++++++++++ libexec/hawk-worker | 9 +++++++++ tests/e2e/run.sh | 12 ++++++------ 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/libexec/hawk-supervise b/libexec/hawk-supervise index 4b877f70..a104078d 100755 --- a/libexec/hawk-supervise +++ b/libexec/hawk-supervise @@ -42,6 +42,16 @@ _setup_run_dir() { _setup_run_dir +HAWK_RUNTIME_AWK="$HAWK_RUN_DIR/runtime.awk" +_runtime_tmp="$(mktemp "$HAWK_RUN_DIR/.runtime.XXXXXX")" +if ! "$LIBS" runtime-awk > "$_runtime_tmp"; then + rm -f "$_runtime_tmp" + exit 1 +fi +chmod 0600 "$_runtime_tmp" +mv -f "$_runtime_tmp" "$HAWK_RUNTIME_AWK" +export HAWK_RUNTIME_AWK + eval "$("$LIBS" libs)" mapfile -t _plugin_paths < <("$LIBS" plugins) _plugin_args=() diff --git a/libexec/hawk-worker b/libexec/hawk-worker index 85d1a55f..388d3275 100755 --- a/libexec/hawk-worker +++ b/libexec/hawk-worker @@ -22,5 +22,14 @@ fi eval "$("$LIBS" libs)" +if [[ "${HAWK_SUPERVISED:-}" == 1 ]]; then + if [[ ! -f "${HAWK_RUNTIME_AWK:-}" ]]; then + echo "[hawk-worker] supervised runtime bundle not found: ${HAWK_RUNTIME_AWK:-}" >&2 + exit 1 + fi + # shellcheck disable=SC2086 + exec gawk -b $LIBS_ARGS $LIBS_VARS -f "$HAWK_RUNTIME_AWK" "${PLUGIN_ARGS[@]}" -f "$APP_AWK" +fi + # shellcheck disable=SC2086 exec gawk -b $LIBS_ARGS $LIBS_VARS -f <("$LIBS" runtime-awk) "${PLUGIN_ARGS[@]}" -f "$APP_AWK" diff --git a/tests/e2e/run.sh b/tests/e2e/run.sh index 14f0943c..22434799 100755 --- a/tests/e2e/run.sh +++ b/tests/e2e/run.sh @@ -14,12 +14,12 @@ HAWK_MAX_BODY_SIZE=1024 \ SERVER=$! trap 'kill -TERM "$SERVER" 2>/dev/null || true; wait "$SERVER" 2>/dev/null || true' EXIT INT TERM -# 起動待ち (最大 3 秒) -for _ in 1 2 3 4 5 6 7 8 9 10; do - if curl -s -o /dev/null "http://127.0.0.1:$PORT/" 2>/dev/null; then - break - fi - sleep 0.3 +# 起動待ち (最大 5 秒) +attempt=0 +until curl -s -o /dev/null "http://127.0.0.1:$PORT/" 2>/dev/null; do + attempt=$((attempt + 1)) + [ "$attempt" -ge 50 ] && break + sleep 0.1 done PASS=0 From aca3671565a984c35343d944d5cbd155f4393046 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Thu, 16 Jul 2026 00:46:19 +0900 Subject: [PATCH 11/20] =?UTF-8?q?fix:=20compat=20symlink=20=E3=81=AE?= =?UTF-8?q?=E6=89=80=E6=9C=89=E6=A8=A9=E3=82=92=E5=AE=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libexec/hawk-libs | 27 ++++++++++++++++++++++----- tests/unit/desugar/run.sh | 17 ++++++++++++++++- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/libexec/hawk-libs b/libexec/hawk-libs index bb371968..8f0954e7 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -281,6 +281,7 @@ case "$subcmd" in _pub_tmp=() _pub_out=() _pub_rel=() + _compat_skip=() # プロジェクト内ファイルへの hard link を指す絶対パス include を、closure 完了後に # まとめて照合するための保留キュー (staging 中は _seen が未確定のため、後続 sibling # を hard link 経由で見逃す。詳細は下の post-closure 検査を参照) @@ -326,7 +327,7 @@ case "$subcmd" in # rel: src_root からの相対パス。desugar して dist/rel に出力し、 # src_root 相対で実在する @include を dist 接頭に書き換えて再帰する。 _desugar_one() { - local rel="$1" in out out_parent tmp inc incs prior + local rel="$1" in out out_parent tmp inc incs prior marker_src marker_rel compat_owned case "$_seen" in *" $rel "*) return 0 ;; esac case "$rel" in ../*|*/../*) @@ -384,10 +385,20 @@ case "$subcmd" in echo "[hawk-libs] desugar failed: output path is not replaceable: $stable" >&2 exit 1 fi - if [[ -L "$compat" && ! -e "$compat" ]]; then + if [[ -e "$compat" || -L "$compat" ]]; then if [[ ! -f "$dist/.hawk-dist" ]] || ! grep -qxF -- "$src_root"$'\t'"$rel" "$dist/.hawk-dist"; then - echo "[hawk-libs] desugar failed: dangling output symlink: $compat" >&2 - exit 1 + compat_owned=0 + if [[ -f "$dist/.hawk-dist" ]]; then + while IFS=$'\t' read -r marker_src marker_rel; do + if [[ "$marker_rel" == "$rel" ]]; then compat_owned=1; break; fi + done < "$dist/.hawk-dist" + fi + if [[ "$compat_owned" -eq 1 ]]; then + _compat_skip+=("$rel") + else + echo "[hawk-libs] desugar failed: refusing to overwrite existing compatibility output: $compat" >&2 + exit 1 + fi fi fi if [[ -e "$stable" || -L "$stable" ]]; then @@ -620,7 +631,13 @@ case "$subcmd" in _stable="$dist/${namespace:+$namespace/}$_rel" _compat="$dist/$_rel" _refresh_output_link "$_stable" "$runtime_ns/$_rel" - [[ "$_compat" == "$_stable" ]] || _refresh_output_link "$_compat" "$runtime_ns/$_rel" + if [[ "$_compat" != "$_stable" ]]; then + _refresh_compat=1 + for _skip_rel in "${_compat_skip[@]}"; do + [[ "$_skip_rel" == "$_rel" ]] && { _refresh_compat=0; break; } + done + [[ "$_refresh_compat" -eq 0 ]] || _refresh_output_link "$_compat" "$runtime_ns/$_rel" + fi done echo "$dist/${namespace:+$namespace/}$entry_rel" ;; diff --git a/tests/unit/desugar/run.sh b/tests/unit/desugar/run.sh index 440a14f5..d44340d4 100755 --- a/tests/unit/desugar/run.sh +++ b/tests/unit/desugar/run.sh @@ -410,6 +410,20 @@ dist="$TMP/distrerun" HAWK_DIST="$dist" "$LIBS" desugar "$FIX/proj/main.awk" >/dev/null if HAWK_DIST="$dist" "$LIBS" desugar "$FIX/proj/main.awk" >/dev/null; then ok "rerun_over_marker_ok"; else ng "rerun_over_marker_ok" "exit != 0"; fi +# --- marker 未登録の compat symlink は上書きしない --- +compat_src="$TMP/unowned-compat-src" +dist="$TMP/unowned-compat-dist" +mkdir -p "$compat_src/apps/a" "$dist" +printf 'BEGIN { print "compat" }\n' > "$compat_src/apps/a/main.awk" +compat_target="$TMP/unowned-compat-target" +printf 'user file\n' > "$compat_target" +ln -s "$compat_target" "$dist/main.awk" +set +e +err=$(cd "$compat_src" && HAWK_DIST="$dist" "$LIBS_ABS" desugar apps/a/main.awk 2>&1 >/dev/null) +st=$? +set -e +if [[ "$st" -eq 1 && -L "$dist/main.awk" && "$(readlink "$dist/main.awk")" == "$compat_target" && ! -e "$dist/current" ]]; then ok "unowned_compat_symlink_rejected"; else ng "unowned_compat_symlink_rejected" "exit=$st, target=$(readlink "$dist/main.awk" 2>/dev/null): $err"; fi + # --- 改ざんされた stable symlink は現行 generation へ戻す --- stale_src="$TMP/stale-stable-src" mkdir -p "$stale_src" @@ -454,9 +468,10 @@ printf 'BEGIN { print "from-b" }\n' > "$multi_entry/apps/b/main.awk" dist="$TMP/distcs" ( cd "$multi_entry" && HAWK_DIST="$dist" "$LIBS_ABS" desugar apps/a/main.awk >/dev/null ) first_a="$dist/$(readlink "$dist/current")/apps/a/main.awk" +compat_a="$(readlink "$dist/main.awk")" ( cd "$multi_entry" && HAWK_DIST="$dist" "$LIBS_ABS" desugar apps/b/main.awk >/dev/null ) entries=$(find -L "$dist/current" -type f -name main.awk | wc -l | tr -d ' ') -if [[ "$entries" -eq 2 ]] && grep -q "from-a" "$dist/apps/a/main.awk" && grep -q "from-b" "$dist/apps/b/main.awk"; then +if [[ "$entries" -eq 2 && "$(readlink "$dist/main.awk")" == "$compat_a" ]] && grep -q "from-a" "$dist/apps/a/main.awk" && grep -q "from-b" "$dist/apps/b/main.awk"; then ok "cross_source_same_basename_isolated" else ng "cross_source_same_basename_isolated" "entries=$entries" From a29364dc6fadcd5369cb577112274f03bcbd5db9 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Thu, 16 Jul 2026 09:48:32 +0900 Subject: [PATCH 12/20] =?UTF-8?q?fix:=20compat=20symlink=20=E3=81=AE?= =?UTF-8?q?=E6=89=80=E6=9C=89=E8=80=85=E3=82=92=E5=9B=BA=E5=AE=9A=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libexec/hawk-libs | 55 +++++++++++++++++++++++---------------- tests/unit/desugar/run.sh | 3 +++ 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/libexec/hawk-libs b/libexec/hawk-libs index 8f0954e7..dc6dc143 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -282,6 +282,11 @@ case "$subcmd" in _pub_out=() _pub_rel=() _compat_skip=() + _compat_is_skipped() { + local rel="$1" skip + for skip in "${_compat_skip[@]}"; do [[ "$skip" == "$rel" ]] && return 0; done + return 1 + } # プロジェクト内ファイルへの hard link を指す絶対パス include を、closure 完了後に # まとめて照合するための保留キュー (staging 中は _seen が未確定のため、後続 sibling # を hard link 経由で見逃す。詳細は下の post-closure 検査を参照) @@ -327,7 +332,8 @@ case "$subcmd" in # rel: src_root からの相対パス。desugar して dist/rel に出力し、 # src_root 相対で実在する @include を dist 接頭に書き換えて再帰する。 _desugar_one() { - local rel="$1" in out out_parent tmp inc incs prior marker_src marker_rel compat_owned + local rel="$1" in out out_parent tmp inc incs prior marker_src marker_rel marker_kind + local compat_owned compat_other stable_owned stable_other case "$_seen" in *" $rel "*) return 0 ;; esac case "$rel" in ../*|*/../*) @@ -386,19 +392,24 @@ case "$subcmd" in exit 1 fi if [[ -e "$compat" || -L "$compat" ]]; then - if [[ ! -f "$dist/.hawk-dist" ]] || ! grep -qxF -- "$src_root"$'\t'"$rel" "$dist/.hawk-dist"; then - compat_owned=0 - if [[ -f "$dist/.hawk-dist" ]]; then - while IFS=$'\t' read -r marker_src marker_rel; do - if [[ "$marker_rel" == "$rel" ]]; then compat_owned=1; break; fi - done < "$dist/.hawk-dist" - fi - if [[ "$compat_owned" -eq 1 ]]; then - _compat_skip+=("$rel") - else - echo "[hawk-libs] desugar failed: refusing to overwrite existing compatibility output: $compat" >&2 - exit 1 - fi + compat_owned=0; compat_other=0; stable_owned=0; stable_other=0 + if [[ -f "$dist/.hawk-dist" ]]; then + while IFS=$'\t' read -r marker_src marker_rel marker_kind; do + [[ "$marker_rel" == "$rel" ]] || continue + if [[ "$marker_kind" == compat ]]; then + if [[ "$marker_src" == "$src_root" ]]; then compat_owned=1; else compat_other=1; fi + elif [[ -z "$marker_kind" ]]; then + if [[ "$marker_src" == "$src_root" ]]; then stable_owned=1; else stable_other=1; fi + fi + done < "$dist/.hawk-dist" + fi + if [[ "$compat_owned" -eq 1 && "$compat_other" -eq 0 ]]; then + : + elif [[ "$compat_owned" -eq 1 || "$compat_other" -eq 1 || "$stable_other" -eq 1 ]]; then + _compat_skip+=("$rel") + elif [[ "$stable_owned" -eq 0 ]]; then + echo "[hawk-libs] desugar failed: refusing to overwrite existing compatibility output: $compat" >&2 + exit 1 fi fi if [[ -e "$stable" || -L "$stable" ]]; then @@ -601,7 +612,8 @@ case "$subcmd" in chmod 644 "${_pub_tmp[$i]}" mv "${_pub_tmp[$i]}" "${_pub_out[$i]}" done - # marker は「今回 publish した rel」を既存の記録に追記して保持する。 + # marker は stable 所有者を2列、compat 所有者を3列目の compat で記録する。 + # compat は最初に作成した source に固定し、別 source の publish では追記しない。 # 空ファイルのままだと最初の成功後にディレクトリ全体を信頼してしまうため、 # rel を1行1件記録し、次回以降の上書き可否を rel 単位で判定できるようにする。 # 更新も稼働中の hawk-serve と競合しないよう mktemp + mv でアトミックに行う @@ -610,6 +622,11 @@ case "$subcmd" in { [[ -f "$dist/.hawk-dist" ]] && cat "$dist/.hawk-dist" for _rel in "${_pub_rel[@]}"; do printf '%s\t%s\n' "$src_root" "$_rel"; done + if [[ -n "$namespace" ]]; then + for _rel in "${_pub_rel[@]}"; do + _compat_is_skipped "$_rel" || printf '%s\t%s\tcompat\n' "$src_root" "$_rel" + done + fi } | sort -u > "$_marker_tmp" mv "$_marker_tmp" "$stage_root/.hawk-dist" for ((i = 0; i < ${#_pub_rel[@]}; i++)); do @@ -631,13 +648,7 @@ case "$subcmd" in _stable="$dist/${namespace:+$namespace/}$_rel" _compat="$dist/$_rel" _refresh_output_link "$_stable" "$runtime_ns/$_rel" - if [[ "$_compat" != "$_stable" ]]; then - _refresh_compat=1 - for _skip_rel in "${_compat_skip[@]}"; do - [[ "$_skip_rel" == "$_rel" ]] && { _refresh_compat=0; break; } - done - [[ "$_refresh_compat" -eq 0 ]] || _refresh_output_link "$_compat" "$runtime_ns/$_rel" - fi + [[ "$_compat" == "$_stable" ]] || _compat_is_skipped "$_rel" || _refresh_output_link "$_compat" "$runtime_ns/$_rel" done echo "$dist/${namespace:+$namespace/}$entry_rel" ;; diff --git a/tests/unit/desugar/run.sh b/tests/unit/desugar/run.sh index d44340d4..4b23d8d7 100755 --- a/tests/unit/desugar/run.sh +++ b/tests/unit/desugar/run.sh @@ -470,6 +470,9 @@ dist="$TMP/distcs" first_a="$dist/$(readlink "$dist/current")/apps/a/main.awk" compat_a="$(readlink "$dist/main.awk")" ( cd "$multi_entry" && HAWK_DIST="$dist" "$LIBS_ABS" desugar apps/b/main.awk >/dev/null ) +compat_after_b="$(readlink "$dist/main.awk")" +( cd "$multi_entry" && HAWK_DIST="$dist" "$LIBS_ABS" desugar apps/b/main.awk >/dev/null ) +if [[ "$compat_after_b" == "$compat_a" && "$(readlink "$dist/main.awk")" == "$compat_a" ]]; then ok "cross_source_compat_owner_persists_on_rerun"; else ng "cross_source_compat_owner_persists_on_rerun"; fi entries=$(find -L "$dist/current" -type f -name main.awk | wc -l | tr -d ' ') if [[ "$entries" -eq 2 && "$(readlink "$dist/main.awk")" == "$compat_a" ]] && grep -q "from-a" "$dist/apps/a/main.awk" && grep -q "from-b" "$dist/apps/b/main.awk"; then ok "cross_source_same_basename_isolated" From 780de69db0fbe02aa1a1392010e15d3a6bc078d0 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Thu, 16 Jul 2026 11:18:21 +0900 Subject: [PATCH 13/20] =?UTF-8?q?fix:=20index=20=E7=8A=B6=E6=85=8B?= =?UTF-8?q?=E3=81=A8=20plugin=20=E6=8E=A2=E7=B4=A2=E5=85=83=E3=82=92?= =?UTF-8?q?=E6=AD=A3=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/plugin.awk | 12 +++++++----- dsl/lex.awk | 5 +++++ tests/unit/desugar/run.sh | 9 +++++++++ tests/unit/run.awk | 1 + tests/unit/test_plugin.awk | 22 +++++++++++++++++++++- 5 files changed, 43 insertions(+), 6 deletions(-) diff --git a/core/plugin.awk b/core/plugin.awk index 821dee50..d0d7dc06 100644 --- a/core/plugin.awk +++ b/core/plugin.awk @@ -17,15 +17,17 @@ # config_keys を満たさなかった場合 PLUGIN_REGISTER_ERROR=1 を立てる # (起動時 main loop が exit 1 で判定する) -function plugin_discover( cmd, pname, func_name, meta) { - cmd = "ls plugins 2>/dev/null" +function plugin_discover( cmd, pname, func_name, meta, root, disabled) { + root = (ENVIRON["HAWK_LIB"] != "" ? ENVIRON["HAWK_LIB"] : ".") "/plugins" + cmd = "ls " _shellquote(root) " 2>/dev/null" while ((cmd | getline pname) > 0) { if (pname == "" || pname ~ /^\./) continue - if ((getline _ < ("plugins/" pname "/.disabled")) >= 0) { - close("plugins/" pname "/.disabled") + disabled = root "/" pname "/.disabled" + if ((getline _ < disabled) >= 0) { + close(disabled) continue } - close("plugins/" pname "/.disabled") + close(disabled) func_name = "plugin_" pname "_manifest" delete meta diff --git a/dsl/lex.awk b/dsl/lex.awk index ad96e829..2b0a54b1 100644 --- a/dsl/lex.awk +++ b/dsl/lex.awk @@ -242,6 +242,11 @@ function v2_mark_unannotated_func(nlines, i, j, k, depth, stripped, tmp, n_op # トークン化する(BQ)。{ を含む行を処理したら解除する。 function v2_lex(src, line, lineno, in_when, in_block, nlines, tok_start, k, \ pending_brace, header_stripped, force_line) { + delete TOK + delete PASS + delete V2_RAWLINE + delete V2_LINE_TEXT + delete V2_FORCE_DSL V2_NLINES = 0 TOK["n"] = 0 in_when = 0 diff --git a/tests/unit/desugar/run.sh b/tests/unit/desugar/run.sh index 4b23d8d7..5574a563 100755 --- a/tests/unit/desugar/run.sh +++ b/tests/unit/desugar/run.sh @@ -47,6 +47,15 @@ st=$? set -e if [[ "$st" -ne 0 && "$err" == *"normalize argument 1 expects Str, got Int"* ]]; then ok "shared_signature_checks_raw_caller"; else ng "shared_signature_checks_raw_caller" "exit=$st: $err"; fi +lex_state="$TMP/lex-state" +mkdir -p "$lex_state" +printf 'function dsl_body() {\n let x: Int = 1\n return x\n}\n' > "$lex_state/a.awk" +printf 'function raw(a, b) {\n return a + b\n}\n' > "$lex_state/b.awk" +printf '@include "a.awk"\n@include "b.awk"\nfunction caller() { return raw(1, 2) }\nBEGIN { print caller() }\n' > "$lex_state/main.awk" +lex_index="$TMP/lex-index.awk" +gawk -v V2_INDEX_LIST="$lex_state/a.awk"$'\034'"$lex_state/b.awk" -v V2_INDEX_OUT="$lex_index" -f dsl/index.awk +if HAWK_DIST="$TMP/distlexstate" "$LIBS" desugar "$lex_state/main.awk" >/dev/null && ! grep -qF 'V2_SHARED_SIG["raw"' "$lex_index"; then ok "index_lex_state_isolated_per_file"; else ng "index_lex_state_isolated_per_file" "raw function was indexed as DSL"; fi + gen="$TMP/generation" HAWK_DIST="$gen" "$LIBS" desugar "$FIX/solo.awk" >/dev/null old_gen=$(readlink "$gen/current") diff --git a/tests/unit/run.awk b/tests/unit/run.awk index f5c5849a..8a12bd83 100644 --- a/tests/unit/run.awk +++ b/tests/unit/run.awk @@ -198,6 +198,7 @@ BEGIN { test_plugin_call_hooks_normal() test_plugin_call_hooks_abort() test_plugin_missing_config() + test_plugin_discover_hawk_lib_root() test_libs_binary_length() test_libs_binary_read_text() diff --git a/tests/unit/test_plugin.awk b/tests/unit/test_plugin.awk index 716b0e2d..ba4032cf 100644 --- a/tests/unit/test_plugin.awk +++ b/tests/unit/test_plugin.awk @@ -92,7 +92,7 @@ function _test_plugin_discover_unknown_function_guard( cmd, line, output, rc) cmd = "tmp=$(mktemp -d); " \ "mkdir -p \"$tmp/plugins/missing\"; " \ "err=\"$tmp/err\"; " \ - "(cd \"$tmp\" && gawk -f \"" ENVIRON["PWD"] "/core/plugin.awk\" -e 'BEGIN { plugin_discover(); exit 0 }') 2>\"$err\"; " \ + "(cd \"$tmp\" && gawk -f \"" ENVIRON["PWD"] "/core/util.awk\" -f \"" ENVIRON["PWD"] "/core/plugin.awk\" -e 'BEGIN { plugin_discover(); exit 0 }') 2>\"$err\"; " \ "rc=$?; " \ "printf 'RC:%s\\n' \"$rc\"; " \ "cat \"$err\"; " \ @@ -107,3 +107,23 @@ function _test_plugin_discover_unknown_function_guard( cmd, line, output, rc) assert_true(index(output, "plugin: function not found: plugin_missing_manifest") > 0, "plugin: unknown manifest function logs stderr") } + +function test_plugin_discover_hawk_lib_root( cmd, line, output) { + cmd = "tmp=$(mktemp -d); " \ + "framework=\"$tmp/framework\"; app=\"$tmp/app\"; " \ + "mkdir -p \"$framework/plugins/demo\" \"$app/plugins/demo\" \"$app/plugins/ignored\"; " \ + "touch \"$app/plugins/demo/.disabled\"; " \ + "printf 'function plugin_demo_manifest(meta) { meta[\"version\"] = \"1.2.3\" }\\n' > \"$framework/plugins/demo/manifest.awk\"; " \ + "(cd \"$app\" && HAWK_LIB=\"$framework\" gawk " \ + "-f \"" ENVIRON["PWD"] "/core/util.awk\" " \ + "-f \"" ENVIRON["PWD"] "/core/plugin.awk\" " \ + "-f \"$framework/plugins/demo/manifest.awk\" " \ + "-e 'BEGIN { plugin_discover(); print \"VERSION:\" PLUGINS[\"demo\", \"version\"]; print \"IGNORED:\" ((\"ignored\", \"version\") in PLUGINS); exit 0 }') 2>&1; " \ + "rm -rf \"$tmp\"" + output = "" + while ((cmd | getline line) > 0) output = output line "\n" + close(cmd) + + assert_true(index(output, "VERSION:1.2.3\n") > 0, "plugin: discover uses HAWK_LIB root") + assert_true(index(output, "IGNORED:0\n") > 0, "plugin: discover ignores cwd plugins") +} From b724586df605b8889b535a5494a153ad5e9b945a Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Thu, 16 Jul 2026 12:25:00 +0900 Subject: [PATCH 14/20] =?UTF-8?q?fix:=20HAWK=5FLIB=20=E3=82=92=E5=AD=90?= =?UTF-8?q?=E3=83=97=E3=83=AD=E3=82=BB=E3=82=B9=E3=81=B8=E6=B8=A1=E3=81=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libexec/hawk-check | 2 +- libexec/hawk-emit | 2 +- libexec/hawk-serve | 2 +- libexec/hawk-supervise | 2 +- libexec/hawk-worker | 2 +- tests/unit/cli/run.sh | 11 +++++++++++ 6 files changed, 16 insertions(+), 5 deletions(-) diff --git a/libexec/hawk-check b/libexec/hawk-check index 92ff7510..7682ec00 100755 --- a/libexec/hawk-check +++ b/libexec/hawk-check @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -e -HAWK_LIB="${HAWK_LIB:-$(cd "$(dirname "$0")/.." && pwd)}" +export HAWK_LIB="${HAWK_LIB:-$(cd "$(dirname "$0")/.." && pwd)}" HAWK_LIBEXEC="${HAWK_LIBEXEC:-$(cd "$(dirname "$0")" && pwd)}" LIBS="${HAWK_LIBEXEC}/hawk-libs" diff --git a/libexec/hawk-emit b/libexec/hawk-emit index 240f04d2..2322576b 100755 --- a/libexec/hawk-emit +++ b/libexec/hawk-emit @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -e -HAWK_LIB="${HAWK_LIB:-$(cd "$(dirname "$0")/.." && pwd)}" +export HAWK_LIB="${HAWK_LIB:-$(cd "$(dirname "$0")/.." && pwd)}" HAWK_LIBEXEC="${HAWK_LIBEXEC:-$(cd "$(dirname "$0")" && pwd)}" LIBS="${HAWK_LIBEXEC}/hawk-libs" diff --git a/libexec/hawk-serve b/libexec/hawk-serve index 55f63f91..f390b1e0 100755 --- a/libexec/hawk-serve +++ b/libexec/hawk-serve @@ -8,7 +8,7 @@ fi set -e -HAWK_LIB="${HAWK_LIB:-$(cd "$(dirname "$0")/.." && pwd)}" +export HAWK_LIB="${HAWK_LIB:-$(cd "$(dirname "$0")/.." && pwd)}" HAWK_LIBEXEC="${HAWK_LIBEXEC:-$(cd "$(dirname "$0")" && pwd)}" LIBS="${HAWK_LIBEXEC}/hawk-libs" diff --git a/libexec/hawk-supervise b/libexec/hawk-supervise index a104078d..b24f74fb 100755 --- a/libexec/hawk-supervise +++ b/libexec/hawk-supervise @@ -10,7 +10,7 @@ fi set -e HAWK_LIBEXEC="${HAWK_LIBEXEC:-$(cd "$(dirname "$0")" && pwd)}" -HAWK_LIB="${HAWK_LIB:-$(cd "$(dirname "$0")/.." && pwd)}" +export HAWK_LIB="${HAWK_LIB:-$(cd "$(dirname "$0")/.." && pwd)}" LIBS="${HAWK_LIBEXEC}/hawk-libs" APP_AWK="${1:?app artifact path required}" diff --git a/libexec/hawk-worker b/libexec/hawk-worker index 388d3275..afb980b3 100755 --- a/libexec/hawk-worker +++ b/libexec/hawk-worker @@ -3,7 +3,7 @@ # libexec/hawk-worker -- supervised worker ラッパー HAWK_LIBEXEC="${HAWK_LIBEXEC:-$(cd "$(dirname "$0")" && pwd)}" -HAWK_LIB="${HAWK_LIB:-$(cd "$(dirname "$0")/.." && pwd)}" +export HAWK_LIB="${HAWK_LIB:-$(cd "$(dirname "$0")/.." && pwd)}" LIBS="${HAWK_LIBEXEC}/hawk-libs" APP_AWK="${1:?app artifact path required}" diff --git a/tests/unit/cli/run.sh b/tests/unit/cli/run.sh index 480483fd..81d5e3fc 100755 --- a/tests/unit/cli/run.sh +++ b/tests/unit/cli/run.sh @@ -115,6 +115,17 @@ if [[ "$worker_out" == *"outside-cwd-ok"* ]]; then else printf " FAIL: worker_outside_hawk_lib (output=%s)\n" "$worker_out"; FAIL=$((FAIL+1)) fi +printf 'BEGIN { print ENVIRON["HAWK_LIB"] }\n' > "$work/env.awk" +worker_env=$(cd "$work" && unset HAWK_LIB && HAWK_NO_LIBS=1 HAWK_NO_SERVE=1 "$HAWK_ROOT/libexec/hawk-worker" "$work/env.awk" 2>/dev/null) +all_exported=1 +for entry in hawk-serve hawk-worker hawk-supervise hawk-check hawk-emit; do + grep -q '^export HAWK_LIB=' "$HAWK_ROOT/libexec/$entry" || all_exported=0 +done +if [[ "$worker_env" == "$HAWK_ROOT" && "$all_exported" -eq 1 ]]; then + printf " PASS: direct_entrypoints_export_hawk_lib\n"; PASS=$((PASS+1)) +else + printf " FAIL: direct_entrypoints_export_hawk_lib (worker_env=%s, all_exported=%s)\n" "$worker_env" "$all_exported"; FAIL=$((FAIL+1)) +fi framework="$work/framework" mkdir -p "$framework/plugins/demo" "$framework/libs/net/zig-out/lib" From 006fe8d5e6dd47f2f3605d56a802b4738d4df911 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Thu, 16 Jul 2026 13:25:37 +0900 Subject: [PATCH 15/20] =?UTF-8?q?fix:=20=E6=97=A7=20dist=20=E6=88=90?= =?UTF-8?q?=E6=9E=9C=E7=89=A9=E3=82=92=20symlink=20=E3=81=B8=E7=A7=BB?= =?UTF-8?q?=E8=A1=8C=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libexec/hawk-libs | 31 +++++++++++++++++++++++++------ tests/unit/desugar/run.sh | 29 ++++++++++++++++++++++++++++- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/libexec/hawk-libs b/libexec/hawk-libs index dc6dc143..08fb9040 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -112,6 +112,15 @@ case "$subcmd" in src_root="$(cd "$(dirname "$src")" && pwd -P)" dist_phys="$(cd "$dist" && pwd -P)" cwd_phys="$(pwd -P)" + _marker_owns_output() { + local owner_src="$1" rel="$2" kind="${3:-}" marker="$dist/.hawk-dist" + [[ -f "$marker" ]] || return 1 + if [[ "$kind" == compat ]]; then + grep -qxF -- "$owner_src"$'\t'"$rel"$'\t'compat "$marker" + else + grep -qxF -- "$owner_src"$'\t'"$rel" "$marker" + fi + } case "$src" in /*) src_abs="$src" @@ -161,7 +170,13 @@ case "$subcmd" in _dist_has_source=0 if [[ "$dist_phys/" == "$src_root/"* ]]; then while IFS= read -r _existing; do - [[ "$_existing" == "$dist/.hawk-dist" || "$_existing" == "$dist/.hawk-generations/"* ]] || { _dist_has_source=1; break; } + [[ "$_existing" == "$dist/.hawk-dist" || "$_existing" == "$dist/.hawk-generations/"* ]] && continue + _existing_rel="${_existing#"$dist"/}" + if _marker_owns_output "$src_root" "$_existing_rel" || _marker_owns_output "$src_root" "$_existing_rel" compat; then + continue + fi + _dist_has_source=1 + break done < <(find "$dist" -type f -print) fi if [[ "$src_abs" == "$dist_phys" || "$src_abs" == "$dist_phys/"* || $_dist_has_source -eq 1 ]]; then @@ -187,8 +202,8 @@ case "$subcmd" in } _refresh_output_link() { - local link="$1" target="$2" actual tmp - if [[ -e "$link" && ! -L "$link" ]]; then + local link="$1" target="$2" owner_src="$3" rel="$4" kind="${5:-}" actual tmp + if [[ -e "$link" && ! -L "$link" ]] && ! _marker_owns_output "$owner_src" "$rel" "$kind"; then echo "[hawk-libs] desugar failed: stable output is not a symlink: $link" >&2 exit 1 fi @@ -387,10 +402,14 @@ case "$subcmd" in echo "[hawk-libs] desugar failed: output directory escapes dist via symlink: $compat_parent_phys" >&2 exit 1 fi - if [[ ( -e "$stable" && ! -L "$stable" ) || ( -e "$compat" && ! -L "$compat" ) ]]; then + if [[ -e "$stable" && ! -L "$stable" ]] && ! _marker_owns_output "$src_root" "$rel"; then echo "[hawk-libs] desugar failed: output path is not replaceable: $stable" >&2 exit 1 fi + if [[ "$compat" != "$stable" && -e "$compat" && ! -L "$compat" ]] && ! _marker_owns_output "$src_root" "$rel" compat; then + echo "[hawk-libs] desugar failed: output path is not replaceable: $compat" >&2 + exit 1 + fi if [[ -e "$compat" || -L "$compat" ]]; then compat_owned=0; compat_other=0; stable_owned=0; stable_other=0 if [[ -f "$dist/.hawk-dist" ]]; then @@ -647,8 +666,8 @@ case "$subcmd" in _rel="${_pub_rel[$i]}" _stable="$dist/${namespace:+$namespace/}$_rel" _compat="$dist/$_rel" - _refresh_output_link "$_stable" "$runtime_ns/$_rel" - [[ "$_compat" == "$_stable" ]] || _compat_is_skipped "$_rel" || _refresh_output_link "$_compat" "$runtime_ns/$_rel" + _refresh_output_link "$_stable" "$runtime_ns/$_rel" "$src_root" "$_rel" "" + [[ "$_compat" == "$_stable" ]] || _compat_is_skipped "$_rel" || _refresh_output_link "$_compat" "$runtime_ns/$_rel" "$src_root" "$_rel" compat done echo "$dist/${namespace:+$namespace/}$entry_rel" ;; diff --git a/tests/unit/desugar/run.sh b/tests/unit/desugar/run.sh index 5574a563..f8e9b023 100755 --- a/tests/unit/desugar/run.sh +++ b/tests/unit/desugar/run.sh @@ -410,10 +410,37 @@ set +e err=$(HAWK_DIST="$srctree/sub" "$LIBS" desugar "$srctree/main.awk" 2>&1 >/dev/null) st=$? set -e -if [[ "$st" -eq 1 && "$err" == *"refusing to overwrite"* ]]; then ok "dist_over_source_rejected"; else ng "dist_over_source_rejected" "exit=$st: $err"; fi +if [[ "$st" -eq 1 && "$err" == *"dist and source trees overlap"* ]]; then ok "dist_over_source_rejected"; else ng "dist_over_source_rejected" "exit=$st: $err"; fi after_hash=$(cksum < "$srctree/sub/main.awk") if [[ "$before_hash" == "$after_hash" ]]; then ok "dist_over_source_untouched"; else ng "dist_over_source_untouched" "source file was modified"; fi +# --- marker 所有の旧 flat regular file は generation symlink へ移行する --- +legacy_stable="$TMP/legacy-stable" +mkdir -p "$legacy_stable/dist" +printf 'BEGIN { print "new-stable" }\n' > "$legacy_stable/main.awk" +printf 'BEGIN { print "old-stable" }\n' > "$legacy_stable/dist/main.awk" +legacy_stable_src="$(cd "$legacy_stable" && pwd -P)" +printf '%s\tmain.awk\n' "$legacy_stable_src" > "$legacy_stable/dist/.hawk-dist" +set +e +err=$(cd "$legacy_stable" && HAWK_DIST=dist "$LIBS_ABS" desugar main.awk 2>&1 >/dev/null) +st=$? +set -e +if [[ "$st" -eq 0 && -L "$legacy_stable/dist/main.awk" && "$(gawk -f "$legacy_stable/dist/main.awk" "$legacy_compat_src/main.awk" +printf 'BEGIN { print "old-compat" }\n' > "$legacy_compat_dist/main.awk" +legacy_compat_src_phys="$(cd "$legacy_compat_src" && pwd -P)" +printf '%s\tmain.awk\tcompat\n' "$legacy_compat_src_phys" > "$legacy_compat_dist/.hawk-dist" +set +e +err=$(cd "$legacy_compat" && HAWK_DIST="$legacy_compat_dist" "$LIBS_ABS" desugar apps/a/main.awk 2>&1 >/dev/null) +st=$? +set -e +if [[ "$st" -eq 0 && -L "$legacy_compat_dist/main.awk" && -L "$legacy_compat_dist/apps/a/main.awk" && "$(gawk -f "$legacy_compat_dist/main.awk" /dev/null From 326025752c2341c4872844a60e26ca1df7f0a288 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Thu, 16 Jul 2026 14:26:30 +0900 Subject: [PATCH 16/20] =?UTF-8?q?fix:=20marker=20=E6=89=80=E6=9C=89=20dire?= =?UTF-8?q?ctory=20=E3=82=92=E6=97=A9=E6=9C=9F=E6=8B=92=E5=90=A6=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libexec/hawk-libs | 12 ++++++++++++ tests/unit/desugar/run.sh | 11 +++++++++++ 2 files changed, 23 insertions(+) diff --git a/libexec/hawk-libs b/libexec/hawk-libs index 08fb9040..9f325907 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -203,6 +203,10 @@ case "$subcmd" in _refresh_output_link() { local link="$1" target="$2" owner_src="$3" rel="$4" kind="${5:-}" actual tmp + if [[ -d "$link" && ! -L "$link" ]]; then + echo "[hawk-libs] desugar failed: stable output is not a symlink: $link" >&2 + exit 1 + fi if [[ -e "$link" && ! -L "$link" ]] && ! _marker_owns_output "$owner_src" "$rel" "$kind"; then echo "[hawk-libs] desugar failed: stable output is not a symlink: $link" >&2 exit 1 @@ -402,10 +406,18 @@ case "$subcmd" in echo "[hawk-libs] desugar failed: output directory escapes dist via symlink: $compat_parent_phys" >&2 exit 1 fi + if [[ -d "$stable" && ! -L "$stable" ]]; then + echo "[hawk-libs] desugar failed: output path is not replaceable: $stable" >&2 + exit 1 + fi if [[ -e "$stable" && ! -L "$stable" ]] && ! _marker_owns_output "$src_root" "$rel"; then echo "[hawk-libs] desugar failed: output path is not replaceable: $stable" >&2 exit 1 fi + if [[ "$compat" != "$stable" && -d "$compat" && ! -L "$compat" ]]; then + echo "[hawk-libs] desugar failed: output path is not replaceable: $compat" >&2 + exit 1 + fi if [[ "$compat" != "$stable" && -e "$compat" && ! -L "$compat" ]] && ! _marker_owns_output "$src_root" "$rel" compat; then echo "[hawk-libs] desugar failed: output path is not replaceable: $compat" >&2 exit 1 diff --git a/tests/unit/desugar/run.sh b/tests/unit/desugar/run.sh index f8e9b023..e7c7c2f6 100755 --- a/tests/unit/desugar/run.sh +++ b/tests/unit/desugar/run.sh @@ -441,6 +441,17 @@ st=$? set -e if [[ "$st" -eq 0 && -L "$legacy_compat_dist/main.awk" && -L "$legacy_compat_dist/apps/a/main.awk" && "$(gawk -f "$legacy_compat_dist/main.awk" "$legacy_dir/main.awk" +legacy_dir_src="$(cd "$legacy_dir" && pwd -P)" +printf '%s\tmain.awk\n' "$legacy_dir_src" > "$legacy_dir/dist/.hawk-dist" +set +e +err=$(cd "$legacy_dir" && HAWK_DIST=dist "$LIBS_ABS" desugar main.awk 2>&1 >/dev/null) +st=$? +set -e +if [[ "$st" -eq 1 && -d "$legacy_dir/dist/main.awk" && ! -e "$legacy_dir/dist/current" && ! -L "$legacy_dir/dist/current" ]]; then ok "marker_owned_directory_rejected_early"; else ng "marker_owned_directory_rejected_early" "exit=$st, current=$([[ -e "$legacy_dir/dist/current" || -L "$legacy_dir/dist/current" ]] && echo yes || echo no): $err"; fi + # --- marker のある dist への再実行 (上書き) は成功する --- dist="$TMP/distrerun" HAWK_DIST="$dist" "$LIBS" desugar "$FIX/proj/main.awk" >/dev/null From 7a9516f17db207d1104bb683edca00204f7925c9 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Thu, 16 Jul 2026 15:09:23 +0900 Subject: [PATCH 17/20] =?UTF-8?q?fix:=20=E6=AC=A0=E8=90=BD=E3=81=97?= =?UTF-8?q?=E3=81=9F=20compat=20=E3=81=AE=E6=89=80=E6=9C=89=E6=A8=A9?= =?UTF-8?q?=E3=82=92=E5=AE=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libexec/hawk-libs | 38 ++++++++++++++++++-------------------- tests/unit/desugar/run.sh | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/libexec/hawk-libs b/libexec/hawk-libs index 9f325907..0dde037d 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -422,26 +422,24 @@ case "$subcmd" in echo "[hawk-libs] desugar failed: output path is not replaceable: $compat" >&2 exit 1 fi - if [[ -e "$compat" || -L "$compat" ]]; then - compat_owned=0; compat_other=0; stable_owned=0; stable_other=0 - if [[ -f "$dist/.hawk-dist" ]]; then - while IFS=$'\t' read -r marker_src marker_rel marker_kind; do - [[ "$marker_rel" == "$rel" ]] || continue - if [[ "$marker_kind" == compat ]]; then - if [[ "$marker_src" == "$src_root" ]]; then compat_owned=1; else compat_other=1; fi - elif [[ -z "$marker_kind" ]]; then - if [[ "$marker_src" == "$src_root" ]]; then stable_owned=1; else stable_other=1; fi - fi - done < "$dist/.hawk-dist" - fi - if [[ "$compat_owned" -eq 1 && "$compat_other" -eq 0 ]]; then - : - elif [[ "$compat_owned" -eq 1 || "$compat_other" -eq 1 || "$stable_other" -eq 1 ]]; then - _compat_skip+=("$rel") - elif [[ "$stable_owned" -eq 0 ]]; then - echo "[hawk-libs] desugar failed: refusing to overwrite existing compatibility output: $compat" >&2 - exit 1 - fi + compat_owned=0; compat_other=0; stable_owned=0; stable_other=0 + if [[ -f "$dist/.hawk-dist" ]]; then + while IFS=$'\t' read -r marker_src marker_rel marker_kind; do + [[ "$marker_rel" == "$rel" ]] || continue + if [[ "$marker_kind" == compat ]]; then + if [[ "$marker_src" == "$src_root" ]]; then compat_owned=1; else compat_other=1; fi + elif [[ -z "$marker_kind" ]]; then + if [[ "$marker_src" == "$src_root" ]]; then stable_owned=1; else stable_other=1; fi + fi + done < "$dist/.hawk-dist" + fi + if [[ "$compat_owned" -eq 1 && "$compat_other" -eq 0 ]]; then + : + elif [[ "$compat_owned" -eq 1 || "$compat_other" -eq 1 || "$stable_other" -eq 1 ]]; then + _compat_skip+=("$rel") + elif [[ ( -e "$compat" || -L "$compat" ) && "$stable_owned" -eq 0 ]]; then + echo "[hawk-libs] desugar failed: refusing to overwrite existing compatibility output: $compat" >&2 + exit 1 fi if [[ -e "$stable" || -L "$stable" ]]; then if [[ ! -f "$dist/.hawk-dist" ]] || ! grep -qxF -- "$src_root"$'\t'"$rel" "$dist/.hawk-dist"; then diff --git a/tests/unit/desugar/run.sh b/tests/unit/desugar/run.sh index e7c7c2f6..e897c1f0 100755 --- a/tests/unit/desugar/run.sh +++ b/tests/unit/desugar/run.sh @@ -534,6 +534,24 @@ else ng "multi_entry_namespace_coexist" "a=$run_a, b=$run_b" fi +owner_a="$(cd "$multi_entry/apps/a" && pwd -P)" +owner_b="$(cd "$multi_entry/apps/b" && pwd -P)" +dist_missing_other="$TMP/dist-compat-missing-other" +( cd "$multi_entry" && HAWK_DIST="$dist_missing_other" "$LIBS_ABS" desugar apps/a/main.awk >/dev/null ) +rm "$dist_missing_other/main.awk" +set +e +( cd "$multi_entry" && HAWK_DIST="$dist_missing_other" "$LIBS_ABS" desugar apps/b/main.awk >/dev/null ) +st=$? +set -e +if [[ "$st" -eq 0 && ! -e "$dist_missing_other/main.awk" && ! -L "$dist_missing_other/main.awk" ]] && ! grep -qxF -- "$owner_b"$'\t'main.awk$'\t'compat "$dist_missing_other/.hawk-dist"; then ok "missing_compat_other_source_skips"; else ng "missing_compat_other_source_skips" "exit=$st, compat=$([[ -e "$dist_missing_other/main.awk" || -L "$dist_missing_other/main.awk" ]] && echo yes || echo no)"; fi + +dist_missing_self="$TMP/dist-compat-missing-self" +( cd "$multi_entry" && HAWK_DIST="$dist_missing_self" "$LIBS_ABS" desugar apps/a/main.awk >/dev/null ) +compat_self="$(readlink "$dist_missing_self/main.awk")" +rm "$dist_missing_self/main.awk" +( cd "$multi_entry" && HAWK_DIST="$dist_missing_self" "$LIBS_ABS" desugar apps/a/main.awk >/dev/null ) +if [[ -L "$dist_missing_self/main.awk" && "$(readlink "$dist_missing_self/main.awk")" == "$compat_self" ]] && grep -qxF -- "$owner_a"$'\t'main.awk$'\t'compat "$dist_missing_self/.hawk-dist"; then ok "missing_compat_owner_repairs"; else ng "missing_compat_owner_repairs"; fi + # --- marker は publish 済み rel のみ信頼し、同じ dist 配下の無関係な既存ファイルは # 引き続き上書き保護される (marker が空ファイルだと最初の成功後に # ディレクトリ全体を信頼してしまう問題の再現) --- From dcadd8b012fc4afd2db57fe7235e48d137a145c2 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Thu, 16 Jul 2026 16:39:59 +0900 Subject: [PATCH 18/20] =?UTF-8?q?fix:=20stable=20marker=20=E3=82=92=20name?= =?UTF-8?q?space=20=E3=81=94=E3=81=A8=E3=81=AB=E5=88=86=E9=9B=A2=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libexec/hawk-libs | 20 +++++++++++--------- tests/unit/desugar/run.sh | 22 ++++++++++++++++++++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/libexec/hawk-libs b/libexec/hawk-libs index 0dde037d..4a26ddcf 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -351,7 +351,7 @@ case "$subcmd" in # rel: src_root からの相対パス。desugar して dist/rel に出力し、 # src_root 相対で実在する @include を dist 接頭に書き換えて再帰する。 _desugar_one() { - local rel="$1" in out out_parent tmp inc incs prior marker_src marker_rel marker_kind + local rel="$1" in out out_parent tmp inc incs prior marker_src marker_rel marker_kind stable_rel local compat_owned compat_other stable_owned stable_other case "$_seen" in *" $rel "*) return 0 ;; esac case "$rel" in @@ -394,8 +394,9 @@ case "$subcmd" in set +f _seen="$_seen$rel " in="$src_root/$rel" - out="$stage_root/${namespace:+$namespace/}$rel" - stable="$dist/${namespace:+$namespace/}$rel" + stable_rel="${namespace:+$namespace/}$rel" + out="$stage_root/$stable_rel" + stable="$dist/$stable_rel" compat="$dist/$rel" compat_parent="$(dirname "$compat")" while [[ ! -e "$compat_parent" && ! -L "$compat_parent" && "$compat_parent" != "." && "$compat_parent" != "/" ]]; do @@ -410,7 +411,7 @@ case "$subcmd" in echo "[hawk-libs] desugar failed: output path is not replaceable: $stable" >&2 exit 1 fi - if [[ -e "$stable" && ! -L "$stable" ]] && ! _marker_owns_output "$src_root" "$rel"; then + if [[ -e "$stable" && ! -L "$stable" ]] && ! _marker_owns_output "$src_root" "$stable_rel"; then echo "[hawk-libs] desugar failed: output path is not replaceable: $stable" >&2 exit 1 fi @@ -442,7 +443,7 @@ case "$subcmd" in exit 1 fi if [[ -e "$stable" || -L "$stable" ]]; then - if [[ ! -f "$dist/.hawk-dist" ]] || ! grep -qxF -- "$src_root"$'\t'"$rel" "$dist/.hawk-dist"; then + if [[ ! -f "$dist/.hawk-dist" ]] || ! grep -qxF -- "$src_root"$'\t'"$stable_rel" "$dist/.hawk-dist"; then echo "[hawk-libs] desugar failed: refusing to overwrite existing namespaced output: $stable" >&2 exit 1 fi @@ -471,7 +472,7 @@ case "$subcmd" in # 後勝ちの desugar が前の source の成果物を無警告で上書きしてしまう。 # dist パス自体の一意化は dist レイアウト規約の再設計 (#119) 側の課題であり、 # ここでは異なる source からの同名 rel 上書きを拒否するだけに留める - if [[ ! -f "$dist/.hawk-dist" ]] || ! grep -qxF -- "$src_root"$'\t'"$rel" "$dist/.hawk-dist"; then + if [[ ! -f "$dist/.hawk-dist" ]] || ! grep -qxF -- "$src_root"$'\t'"$stable_rel" "$dist/.hawk-dist"; then echo "[hawk-libs] desugar failed: refusing to overwrite existing file in non-dist directory: $out (remove it or the whole directory if it is a stale dist)" >&2 exit 1 fi @@ -650,7 +651,7 @@ case "$subcmd" in _scratch_arr+=("$_marker_tmp") { [[ -f "$dist/.hawk-dist" ]] && cat "$dist/.hawk-dist" - for _rel in "${_pub_rel[@]}"; do printf '%s\t%s\n' "$src_root" "$_rel"; done + for _rel in "${_pub_rel[@]}"; do printf '%s\t%s\n' "$src_root" "${namespace:+$namespace/}$_rel"; done if [[ -n "$namespace" ]]; then for _rel in "${_pub_rel[@]}"; do _compat_is_skipped "$_rel" || printf '%s\t%s\tcompat\n' "$src_root" "$_rel" @@ -674,9 +675,10 @@ case "$subcmd" in mv -f "$_root_marker_tmp" "$dist/.hawk-dist" for ((i = 0; i < ${#_pub_rel[@]}; i++)); do _rel="${_pub_rel[$i]}" - _stable="$dist/${namespace:+$namespace/}$_rel" + _stable_rel="${namespace:+$namespace/}$_rel" + _stable="$dist/$_stable_rel" _compat="$dist/$_rel" - _refresh_output_link "$_stable" "$runtime_ns/$_rel" "$src_root" "$_rel" "" + _refresh_output_link "$_stable" "$runtime_ns/$_rel" "$src_root" "$_stable_rel" "" [[ "$_compat" == "$_stable" ]] || _compat_is_skipped "$_rel" || _refresh_output_link "$_compat" "$runtime_ns/$_rel" "$src_root" "$_rel" compat done echo "$dist/${namespace:+$namespace/}$entry_rel" diff --git a/tests/unit/desugar/run.sh b/tests/unit/desugar/run.sh index e897c1f0..7a19c02f 100755 --- a/tests/unit/desugar/run.sh +++ b/tests/unit/desugar/run.sh @@ -534,6 +534,28 @@ else ng "multi_entry_namespace_coexist" "a=$run_a, b=$run_b" fi +dist_namespace_owner="$TMP/dist-namespace-owner" +( cd "$multi_entry" && HAWK_DIST="$dist_namespace_owner" "$LIBS_ABS" desugar apps/a/main.awk >/dev/null ) +if ( cd "$multi_entry" && HAWK_DIST="$dist_namespace_owner" "$LIBS_ABS" desugar apps/a/main.awk >/dev/null ); then + ok "namespaced_same_cwd_rerun" +else + ng "namespaced_same_cwd_rerun" +fi +mkdir -p "$dist_namespace_owner/a" +unrelated_namespace_target="$TMP/unrelated-namespace-target.awk" +printf 'BEGIN { print "unrelated" }\n' > "$unrelated_namespace_target" +ln -s "$unrelated_namespace_target" "$dist_namespace_owner/a/main.awk" +unrelated_namespace_link="$(readlink "$dist_namespace_owner/a/main.awk")" +set +e +err=$(cd "$multi_entry/apps" && HAWK_DIST="$dist_namespace_owner" "$LIBS_ABS" desugar a/main.awk 2>&1 >/dev/null) +st=$? +set -e +if [[ "$st" -eq 1 && "$err" == *"refusing to overwrite existing namespaced output"* && -L "$dist_namespace_owner/a/main.awk" && "$(readlink "$dist_namespace_owner/a/main.awk")" == "$unrelated_namespace_link" ]]; then + ok "cross_cwd_namespace_marker_isolated" +else + ng "cross_cwd_namespace_marker_isolated" "exit=$st: $err" +fi + owner_a="$(cd "$multi_entry/apps/a" && pwd -P)" owner_b="$(cd "$multi_entry/apps/b" && pwd -P)" dist_missing_other="$TMP/dist-compat-missing-other" From 41314c024ef2a40ebd40af24bc3a600d6c05b3c8 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Thu, 16 Jul 2026 17:37:45 +0900 Subject: [PATCH 19/20] =?UTF-8?q?fix:=20app=20=E3=81=A8=20framework=20?= =?UTF-8?q?=E3=81=AE=20plugin=20=E3=82=92=E7=B5=B1=E5=90=88=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/plugin.awk | 24 ++++++++++++++----- docs/ja/plugins.ja.md | 2 ++ docs/plugins.md | 2 ++ libexec/hawk-libs | 40 +++++++++++++++++++++++++------ libexec/hawk-serve | 2 ++ libexec/hawk-supervise | 2 ++ tests/unit/cli/run.sh | 48 +++++++++++++++++++++++++++++++++++++- tests/unit/test_plugin.awk | 20 ++++++++++++---- 8 files changed, 121 insertions(+), 19 deletions(-) diff --git a/core/plugin.awk b/core/plugin.awk index d0d7dc06..0c688685 100644 --- a/core/plugin.awk +++ b/core/plugin.awk @@ -17,12 +17,25 @@ # config_keys を満たさなかった場合 PLUGIN_REGISTER_ERROR=1 を立てる # (起動時 main loop が exit 1 で判定する) -function plugin_discover( cmd, pname, func_name, meta, root, disabled) { - root = (ENVIRON["HAWK_LIB"] != "" ? ENVIRON["HAWK_LIB"] : ".") "/plugins" - cmd = "ls " _shellquote(root) " 2>/dev/null" - while ((cmd | getline pname) > 0) { +function plugin_discover( cmd, pname, func_name, meta, root, disabled, dirs, n, i, dir) { + if (ENVIRON["HAWK_PLUGIN_DIRS"] != "") { + n = split(ENVIRON["HAWK_PLUGIN_DIRS"], dirs, "\n") + } else { + root = (ENVIRON["HAWK_LIB"] != "" ? ENVIRON["HAWK_LIB"] : ".") "/plugins" + cmd = "ls " _shellquote(root) " 2>/dev/null" + while ((cmd | getline pname) > 0) { + if (pname == "" || pname ~ /^\./) continue + dirs[++n] = root "/" pname + } + close(cmd) + } + for (i = 1; i <= n; i++) { + dir = dirs[i] + sub(/\/+$/, "", dir) + pname = dir + sub(/^.*\//, "", pname) if (pname == "" || pname ~ /^\./) continue - disabled = root "/" pname "/.disabled" + disabled = dir "/.disabled" if ((getline _ < disabled) >= 0) { close(disabled) continue @@ -38,7 +51,6 @@ function plugin_discover( cmd, pname, func_name, meta, root, disabled) { @func_name(meta) plugin_register(pname, meta) } - close(cmd) } function plugin_register(pname, meta, keys, n, j, hooks, m, hook_name, k) { diff --git a/docs/ja/plugins.ja.md b/docs/ja/plugins.ja.md index 11abd632..8f9e9c2f 100644 --- a/docs/ja/plugins.ja.md +++ b/docs/ja/plugins.ja.md @@ -4,6 +4,8 @@ `plugins//` にプラグインディレクトリを配置すると、H-awk が起動時に自動発見してロードします。 +H-awk は `$HAWK_LIB/plugins`(framework 側)と `./plugins`(アプリの作業ディレクトリ)の両方からプラグインをロードします。同名プラグインが両方にある場合はアプリ側が優先され、アプリ側に `.disabled` があれば同名の framework 側プラグインも無効になります。 + ```awk # plugins/logger/manifest.awk function plugin_logger_manifest(meta) { diff --git a/docs/plugins.md b/docs/plugins.md index 08b08a59..65263f44 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -4,6 +4,8 @@ Drop a plugin directory into `plugins//`. H-awk auto-discovers and loads plugins at startup. +H-awk loads plugins from both `$HAWK_LIB/plugins` (the framework) and `./plugins` (the app working directory). If both contain the same plugin name, the app plugin wins; an app-side `.disabled` also shadows and disables the framework plugin of that name. + ```awk # plugins/logger/manifest.awk function plugin_logger_manifest(meta) { diff --git a/libexec/hawk-libs b/libexec/hawk-libs index 4a26ddcf..c6359248 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -5,17 +5,43 @@ HAWK_LIB="${HAWK_LIB:-$(cd "$(dirname "$0")/.." && pwd)}" subcmd="${1:-}" shift +_plugin_dirs() { + local app_root fw_root d name same_root=0 + app_root="$(pwd -P)/plugins" + fw_root="$(cd "$HAWK_LIB" && pwd -P)/plugins" + if [[ "$fw_root" == "$app_root" ]] || { [[ -d "$fw_root" && -d "$app_root" ]] && [[ "$fw_root" -ef "$app_root" ]]; }; then + same_root=1 + fi + if [[ "$same_root" -eq 0 ]]; then + for d in "$fw_root"/*/; do + [[ -d "$d" ]] || continue + d="${d%/}" + name="${d##*/}" + [[ -d "$app_root/$name" ]] && continue + [[ -f "$d/.disabled" ]] && continue + printf '%s\n' "$d" + done + fi + for d in "$app_root"/*/; do + [[ -d "$d" ]] || continue + d="${d%/}" + [[ -f "$d/.disabled" ]] && continue + printf '%s\n' "$d" + done +} + case "$subcmd" in plugins) - for d in "$HAWK_LIB"/plugins/*/; do - [[ -d "$d" ]] || continue - [[ -f "${d}.disabled" ]] && continue - name=$(basename "$d") - manifest="${d}manifest.awk" - impl="${d}${name}.awk" + while IFS= read -r d; do + name="${d##*/}" + manifest="$d/manifest.awk" + impl="$d/$name.awk" [[ -f "$manifest" ]] && printf '%s\n' "$manifest" [[ -f "$impl" ]] && printf '%s\n' "$impl" - done + done < <(_plugin_dirs) + ;; + plugin-dirs) + _plugin_dirs ;; libs) libs_args="" diff --git a/libexec/hawk-serve b/libexec/hawk-serve index f390b1e0..1ccd85fc 100755 --- a/libexec/hawk-serve +++ b/libexec/hawk-serve @@ -40,6 +40,8 @@ if [[ -f .env ]]; then set +a fi +HAWK_PLUGIN_DIRS="$("$LIBS" plugin-dirs)" +export HAWK_PLUGIN_DIRS mapfile -t _plugin_paths < <("$LIBS" plugins) _plugin_args=() for p in "${_plugin_paths[@]}"; do diff --git a/libexec/hawk-supervise b/libexec/hawk-supervise index b24f74fb..43183b1b 100755 --- a/libexec/hawk-supervise +++ b/libexec/hawk-supervise @@ -53,6 +53,8 @@ mv -f "$_runtime_tmp" "$HAWK_RUNTIME_AWK" export HAWK_RUNTIME_AWK eval "$("$LIBS" libs)" +HAWK_PLUGIN_DIRS="$("$LIBS" plugin-dirs)" +export HAWK_PLUGIN_DIRS mapfile -t _plugin_paths < <("$LIBS" plugins) _plugin_args=() for p in "${_plugin_paths[@]}"; do diff --git a/tests/unit/cli/run.sh b/tests/unit/cli/run.sh index 81d5e3fc..ebb6783a 100755 --- a/tests/unit/cli/run.sh +++ b/tests/unit/cli/run.sh @@ -100,6 +100,7 @@ rm -rf "$work" # framework 外の cwd でも framework 本体・plugin・native lib を HAWK_LIB から解決する work=$(mktemp -d) +work="$(cd "$work" && pwd -P)" mkdir -p "$work/data" printf 'name\noutside-cwd-ok\n' > "$work/data/items.tsv" printf 'BEGIN { if (read_tsv("data/items.tsv", rows) == 1) print rows[1, "name"] }\n' > "$work/app.awk" @@ -128,7 +129,9 @@ else fi framework="$work/framework" -mkdir -p "$framework/plugins/demo" "$framework/libs/net/zig-out/lib" +mkdir -p "$framework/plugins/base" "$framework/plugins/demo" "$framework/libs/net/zig-out/lib" +printf '# manifest\n' > "$framework/plugins/base/manifest.awk" +printf '# plugin\n' > "$framework/plugins/base/base.awk" printf '# manifest\n' > "$framework/plugins/demo/manifest.awk" printf '# plugin\n' > "$framework/plugins/demo/demo.awk" case "$(uname -s)" in Darwin) so_ext=dylib ;; *) so_ext=so ;; esac @@ -139,6 +142,49 @@ if [[ "$plugins_out" == *"$framework/plugins/demo/manifest.awk"* && "$plugins_ou else printf " FAIL: plugins_outside_hawk_lib (output=%s)\n" "$plugins_out"; FAIL=$((FAIL+1)) fi + +mkdir -p "$work/plugins/logger" +printf '# manifest\n' > "$work/plugins/logger/manifest.awk" +printf '# plugin\n' > "$work/plugins/logger/logger.awk" +mapfile -t plugin_paths < <(cd "$work" && HAWK_LIB="$framework" "$HAWK_ROOT/libexec/hawk-libs" plugins) +if [[ "${#plugin_paths[@]}" -eq 6 && "${plugin_paths[0]}" == "$framework/plugins/base/manifest.awk" && "${plugin_paths[2]}" == "$framework/plugins/demo/manifest.awk" && "${plugin_paths[4]}" == "$work/plugins/logger/manifest.awk" ]]; then + printf " PASS: plugins_dual_roots\n"; PASS=$((PASS+1)) +else + printf " FAIL: plugins_dual_roots (paths=%s)\n" "${plugin_paths[*]}"; FAIL=$((FAIL+1)) +fi + +mkdir -p "$work/plugins/demo" +printf '# app manifest\n' > "$work/plugins/demo/manifest.awk" +printf '# app plugin\n' > "$work/plugins/demo/demo.awk" +mapfile -t plugin_paths < <(cd "$work" && HAWK_LIB="$framework" "$HAWK_ROOT/libexec/hawk-libs" plugins) +if [[ "${#plugin_paths[@]}" -eq 6 && "${plugin_paths[2]}" == "$work/plugins/demo/manifest.awk" && "${plugin_paths[3]}" == "$work/plugins/demo/demo.awk" ]]; then + printf " PASS: plugins_app_shadow\n"; PASS=$((PASS+1)) +else + printf " FAIL: plugins_app_shadow (paths=%s)\n" "${plugin_paths[*]}"; FAIL=$((FAIL+1)) +fi + +touch "$work/plugins/demo/.disabled" +mapfile -t plugin_paths < <(cd "$work" && HAWK_LIB="$framework" "$HAWK_ROOT/libexec/hawk-libs" plugins) +if [[ "${#plugin_paths[@]}" -eq 4 && "${plugin_paths[0]}" == "$framework/plugins/base/manifest.awk" && "${plugin_paths[2]}" == "$work/plugins/logger/manifest.awk" ]]; then + printf " PASS: plugins_disabled_app_shadows\n"; PASS=$((PASS+1)) +else + printf " FAIL: plugins_disabled_app_shadows (paths=%s)\n" "${plugin_paths[*]}"; FAIL=$((FAIL+1)) +fi + +mapfile -t plugin_paths < <(cd "$work" && HAWK_LIB="$work" "$HAWK_ROOT/libexec/hawk-libs" plugins) +if [[ "${#plugin_paths[@]}" -eq 2 && "${plugin_paths[0]}" == "$work/plugins/logger/manifest.awk" && "${plugin_paths[1]}" == "$work/plugins/logger/logger.awk" ]]; then + printf " PASS: plugins_same_root_dedup\n"; PASS=$((PASS+1)) +else + printf " FAIL: plugins_same_root_dedup (paths=%s)\n" "${plugin_paths[*]}"; FAIL=$((FAIL+1)) +fi + +mapfile -t plugin_dirs < <(cd "$work" && HAWK_LIB="$framework" "$HAWK_ROOT/libexec/hawk-libs" plugin-dirs) +if [[ "${#plugin_dirs[@]}" -eq 2 && "${plugin_dirs[0]}" == "$framework/plugins/base" && "${plugin_dirs[1]}" == "$work/plugins/logger" ]]; then + printf " PASS: plugin_dirs_match_selection\n"; PASS=$((PASS+1)) +else + printf " FAIL: plugin_dirs_match_selection (dirs=%s)\n" "${plugin_dirs[*]}"; FAIL=$((FAIL+1)) +fi + libs_out=$(cd "$work" && HAWK_LIB="$framework" "$HAWK_ROOT/libexec/hawk-libs" libs) if [[ "$libs_out" == *"$framework/libs/net/zig-out/lib/libhawk_net.$so_ext"* && "$libs_out" == *"HAS_NET=1"* ]]; then printf " PASS: libs_outside_hawk_lib\n"; PASS=$((PASS+1)) diff --git a/tests/unit/test_plugin.awk b/tests/unit/test_plugin.awk index ba4032cf..c3335da1 100644 --- a/tests/unit/test_plugin.awk +++ b/tests/unit/test_plugin.awk @@ -111,19 +111,29 @@ function _test_plugin_discover_unknown_function_guard( cmd, line, output, rc) function test_plugin_discover_hawk_lib_root( cmd, line, output) { cmd = "tmp=$(mktemp -d); " \ "framework=\"$tmp/framework\"; app=\"$tmp/app\"; " \ - "mkdir -p \"$framework/plugins/demo\" \"$app/plugins/demo\" \"$app/plugins/ignored\"; " \ - "touch \"$app/plugins/demo/.disabled\"; " \ + "mkdir -p \"$framework/plugins/demo\" \"$app/plugins/local\"; " \ "printf 'function plugin_demo_manifest(meta) { meta[\"version\"] = \"1.2.3\" }\\n' > \"$framework/plugins/demo/manifest.awk\"; " \ + "printf 'function plugin_local_manifest(meta) { meta[\"version\"] = \"4.5.6\" }\\n' > \"$app/plugins/local/manifest.awk\"; " \ + "dirs=$(printf '%s\\n%s' \"$framework/plugins/demo\" \"$app/plugins/local\"); " \ + "(cd \"$app\" && HAWK_LIB=\"$framework\" HAWK_PLUGIN_DIRS=\"$dirs\" gawk " \ + "-f \"" ENVIRON["PWD"] "/core/util.awk\" " \ + "-f \"" ENVIRON["PWD"] "/core/plugin.awk\" " \ + "-f \"$framework/plugins/demo/manifest.awk\" " \ + "-f \"$app/plugins/local/manifest.awk\" " \ + "-e 'BEGIN { plugin_discover(); print \"ENV_FW:\" PLUGINS[\"demo\", \"version\"]; print \"ENV_APP:\" PLUGINS[\"local\", \"version\"]; exit 0 }') 2>&1; " \ "(cd \"$app\" && HAWK_LIB=\"$framework\" gawk " \ "-f \"" ENVIRON["PWD"] "/core/util.awk\" " \ "-f \"" ENVIRON["PWD"] "/core/plugin.awk\" " \ "-f \"$framework/plugins/demo/manifest.awk\" " \ - "-e 'BEGIN { plugin_discover(); print \"VERSION:\" PLUGINS[\"demo\", \"version\"]; print \"IGNORED:\" ((\"ignored\", \"version\") in PLUGINS); exit 0 }') 2>&1; " \ + "-f \"$app/plugins/local/manifest.awk\" " \ + "-e 'BEGIN { plugin_discover(); print \"FALLBACK_FW:\" PLUGINS[\"demo\", \"version\"]; print \"FALLBACK_APP:\" ((\"local\", \"version\") in PLUGINS); exit 0 }') 2>&1; " \ "rm -rf \"$tmp\"" output = "" while ((cmd | getline line) > 0) output = output line "\n" close(cmd) - assert_true(index(output, "VERSION:1.2.3\n") > 0, "plugin: discover uses HAWK_LIB root") - assert_true(index(output, "IGNORED:0\n") > 0, "plugin: discover ignores cwd plugins") + assert_true(index(output, "ENV_FW:1.2.3\n") > 0, "plugin: discover env includes framework root") + assert_true(index(output, "ENV_APP:4.5.6\n") > 0, "plugin: discover env includes app root") + assert_true(index(output, "FALLBACK_FW:1.2.3\n") > 0, "plugin: discover fallback uses HAWK_LIB root") + assert_true(index(output, "FALLBACK_APP:0\n") > 0, "plugin: discover fallback ignores cwd plugins") } From dbcc13e908f6e37654580ddf377abc750b8283c1 Mon Sep 17 00:00:00 2001 From: redpeacock78 Date: Sat, 25 Jul 2026 01:16:44 +0900 Subject: [PATCH 20/20] =?UTF-8?q?fix:=20=E6=97=A72=E5=88=97=20compat=20mar?= =?UTF-8?q?ker=20=E3=82=92=E7=A7=BB=E8=A1=8C=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libexec/hawk-libs | 5 ++++- tests/unit/desugar/run.sh | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/libexec/hawk-libs b/libexec/hawk-libs index c6359248..2f96ad64 100755 --- a/libexec/hawk-libs +++ b/libexec/hawk-libs @@ -142,7 +142,10 @@ case "$subcmd" in local owner_src="$1" rel="$2" kind="${3:-}" marker="$dist/.hawk-dist" [[ -f "$marker" ]] || return 1 if [[ "$kind" == compat ]]; then - grep -qxF -- "$owner_src"$'\t'"$rel"$'\t'compat "$marker" + # 旧2列行は同じ src_root が flat dist/ を publish した所有記録。 + # namespaced stable は ns/rel なので bare rel に一致せず、empty namespace は + # stable == compat の同一パス。他 src_root も行全体の完全一致で除外される。 + grep -qxF -- "$owner_src"$'\t'"$rel"$'\t'compat "$marker" || grep -qxF -- "$owner_src"$'\t'"$rel" "$marker" else grep -qxF -- "$owner_src"$'\t'"$rel" "$marker" fi diff --git a/tests/unit/desugar/run.sh b/tests/unit/desugar/run.sh index 7a19c02f..17cc1e33 100755 --- a/tests/unit/desugar/run.sh +++ b/tests/unit/desugar/run.sh @@ -441,6 +441,20 @@ st=$? set -e if [[ "$st" -eq 0 && -L "$legacy_compat_dist/main.awk" && -L "$legacy_compat_dist/apps/a/main.awk" && "$(gawk -f "$legacy_compat_dist/main.awk" "$legacy_flat_src/main.awk" +printf 'BEGIN { print "old-flat" }\n' > "$legacy_flat_dist/main.awk" +legacy_flat_src_phys="$(cd "$legacy_flat_src" && pwd -P)" +printf '%s\tmain.awk\n' "$legacy_flat_src_phys" > "$legacy_flat_dist/.hawk-dist" +set +e +err=$(cd "$legacy_flat" && HAWK_DIST=dist "$LIBS_ABS" desugar apps/a/main.awk 2>&1 >/dev/null) +st=$? +set -e +if [[ "$st" -eq 0 && -L "$legacy_flat_dist/main.awk" && -L "$legacy_flat_dist/apps/a/main.awk" && "$(gawk -f "$legacy_flat_dist/main.awk" "$legacy_dir/main.awk"