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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a732844e2441328f7519ffd93571c3ff611fd0514c2b1f927b5c4ada499db16
4
- data.tar.gz: 63a3e530439c37fbbff8c8b3864b1489cd34c88c88bd1c10f03ae980f4a66041
3
+ metadata.gz: 65cfe5f73a64dda44280d1dd58e418aecdc94b79e1abb68340e973d364da3465
4
+ data.tar.gz: c35f1428a0965c11c946395a8fb7dfeb60202e2fac2514b1bb04dfae1e426ed2
5
5
  SHA512:
6
- metadata.gz: 5470a6aaf29f1b081144b2eca3b1e87e5b1d465977422c967366196d259041cdbb8e0993ce293d87d7903f9446649f80ea99e1e6367b9731f7b839bf18f9d67f
7
- data.tar.gz: dcd3f57064d2f6753f049daf1315a937566e3b77a9ec130455a54410292f73ec55104bafe1e1b20431dc186a1c13735a7e6c6018ef8f54b77122f0eb2e267e8c
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
- ops = Textus::Operations.for(store, role: "builder")
9
- build_res = ops.writes.build.call(prefix: prefix)
10
- publish_res = ops.writes.publish.call(prefix: prefix)
11
- emit({ "protocol" => Textus::PROTOCOL,
12
- "built" => build_res["built"],
13
- "published_leaves" => publish_res["published_leaves"] })
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
@@ -1,4 +1,4 @@
1
1
  module Textus
2
- VERSION = "0.14.2"
2
+ VERSION = "0.14.3"
3
3
  PROTOCOL = "textus/3"
4
4
  end
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.2
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