planning-with-files 3.11.2 → 3.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,9 +6,10 @@
6
6
  .DESCRIPTION
7
7
  The ONLY sanctioned concurrent-safe writer of task_plan.md status lines. The
8
8
  orchestrator owns task_plan.md; workers NEVER edit it directly. The edit is
9
- a read-modify-write under an exclusive lock on the <plan-dir>\.write_lock
10
- sentinel, with an atomic temp-file + move swap so a torn write can never
11
- leave a half-rewritten plan on disk (architecture C4).
9
+ a read-modify-write under the portable
10
+ <plan-dir>\.pwf-locks\phase-status.lock directory lock, with an atomic
11
+ temp-file + move swap so a torn write can never leave a half-rewritten plan
12
+ on disk (architecture C4).
12
13
 
13
14
  Editing task_plan.md changes its SHA, so the orchestrator must re-attest at
14
15
  phase boundaries (see attest-plan.ps1).
@@ -76,6 +77,70 @@ function Resolve-PlanFile {
76
77
  return $null
77
78
  }
78
79
 
80
+ function Enter-PwfDirectoryLock {
81
+ param(
82
+ [string] $LockRoot,
83
+ [string] $LockDir
84
+ )
85
+
86
+ try {
87
+ [void][System.IO.Directory]::CreateDirectory($LockRoot)
88
+ } catch {
89
+ Write-Error ("[phase-status] Cannot create lock root " + $LockRoot + ": " + $_.Exception.Message)
90
+ return $null
91
+ }
92
+
93
+ $token = "phase-status-" + $PID + "-" + [Guid]::NewGuid().ToString("N")
94
+ $ownerFile = Join-Path $LockDir ".owner"
95
+ $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
96
+
97
+ $wait = [Diagnostics.Stopwatch]::StartNew()
98
+ while ($wait.Elapsed.TotalSeconds -lt 5) {
99
+ $createdByUs = $false
100
+ try {
101
+ New-Item -Path $LockDir -ItemType Directory -ErrorAction Stop | Out-Null
102
+ $createdByUs = $true
103
+ [System.IO.File]::WriteAllText($ownerFile, $token + "`n", $utf8NoBom)
104
+ return [PSCustomObject]@{
105
+ Directory = $LockDir
106
+ OwnerFile = $ownerFile
107
+ Token = $token
108
+ }
109
+ } catch {
110
+ if ($createdByUs) {
111
+ try {
112
+ if ([System.IO.File]::Exists($ownerFile)) {
113
+ $ownerValue = [System.IO.File]::ReadAllText($ownerFile).Trim()
114
+ if ([string]::Equals($ownerValue, $token, [StringComparison]::Ordinal)) {
115
+ [System.IO.File]::Delete($ownerFile)
116
+ }
117
+ }
118
+ [System.IO.Directory]::Delete($LockDir, $false)
119
+ } catch {
120
+ # Leave any directory we cannot prove is still ours intact.
121
+ }
122
+ }
123
+ Start-Sleep -Milliseconds 100
124
+ }
125
+ }
126
+
127
+ return $null
128
+ }
129
+
130
+ function Exit-PwfDirectoryLock {
131
+ param($Lock)
132
+ if (-not $Lock) { return }
133
+ try {
134
+ if (-not [System.IO.File]::Exists($Lock.OwnerFile)) { return }
135
+ $ownerValue = [System.IO.File]::ReadAllText($Lock.OwnerFile).Trim()
136
+ if (-not [string]::Equals($ownerValue, $Lock.Token, [StringComparison]::Ordinal)) { return }
137
+ [System.IO.File]::Delete($Lock.OwnerFile)
138
+ [System.IO.Directory]::Delete($Lock.Directory, $false)
139
+ } catch {
140
+ # Cleanup is best-effort and never removes a lock with another owner.
141
+ }
142
+ }
143
+
79
144
  # Validate phase number is a positive integer.
80
145
  if ($Phase -notmatch '^[0-9]+$') {
81
146
  Write-Error ("[phase-status] phase number must be a positive integer, got '" + $Phase + "'.")
@@ -96,18 +161,16 @@ if (-not $planFile) {
96
161
  }
97
162
 
98
163
  $planDir = Split-Path -Parent $planFile
99
- $lockFile = Join-Path $planDir ".write_lock"
100
-
101
- # Acquire an exclusive lock on the sentinel so concurrent writers serialize.
102
- $fs = $null
103
- $acquired = $false
104
- for ($i = 0; $i -lt 50 -and -not $acquired; $i++) {
105
- try {
106
- $fs = [System.IO.File]::Open($lockFile, [System.IO.FileMode]::OpenOrCreate, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
107
- $acquired = $true
108
- } catch {
109
- Start-Sleep -Milliseconds 100
110
- }
164
+ $lockRoot = Join-Path $planDir ".pwf-locks"
165
+ $lockDir = Join-Path $lockRoot "phase-status.lock"
166
+
167
+ # Atomic directory creation is the common lock primitive used by both the sh
168
+ # and PowerShell implementations. Failure to acquire within about five seconds
169
+ # is fail-closed: no plan read/rewrite is attempted.
170
+ $lock = Enter-PwfDirectoryLock -LockRoot $lockRoot -LockDir $lockDir
171
+ if (-not $lock) {
172
+ Write-Error ("[phase-status] Timed out waiting for lock " + $lockDir + ". No plan changes were made.")
173
+ exit 75
111
174
  }
112
175
 
113
176
  $tmpFile = $planFile + ".tmp." + $PID
@@ -164,8 +227,7 @@ try {
164
227
  Write-Error ("[phase-status] " + $_.Exception.Message)
165
228
  $rc = 1
166
229
  } finally {
167
- if ($fs) { $fs.Close(); $fs.Dispose() }
168
- if (Test-Path -LiteralPath $lockFile) { Remove-Item -LiteralPath $lockFile -Force -ErrorAction SilentlyContinue }
230
+ Exit-PwfDirectoryLock -Lock $lock
169
231
  if (Test-Path -LiteralPath $tmpFile) { Remove-Item -LiteralPath $tmpFile -Force -ErrorAction SilentlyContinue }
170
232
  }
171
233
 
@@ -3,9 +3,10 @@
3
3
  #
4
4
  # This is the ONLY sanctioned concurrent-safe writer of task_plan.md status
5
5
  # lines. The orchestrator owns task_plan.md; workers NEVER edit it directly.
6
- # All status edits go through this read-modify-write under an advisory flock on
7
- # the <plan-dir>/.write_lock sentinel, with an atomic temp-file + mv swap so a
8
- # torn write can never leave a half-rewritten plan on disk (architecture C4).
6
+ # All status edits go through this read-modify-write under the portable
7
+ # <plan-dir>/.pwf-locks/phase-status.lock directory lock, with an atomic
8
+ # temp-file + mv swap so a torn write can never leave a half-rewritten plan on
9
+ # disk (architecture C4).
9
10
  #
10
11
  # Note: editing task_plan.md changes its SHA, so the orchestrator must
11
12
  # re-attest at phase boundaries (see attest-plan.sh).
@@ -77,9 +78,62 @@ PLAN_FILE="$(resolve_plan_file)" || {
77
78
  }
78
79
 
79
80
  PLAN_DIR="$(dirname "${PLAN_FILE}")"
80
- LOCK_FILE="${PLAN_DIR}/.write_lock"
81
+ LOCK_ROOT="${PLAN_DIR}/.pwf-locks"
82
+ LOCK_DIR="${LOCK_ROOT}/phase-status.lock"
83
+ LOCK_TOKEN=""
84
+ LOCK_ACQUIRED=0
81
85
 
82
- # Confirm the phase heading exists before touching the file.
86
+ release_lock() {
87
+ if [ "${LOCK_ACQUIRED}" -ne 1 ] || [ -z "${LOCK_TOKEN}" ]; then
88
+ return 0
89
+ fi
90
+ owner_file="${LOCK_DIR}/.owner"
91
+ owner_value="$(cat "${owner_file}" 2>/dev/null || true)"
92
+ if [ "${owner_value}" = "${LOCK_TOKEN}" ]; then
93
+ rm -f "${owner_file}" 2>/dev/null || true
94
+ rmdir "${LOCK_DIR}" 2>/dev/null || true
95
+ fi
96
+ LOCK_ACQUIRED=0
97
+ }
98
+
99
+ acquire_lock() {
100
+ mkdir -p "${LOCK_ROOT}" 2>/dev/null || {
101
+ printf "[phase-status] Cannot create lock root %s.\n" "${LOCK_ROOT}" >&2
102
+ return 1
103
+ }
104
+ LOCK_TOKEN="phase-status-$$-$(date +%s 2>/dev/null || printf 0)"
105
+ started_at="$(date +%s 2>/dev/null || printf 0)"
106
+ attempts=0
107
+ while ! mkdir "${LOCK_DIR}" 2>/dev/null; do
108
+ attempts=$((attempts + 1))
109
+ now="$(date +%s 2>/dev/null || printf 0)"
110
+ if { [ "${started_at}" -gt 0 ] 2>/dev/null \
111
+ && [ $((now - started_at)) -ge 5 ]; } \
112
+ || [ "${attempts}" -ge 50 ]; then
113
+ printf "[phase-status] Timed out waiting for lock %s. No plan changes were made.\n" "${LOCK_DIR}" >&2
114
+ return 75
115
+ fi
116
+ sleep 0.1
117
+ done
118
+ if ! printf '%s\n' "${LOCK_TOKEN}" > "${LOCK_DIR}/.owner" 2>/dev/null; then
119
+ rmdir "${LOCK_DIR}" 2>/dev/null || true
120
+ printf "[phase-status] Cannot record lock ownership in %s.\n" "${LOCK_DIR}" >&2
121
+ return 1
122
+ fi
123
+ LOCK_ACQUIRED=1
124
+ return 0
125
+ }
126
+
127
+ trap 'release_lock' EXIT
128
+ trap 'release_lock; exit 1' HUP INT TERM
129
+
130
+ acquire_lock
131
+ lock_rc=$?
132
+ if [ "${lock_rc}" -ne 0 ]; then
133
+ exit "${lock_rc}"
134
+ fi
135
+
136
+ # Confirm the phase heading exists while holding the same lock as the rewrite.
83
137
  if ! grep -q "### Phase ${PHASE_NUM}\b" "${PLAN_FILE}" 2>/dev/null; then
84
138
  # Fall back to a looser match for headings like "### Phase 1:" where \b may
85
139
  # not be honored by a minimal grep.
@@ -137,17 +191,7 @@ do_write() {
137
191
  }
138
192
 
139
193
  rc=0
140
- if command -v flock >/dev/null 2>&1; then
141
- (
142
- flock -w 5 9 || true
143
- do_write
144
- ) 9>"${LOCK_FILE}" 2>/dev/null
145
- rc=$?
146
- rm -f "${LOCK_FILE}" 2>/dev/null || true
147
- else
148
- do_write
149
- rc=$?
150
- fi
194
+ do_write || rc=$?
151
195
 
152
196
  if [ "${rc}" -ne 0 ]; then
153
197
  rm -f "${TMP_FILE}" 2>/dev/null
@@ -18,6 +18,64 @@ param(
18
18
 
19
19
  $projectRoot = (Get-Location).Path
20
20
 
21
+ # Resolve-Path is lexical for Windows junctions: it can return the junction's
22
+ # spelling rather than the directory opened by the filesystem. Use a directory
23
+ # handle and GetFinalPathNameByHandleW on Windows so containment is decided from
24
+ # the object the kernel actually opened.
25
+ $script:IsWindowsHost = [Environment]::OSVersion.Platform -eq [PlatformID]::Win32NT
26
+ if ($script:IsWindowsHost -and -not ("PwfResolverNative" -as [type])) {
27
+ Add-Type -TypeDefinition @'
28
+ using System;
29
+ using System.ComponentModel;
30
+ using System.Runtime.InteropServices;
31
+ using System.Text;
32
+ using Microsoft.Win32.SafeHandles;
33
+
34
+ public static class PwfResolverNative {
35
+ private const uint FILE_SHARE_READ = 0x00000001;
36
+ private const uint FILE_SHARE_WRITE = 0x00000002;
37
+ private const uint FILE_SHARE_DELETE = 0x00000004;
38
+ private const uint OPEN_EXISTING = 3;
39
+ private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
40
+
41
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
42
+ private static extern SafeFileHandle CreateFileW(
43
+ string name, uint access, uint share, IntPtr security,
44
+ uint creation, uint flags, IntPtr template);
45
+
46
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
47
+ private static extern uint GetFinalPathNameByHandleW(
48
+ SafeFileHandle handle, StringBuilder path, uint length, uint flags);
49
+
50
+ public static string FinalDirectoryPath(string path) {
51
+ using (SafeFileHandle handle = CreateFileW(
52
+ path, 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
53
+ IntPtr.Zero, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, IntPtr.Zero)) {
54
+ if (handle.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error());
55
+ StringBuilder buffer = new StringBuilder(32768);
56
+ uint length = GetFinalPathNameByHandleW(handle, buffer, (uint)buffer.Capacity, 0);
57
+ if (length == 0 || length >= buffer.Capacity)
58
+ throw new Win32Exception(Marshal.GetLastWin32Error());
59
+ string result = buffer.ToString();
60
+ if (result.StartsWith(@"\\?\UNC\", StringComparison.OrdinalIgnoreCase))
61
+ return @"\\" + result.Substring(8);
62
+ if (result.StartsWith(@"\\?\", StringComparison.OrdinalIgnoreCase))
63
+ return result.Substring(4);
64
+ return result;
65
+ }
66
+ }
67
+ }
68
+ '@
69
+ }
70
+
71
+ function Get-FinalDirectoryPath {
72
+ param([string]$Path)
73
+ if ($script:IsWindowsHost) {
74
+ return [PwfResolverNative]::FinalDirectoryPath($Path)
75
+ }
76
+ return (Resolve-Path -LiteralPath $Path -ErrorAction Stop).Path
77
+ }
78
+
21
79
  # PWF_PLAN_ROOT: absolute plan-root binding (issue #212), mirroring
22
80
  # resolve-plan-dir.sh. A thread whose cwd is a shared PARENT of the real
23
81
  # project resolves the parent's plan and never sees the nested one;
@@ -30,9 +88,12 @@ $projectRoot = (Get-Location).Path
30
88
  # user-facing notice; stdout here is the data channel). Containment is then
31
89
  # checked against the pinned root. Unset keeps legacy behavior unchanged.
32
90
  if ($env:PWF_PLAN_ROOT) {
33
- if (Test-Path -LiteralPath $env:PWF_PLAN_ROOT -PathType Container) {
34
- $projectRoot = $env:PWF_PLAN_ROOT
35
- $PlanRoot = Join-Path $env:PWF_PLAN_ROOT ".planning"
91
+ $pin = $env:PWF_PLAN_ROOT
92
+ $isUnc = $pin.StartsWith('\\') -or $pin.StartsWith('//')
93
+ $isAbsolute = [System.IO.Path]::IsPathFullyQualified($pin)
94
+ if ($isAbsolute -and -not $isUnc -and (Test-Path -LiteralPath $pin -PathType Container)) {
95
+ $projectRoot = $pin
96
+ $PlanRoot = Join-Path $pin ".planning"
36
97
  } else {
37
98
  exit 0
38
99
  }
@@ -55,8 +116,8 @@ function Test-ValidSlug {
55
116
  function Test-WithinRoot {
56
117
  param([string]$Candidate)
57
118
  try {
58
- $rootReal = (Resolve-Path -LiteralPath $projectRoot -ErrorAction Stop).Path
59
- $candReal = (Resolve-Path -LiteralPath $Candidate -ErrorAction Stop).Path
119
+ $rootReal = Get-FinalDirectoryPath $projectRoot
120
+ $candReal = Get-FinalDirectoryPath $Candidate
60
121
  } catch {
61
122
  return $false
62
123
  }
@@ -79,8 +140,17 @@ if ($env:PLAN_ID) {
79
140
  }
80
141
  }
81
142
 
82
- if (Test-Path $activeFile) {
83
- $planId = (Get-Content $activeFile -Raw).Trim()
143
+ # Get-Item observes the link object even when its target is missing, unlike
144
+ # Test-Path which follows the target. An active pointer that is a directory or
145
+ # reparse point is an unsafe/ambiguous selector and must terminate resolution;
146
+ # falling through would silently select and expose the newest unrelated plan.
147
+ $activeItem = Get-Item -LiteralPath $activeFile -Force -ErrorAction SilentlyContinue
148
+ if ($activeItem) {
149
+ if ($activeItem.PSIsContainer -or
150
+ (($activeItem.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0)) {
151
+ exit 0
152
+ }
153
+ $planId = (Get-Content -LiteralPath $activeFile -Raw).Trim()
84
154
  if ($planId -and (Test-ValidSlug $planId)) {
85
155
  $candidate = Join-Path $PlanRoot $planId
86
156
  if ((Test-Path $candidate -PathType Container) -and (Test-WithinRoot $candidate)) {
@@ -31,7 +31,12 @@ PLAN_ROOT="${1:-${PWD}/.planning}"
31
31
  # With the variable unset, behavior is byte-identical to the legacy shape.
32
32
  PWF_ROOT_PIN=""
33
33
  if [ -n "${PWF_PLAN_ROOT:-}" ]; then
34
- if [ -d "${PWF_PLAN_ROOT}" ]; then
34
+ case "${PWF_PLAN_ROOT}" in
35
+ \\\\*|//*|[A-Za-z]:[!\\/]*) _pwf_pin_absolute=0 ;;
36
+ /*|[A-Za-z]:[\\/]*) _pwf_pin_absolute=1 ;;
37
+ *) _pwf_pin_absolute=0 ;;
38
+ esac
39
+ if [ "$_pwf_pin_absolute" = "1" ] && [ -d "${PWF_PLAN_ROOT}" ]; then
35
40
  PWF_ROOT_PIN="${PWF_PLAN_ROOT}"
36
41
  PLAN_ROOT="${PWF_PLAN_ROOT}/.planning"
37
42
  else
@@ -85,10 +90,54 @@ norm_slashes() {
85
90
  done
86
91
  }
87
92
 
93
+ # Return true when a candidate path names the Microsoft Store WindowsApps
94
+ # directory. Store app aliases are not stable interpreter binaries and may
95
+ # present as executable while refusing script execution. Matching is
96
+ # case-insensitive and works before or after Windows slash normalization.
97
+ is_windowsapps_path() {
98
+ norm_slashes "$1"
99
+ case "${NORM_OUT}" in
100
+ [Ww][Ii][Nn][Dd][Oo][Ww][Ss][Aa][Pp][Pp][Ss]|\
101
+ [Ww][Ii][Nn][Dd][Oo][Ww][Ss][Aa][Pp][Pp][Ss]/*|\
102
+ */[Ww][Ii][Nn][Dd][Oo][Ww][Ss][Aa][Pp][Pp][Ss]|\
103
+ */[Ww][Ii][Nn][Dd][Oo][Ww][Ss][Aa][Pp][Pp][Ss]/*) return 0 ;;
104
+ esac
105
+ return 1
106
+ }
107
+
108
+ # Select only an interpreter path the caller explicitly trusted.
109
+ # PWF_TRUSTED_PYTHON is preferred; PYTHON_BIN remains a compatibility alias.
110
+ # PATH discovery is intentionally forbidden because resolver hooks can run in
111
+ # repositories that control PATH. Windows-native absolute paths are converted
112
+ # with Git Bash's fixed system cygpath, never a PATH-selected shim.
113
+ trusted_python() {
114
+ for _tp_candidate in "${PWF_TRUSTED_PYTHON:-}" "${PYTHON_BIN:-}"; do
115
+ [ -n "${_tp_candidate}" ] || continue
116
+ case "${_tp_candidate}" in
117
+ \\\\*|//*) continue ;;
118
+ [A-Za-z]:[\\/]*)
119
+ is_windowsapps_path "${_tp_candidate}" && continue
120
+ _tp_cygpath="/usr/bin/cygpath.exe"
121
+ [ -f "${_tp_cygpath}" ] && [ -x "${_tp_cygpath}" ] || continue
122
+ _tp_candidate="$("${_tp_cygpath}" -u "${_tp_candidate}" 2>/dev/null)" \
123
+ || continue
124
+ ;;
125
+ /*) ;;
126
+ *) continue ;;
127
+ esac
128
+ is_windowsapps_path "${_tp_candidate}" && continue
129
+ [ -f "${_tp_candidate}" ] || continue
130
+ [ -x "${_tp_candidate}" ] || continue
131
+ printf "%s\n" "${_tp_candidate}"
132
+ return 0
133
+ done
134
+ return 1
135
+ }
136
+
88
137
  # Portable path canonicalizer. realpath first (Linux, modern coreutils),
89
- # then readlink -f (older GNU), then python3/python os.path.realpath. Prints
90
- # the canonical absolute path on success; prints nothing and returns 1 on a
91
- # full miss so the caller can decide what to do. No python spawn on the happy
138
+ # then readlink -f (older GNU), then an explicitly trusted Python interpreter.
139
+ # Prints the canonical absolute path on success; prints nothing and returns 1
140
+ # on a full miss so containment fails closed. No Python spawn on the happy
92
141
  # path: realpath/readlink cover Linux, WSL, Git-Bash, and modern macOS.
93
142
  canonicalize() {
94
143
  target="$1"
@@ -100,12 +149,9 @@ canonicalize() {
100
149
  out="$(readlink -f "${target}" 2>/dev/null)" && [ -n "${out}" ] && {
101
150
  printf "%s\n" "${out}"; return 0; }
102
151
  fi
103
- if command -v python3 >/dev/null 2>&1; then
104
- out="$(python3 -c "import os,sys;print(os.path.realpath(sys.argv[1]))" "${target}" 2>/dev/null)" \
105
- && [ -n "${out}" ] && { printf "%s\n" "${out}"; return 0; }
106
- fi
107
- if command -v python >/dev/null 2>&1; then
108
- out="$(python -c "import os,sys;print(os.path.realpath(sys.argv[1]))" "${target}" 2>/dev/null)" \
152
+ _canonical_python="$(trusted_python)" || _canonical_python=""
153
+ if [ -n "${_canonical_python}" ]; then
154
+ out="$("${_canonical_python}" -c "import os,sys;print(os.path.realpath(sys.argv[1]))" "${target}" 2>/dev/null)" \
109
155
  && [ -n "${out}" ] && { printf "%s\n" "${out}"; return 0; }
110
156
  fi
111
157
  return 1
@@ -175,7 +221,8 @@ is_within_root() {
175
221
  }
176
222
 
177
223
  # Portable mtime resolver. Tries GNU stat, BSD stat, BSD/macOS date -r,
178
- # python3, then perl. Returns "0" on full miss so callers can sort.
224
+ # then an explicitly trusted Python interpreter. Returns "0" on a full miss
225
+ # so newest-plan selection fails closed instead of executing from PATH.
179
226
  mtime_of() {
180
227
  target="$1"
181
228
  out="$(stat -c '%Y' "${target}" 2>/dev/null)"
@@ -184,16 +231,9 @@ mtime_of() {
184
231
  if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi
185
232
  out="$(date -r "${target}" +%s 2>/dev/null)"
186
233
  if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi
187
- if command -v python3 >/dev/null 2>&1; then
188
- out="$(python3 -c "import os,sys;print(int(os.stat(sys.argv[1]).st_mtime))" "${target}" 2>/dev/null)"
189
- if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi
190
- fi
191
- if command -v python >/dev/null 2>&1; then
192
- out="$(python -c "import os,sys;print(int(os.stat(sys.argv[1]).st_mtime))" "${target}" 2>/dev/null)"
193
- if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi
194
- fi
195
- if command -v perl >/dev/null 2>&1; then
196
- out="$(perl -e 'print((stat shift)[9])' "${target}" 2>/dev/null)"
234
+ _mtime_python="$(trusted_python)" || _mtime_python=""
235
+ if [ -n "${_mtime_python}" ]; then
236
+ out="$("${_mtime_python}" -c "import os,sys;print(int(os.stat(sys.argv[1]).st_mtime))" "${target}" 2>/dev/null)"
197
237
  if [ -n "${out}" ]; then printf "%s\n" "${out}"; return 0; fi
198
238
  fi
199
239
  printf "0\n"