konpeito 0.3.0 → 0.3.1

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: 5a2c905a6cc383d9539c3d0c1377c4dc8b033636cf8c7b85987e6acb59e4c69c
4
- data.tar.gz: 2799693d9a59c0f0b90b0d11bc43da17bd78278aa88b11d3fd0796dfbad5f39b
3
+ metadata.gz: 1fe9ef9f2b988dc2d9751cecb6f53c2828e1e0d412cecc897557d7988e967b48
4
+ data.tar.gz: 68b6e211a60e31b48b61858b62902229b0980e786147b2e4427688f169c0caea
5
5
  SHA512:
6
- metadata.gz: 184994190193063c34ea651f672b04eeb9499e3d08da65cb3b09297ca321b2a0a645241f27fcf993871858ea17defbfdbe0674a4e0ced6298f1c4e595d36c700
7
- data.tar.gz: d8689a1392633e4ed8c29756259e6196c3571c68c4148058013b962b09ee1168fec3b6d095f29f8ae2c5aa1262bde0efa2651d4265480507044b51e5d4182c21
6
+ metadata.gz: 9dbf1f9e6edffb5e9e91af5398620fa96218f2ddde0cddebfa31ab4b90a42c0206cc71d24a6e2035dfd34f3695b7ba5265aef606c91af1b3cdee430609f537de
7
+ data.tar.gz: 7e595a3ec330e9f359bd5dc412b7d3c9fe9e9a3bef0fde8632f7b35fbe4f9902b174a93a25b11a5ea1e5364983f4ec8d1a92ff3e78a0aa5ebeeaf015bf2dd821
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+ require "fileutils"
6
+
7
+ module Konpeito
8
+ module Cache
9
+ # Manages compilation cache for `konpeito run`.
10
+ # Content-addressed: cache key is SHA256 of all inputs.
11
+ class RunCache
12
+ MANIFEST_FILE = "run_manifest.json"
13
+ DEFAULT_MAX_ENTRIES = 20
14
+
15
+ attr_reader :cache_dir
16
+
17
+ def initialize(cache_dir: ".konpeito_cache/run")
18
+ @cache_dir = File.expand_path(cache_dir)
19
+ @manifest = nil
20
+ load_manifest
21
+ end
22
+
23
+ # Compute a cache key from all compilation inputs.
24
+ def compute_cache_key(source_files:, rbs_files:, options_hash:)
25
+ digest = Digest::SHA256.new
26
+ source_files.sort.each { |f| digest.update(Digest::SHA256.file(f).hexdigest) }
27
+ rbs_files.sort.each { |f| digest.update(Digest::SHA256.file(f).hexdigest) }
28
+ digest.update(options_hash.sort.map { |k, v| "#{k}=#{v}" }.join("|"))
29
+ digest.update(Konpeito::VERSION)
30
+ digest.hexdigest
31
+ end
32
+
33
+ # Look up a cached artifact. Returns the path if it exists, nil otherwise.
34
+ def lookup(cache_key, basename)
35
+ entry = @manifest["entries"][cache_key]
36
+ return nil unless entry
37
+
38
+ artifact = artifact_path(cache_key, basename)
39
+ return nil unless File.exist?(artifact)
40
+
41
+ # Update last_used_at
42
+ entry["last_used_at"] = Time.now.iso8601
43
+ save_manifest
44
+ artifact
45
+ end
46
+
47
+ # Register a compiled artifact in the manifest.
48
+ # The artifact should already exist at artifact_dir(cache_key)/basename.
49
+ def store(cache_key, basename)
50
+ artifact = artifact_path(cache_key, basename)
51
+ return nil unless File.exist?(artifact)
52
+
53
+ @manifest["entries"][cache_key] = {
54
+ "basename" => basename,
55
+ "created_at" => Time.now.iso8601,
56
+ "last_used_at" => Time.now.iso8601
57
+ }
58
+ save_manifest
59
+ cleanup!
60
+ artifact
61
+ end
62
+
63
+ # Directory for a given cache key's artifact.
64
+ def artifact_dir(cache_key)
65
+ File.join(@cache_dir, cache_key)
66
+ end
67
+
68
+ # Full path to the artifact file.
69
+ def artifact_path(cache_key, basename)
70
+ File.join(artifact_dir(cache_key), basename)
71
+ end
72
+
73
+ # Remove all cached entries.
74
+ def clean!
75
+ FileUtils.rm_rf(@cache_dir)
76
+ FileUtils.mkdir_p(@cache_dir)
77
+ @manifest = create_empty_manifest
78
+ save_manifest
79
+ end
80
+
81
+ # Evict oldest entries beyond max_entries.
82
+ def cleanup!(max_entries: DEFAULT_MAX_ENTRIES)
83
+ entries = @manifest["entries"]
84
+ return if entries.size <= max_entries
85
+
86
+ # Sort by last_used_at ascending (oldest first)
87
+ sorted = entries.sort_by { |_k, v| v["last_used_at"] || v["created_at"] || "" }
88
+ to_remove = sorted.first(entries.size - max_entries)
89
+
90
+ to_remove.each do |key, _|
91
+ dir = artifact_dir(key)
92
+ FileUtils.rm_rf(dir) if Dir.exist?(dir)
93
+ entries.delete(key)
94
+ end
95
+
96
+ save_manifest
97
+ end
98
+
99
+ private
100
+
101
+ def load_manifest
102
+ FileUtils.mkdir_p(@cache_dir)
103
+ manifest_path = File.join(@cache_dir, MANIFEST_FILE)
104
+
105
+ if File.exist?(manifest_path)
106
+ begin
107
+ @manifest = JSON.parse(File.read(manifest_path))
108
+ # Ensure entries key exists
109
+ @manifest["entries"] ||= {}
110
+ rescue JSON::ParserError
111
+ @manifest = create_empty_manifest
112
+ end
113
+ else
114
+ @manifest = create_empty_manifest
115
+ end
116
+ end
117
+
118
+ def save_manifest
119
+ manifest_path = File.join(@cache_dir, MANIFEST_FILE)
120
+ # Write to tmp file then rename for atomicity
121
+ tmp_path = "#{manifest_path}.tmp.#{Process.pid}"
122
+ File.write(tmp_path, JSON.pretty_generate(@manifest))
123
+ File.rename(tmp_path, manifest_path)
124
+ rescue StandardError
125
+ # Best effort — don't crash if manifest write fails
126
+ FileUtils.rm_f(tmp_path) if tmp_path
127
+ end
128
+
129
+ def create_empty_manifest
130
+ {
131
+ "version" => Konpeito::VERSION,
132
+ "entries" => {}
133
+ }
134
+ end
135
+ end
136
+ end
137
+ end
@@ -4,5 +4,6 @@ module Konpeito
4
4
  module Cache
5
5
  autoload :CacheManager, "konpeito/cache/cache_manager"
6
6
  autoload :DependencyGraph, "konpeito/cache/dependency_graph"
7
+ autoload :RunCache, "konpeito/cache/run_cache"
7
8
  end
8
9
  end
@@ -102,7 +102,7 @@ module Konpeito
102
102
  ;;
103
103
  run)
104
104
  if [[ "${cur}" == -* ]]; then
105
- COMPREPLY=( $(compgen -W "--target --classpath --rbs -I --require-path --inline -v --verbose --no-color -h --help" -- "${cur}") )
105
+ COMPREPLY=( $(compgen -W "--target --classpath --rbs -I --require-path --inline --no-cache --clean-run-cache -v --verbose --no-color -h --help" -- "${cur}") )
106
106
  else
107
107
  COMPREPLY=( $(compgen -f -X '!*.rb' -- "${cur}") )
108
108
  fi
@@ -191,6 +191,8 @@ module Konpeito
191
191
  '-I[Add require search path]:path:_directories' \
192
192
  '--require-path[Add require search path]:path:_directories' \
193
193
  '--inline[Use inline RBS annotations]' \
194
+ '--no-cache[Force recompilation]' \
195
+ '--clean-run-cache[Clear run cache before building]' \
194
196
  '-v[Verbose output]' \
195
197
  '--verbose[Verbose output]' \
196
198
  '--no-color[Disable colored output]' \
@@ -278,6 +280,9 @@ module Konpeito
278
280
  complete -c konpeito -n '__fish_seen_subcommand_from run' -l classpath -r -d 'JVM classpath'
279
281
  complete -c konpeito -n '__fish_seen_subcommand_from run' -l rbs -r -d 'RBS type definition file'
280
282
  complete -c konpeito -n '__fish_seen_subcommand_from run' -s I -l require-path -r -d 'Add require search path'
283
+ complete -c konpeito -n '__fish_seen_subcommand_from run' -l inline -d 'Use inline RBS annotations'
284
+ complete -c konpeito -n '__fish_seen_subcommand_from run' -l no-cache -d 'Force recompilation'
285
+ complete -c konpeito -n '__fish_seen_subcommand_from run' -l clean-run-cache -d 'Clear run cache'
281
286
  complete -c konpeito -n '__fish_seen_subcommand_from run' -s v -l verbose -d 'Verbose output'
282
287
  complete -c konpeito -n '__fish_seen_subcommand_from run' -l no-color -d 'Disable colored output'
283
288
  complete -c konpeito -n '__fish_seen_subcommand_from run' -F -d 'Ruby source file'
@@ -49,7 +49,9 @@ module Konpeito
49
49
  rbs_paths: config.rbs_paths.dup,
50
50
  require_paths: config.require_paths.dup,
51
51
  inline_rbs: false,
52
- lib: false
52
+ lib: false,
53
+ no_cache: false,
54
+ clean_run_cache: false
53
55
  }
54
56
  end
55
57
 
@@ -74,6 +76,14 @@ module Konpeito
74
76
  options[:inline_rbs] = true
75
77
  end
76
78
 
79
+ opts.on("--no-cache", "Force recompilation (skip run cache)") do
80
+ options[:no_cache] = true
81
+ end
82
+
83
+ opts.on("--clean-run-cache", "Clear the run cache before building") do
84
+ options[:clean_run_cache] = true
85
+ end
86
+
77
87
  super
78
88
  end
79
89
 
@@ -82,7 +92,9 @@ module Konpeito
82
92
  Usage: konpeito run [options] [source.rb]
83
93
 
84
94
  Examples:
85
- konpeito run src/main.rb Build and run (native)
95
+ konpeito run src/main.rb Build and run (native, cached)
96
+ konpeito run --no-cache src/main.rb Force recompilation
97
+ konpeito run --clean-run-cache src/main.rb Clear cache, then build and run
86
98
  konpeito run --inline src/main.rb Build and run with inline RBS
87
99
  konpeito run --target jvm src/main.rb Build and run (JVM)
88
100
  BANNER
@@ -108,14 +120,101 @@ module Konpeito
108
120
  end
109
121
 
110
122
  def run_native(source_file)
123
+ require "konpeito/cache"
124
+
125
+ basename = "#{File.basename(source_file, '.rb')}#{Platform.shared_lib_extension}"
126
+ run_cache = Cache::RunCache.new
127
+
128
+ if options[:clean_run_cache]
129
+ run_cache.clean!
130
+ emit("Cleaned", "run cache")
131
+ end
132
+
133
+ if options[:no_cache]
134
+ build_and_run_tmpdir(source_file, basename)
135
+ return
136
+ end
137
+
138
+ # Compute cache key once (runs DependencyResolver)
139
+ cache_key = compute_run_cache_key(source_file, run_cache)
140
+
141
+ # Try cache hit
142
+ if cache_key
143
+ artifact = run_cache.lookup(cache_key, basename)
144
+ if artifact
145
+ emit("Cached", source_file)
146
+ emit("Running", artifact)
147
+ run_without_bundler("ruby", "-r", artifact, "-e", "")
148
+ return
149
+ end
150
+ end
151
+
152
+ # Cache miss: build into cache dir
153
+ if cache_key
154
+ build_and_run_cached(source_file, run_cache, cache_key, basename)
155
+ else
156
+ build_and_run_tmpdir(source_file, basename)
157
+ end
158
+ end
159
+
160
+ def compute_run_cache_key(source_file, run_cache)
161
+ resolver = DependencyResolver.new(
162
+ base_paths: options[:require_paths],
163
+ verbose: false
164
+ )
165
+ resolver.resolve(source_file)
166
+
167
+ all_sources = resolver.resolved_files.keys
168
+ auto_rbs = resolver.rbs_paths
169
+ all_rbs = (options[:rbs_paths].map { |p| File.expand_path(p) } + auto_rbs).uniq
170
+ all_rbs = all_rbs.select { |f| File.exist?(f) }
171
+
172
+ options_hash = {
173
+ "inline_rbs" => options[:inline_rbs].to_s,
174
+ "target" => "native"
175
+ }
176
+
177
+ run_cache.compute_cache_key(
178
+ source_files: all_sources,
179
+ rbs_files: all_rbs,
180
+ options_hash: options_hash
181
+ )
182
+ rescue StandardError => e
183
+ puts_verbose("Cache key computation failed: #{e.message}")
184
+ nil
185
+ end
186
+
187
+ def build_and_run_cached(source_file, run_cache, cache_key, basename)
188
+ dir = run_cache.artifact_dir(cache_key)
189
+ FileUtils.mkdir_p(dir)
190
+ output_file = File.join(dir, basename)
191
+
192
+ build_args = build_native_args(source_file, output_file)
193
+ Commands::BuildCommand.new(build_args, config: config).run
194
+
195
+ run_cache.store(cache_key, basename)
196
+
197
+ emit("Running", output_file)
198
+ run_without_bundler("ruby", "-r", output_file, "-e", "")
199
+ end
200
+
201
+ def build_and_run_tmpdir(source_file, basename)
111
202
  require "tmpdir"
112
203
 
113
- # Use a subdirectory to avoid conflicts, but keep the basename matching
114
- # the Init_ function name (Ruby requires Init_<basename> to match the filename)
115
204
  @run_tmpdir = File.join(Dir.tmpdir, "konpeito_run_#{Process.pid}")
116
205
  FileUtils.mkdir_p(@run_tmpdir)
117
- output_file = File.join(@run_tmpdir, "#{File.basename(source_file, '.rb')}#{Platform.shared_lib_extension}")
206
+ output_file = File.join(@run_tmpdir, basename)
207
+
208
+ build_args = build_native_args(source_file, output_file)
209
+ Commands::BuildCommand.new(build_args, config: config).run
210
+
211
+ emit("Running", output_file)
212
+ run_without_bundler("ruby", "-r", output_file, "-e", "")
213
+ ensure
214
+ FileUtils.rm_rf(@run_tmpdir) if @run_tmpdir && Dir.exist?(@run_tmpdir)
215
+ end
118
216
 
217
+ def build_native_args(source_file, output_file)
119
218
  build_args = ["-o", output_file]
120
219
  build_args << "-v" if options[:verbose]
121
220
  build_args << "--no-color" unless options[:color]
@@ -123,15 +222,7 @@ module Konpeito
123
222
  options[:rbs_paths].each { |p| build_args << "--rbs" << p }
124
223
  options[:require_paths].each { |p| build_args << "-I" << p }
125
224
  build_args << source_file
126
-
127
- Commands::BuildCommand.new(build_args, config: config).run
128
-
129
- emit("Running", output_file)
130
- # Run without Bundler environment so the compiled extension can load
131
- # any installed gem (not just those in the current Gemfile)
132
- run_without_bundler("ruby", "-r", output_file, "-e", "")
133
- ensure
134
- FileUtils.rm_rf(@run_tmpdir) if @run_tmpdir && Dir.exist?(@run_tmpdir)
225
+ build_args
135
226
  end
136
227
 
137
228
  def build_classpath
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Konpeito
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: konpeito
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yasushi Itoh
@@ -64,6 +64,7 @@ files:
64
64
  - lib/konpeito/cache.rb
65
65
  - lib/konpeito/cache/cache_manager.rb
66
66
  - lib/konpeito/cache/dependency_graph.rb
67
+ - lib/konpeito/cache/run_cache.rb
67
68
  - lib/konpeito/cli.rb
68
69
  - lib/konpeito/cli/base_command.rb
69
70
  - lib/konpeito/cli/build_command.rb