textus 0.14.2 → 0.14.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +15 -0
- data/lib/textus/cli/verb/build.rb +8 -6
- data/lib/textus/errors.rb +12 -0
- data/lib/textus/infra/build_lock.rb +59 -0
- data/lib/textus/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 65cfe5f73a64dda44280d1dd58e418aecdc94b79e1abb68340e973d364da3465
|
|
4
|
+
data.tar.gz: c35f1428a0965c11c946395a8fb7dfeb60202e2fac2514b1bb04dfae1e426ed2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: edeefd203bb0f4af807457cb96353affd3a21de846331481bb3d6e69e344fc1ea8054f8066ed278e7a7ced366704a65df00269d38b58a5b729d408477bc4f5a5
|
|
7
|
+
data.tar.gz: 4c30fa46409c381bfa8ef381c38314ce988786b9f2cf81c8b1c76f7508301587b9e5562ce1ced415f85484fe794fe3aeff5186203f733cedea34443d43a4eb57
|
data/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,21 @@ The **gem version** (`0.x.y`) is distinct from the **protocol version**
|
|
|
9
9
|
bump is a breaking change that requires a store migration; the gem version
|
|
10
10
|
tracks both additive improvements and breaking protocol bumps independently.
|
|
11
11
|
|
|
12
|
+
## 0.14.3 — 2026-05-26
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Top-level `flock(2)` mutex at `<root>/.build.lock` prevents concurrent
|
|
17
|
+
`textus build` invocations against the same store. A second build
|
|
18
|
+
while one is already running exits with code `75` (`EX_TEMPFAIL`) and
|
|
19
|
+
emits a `build_in_progress` error envelope, so wrappers like `rake
|
|
20
|
+
update` and CI can distinguish "another build is busy" from "this
|
|
21
|
+
build is broken". The lock is FD-bound and released by the kernel on
|
|
22
|
+
process death (including SIGKILL/OOM), so no stale-lock takeover
|
|
23
|
+
logic is needed. `close_on_exec` prevents the lock from leaking into
|
|
24
|
+
`bundle exec` and lefthook child processes. Per-key locks under
|
|
25
|
+
`.locks/` are unchanged. (#56)
|
|
26
|
+
|
|
12
27
|
## 0.14.2 — 2026-05-26
|
|
13
28
|
|
|
14
29
|
### Added
|
|
@@ -5,12 +5,14 @@ module Textus
|
|
|
5
5
|
option :prefix, "--prefix=K"
|
|
6
6
|
|
|
7
7
|
def call(store)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
Textus::Infra::BuildLock.with(root: store.root) do
|
|
9
|
+
ops = Textus::Operations.for(store, role: "builder")
|
|
10
|
+
build_res = ops.writes.build.call(prefix: prefix)
|
|
11
|
+
publish_res = ops.writes.publish.call(prefix: prefix)
|
|
12
|
+
emit({ "protocol" => Textus::PROTOCOL,
|
|
13
|
+
"built" => build_res["built"],
|
|
14
|
+
"published_leaves" => publish_res["published_leaves"] })
|
|
15
|
+
end
|
|
14
16
|
end
|
|
15
17
|
end
|
|
16
18
|
end
|
data/lib/textus/errors.rb
CHANGED
|
@@ -122,6 +122,18 @@ module Textus
|
|
|
122
122
|
def initialize(m) = super("io_error", m, exit_code: 64)
|
|
123
123
|
end
|
|
124
124
|
|
|
125
|
+
class BuildInProgress < Error
|
|
126
|
+
def initialize(holder)
|
|
127
|
+
super(
|
|
128
|
+
"build_in_progress",
|
|
129
|
+
"textus build already running (#{holder})",
|
|
130
|
+
details: { "holder" => holder },
|
|
131
|
+
exit_code: 75,
|
|
132
|
+
hint: "wait for the running build to finish, or check for a recursive hook trigger"
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
125
137
|
class UsageError < Error
|
|
126
138
|
def initialize(m, hint: nil) = super("usage", m, exit_code: 2, hint: hint)
|
|
127
139
|
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "socket"
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Textus
|
|
6
|
+
module Infra
|
|
7
|
+
class BuildLock
|
|
8
|
+
LOCK_FILENAME = ".build.lock"
|
|
9
|
+
MAX_HOLDER_BYTES = 512
|
|
10
|
+
|
|
11
|
+
def self.with(root:, &)
|
|
12
|
+
new(root: root).acquire_or_raise(&)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def initialize(root:)
|
|
16
|
+
@path = File.join(root, LOCK_FILENAME)
|
|
17
|
+
@file = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def acquire_or_raise
|
|
21
|
+
FileUtils.mkdir_p(File.dirname(@path))
|
|
22
|
+
@file = File.open(@path, File::RDWR | File::CREAT, 0o644)
|
|
23
|
+
@file.close_on_exec = true
|
|
24
|
+
|
|
25
|
+
unless @file.flock(File::LOCK_EX | File::LOCK_NB)
|
|
26
|
+
holder = read_holder_safely
|
|
27
|
+
@file.close
|
|
28
|
+
@file = nil
|
|
29
|
+
raise Textus::BuildInProgress.new(holder)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@file.truncate(0)
|
|
33
|
+
@file.write("pid=#{Process.pid} started=#{Time.now.utc.iso8601} host=#{Socket.gethostname}\n")
|
|
34
|
+
@file.flush
|
|
35
|
+
|
|
36
|
+
yield
|
|
37
|
+
ensure
|
|
38
|
+
release
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def release
|
|
44
|
+
return unless @file
|
|
45
|
+
|
|
46
|
+
@file.flock(File::LOCK_UN)
|
|
47
|
+
@file.close
|
|
48
|
+
@file = nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def read_holder_safely
|
|
52
|
+
content = File.read(@path, MAX_HOLDER_BYTES)
|
|
53
|
+
content.gsub(/[^[:print:]\t ]/, "").strip
|
|
54
|
+
rescue StandardError
|
|
55
|
+
"unknown"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
data/lib/textus/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.14.
|
|
4
|
+
version: 0.14.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -223,6 +223,7 @@ files:
|
|
|
223
223
|
- lib/textus/hooks/dsl.rb
|
|
224
224
|
- lib/textus/hooks/loader.rb
|
|
225
225
|
- lib/textus/hooks/registry.rb
|
|
226
|
+
- lib/textus/infra/build_lock.rb
|
|
226
227
|
- lib/textus/infra/clock.rb
|
|
227
228
|
- lib/textus/infra/event_bus.rb
|
|
228
229
|
- lib/textus/infra/publisher.rb
|