kompo 0.2.0 → 0.3.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.
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'fileutils'
5
+ require 'find'
6
+
7
+ module Kompo
8
+ # Struct to hold file data for embedding
9
+ KompoFile = Struct.new(:path, :bytes)
10
+
11
+ # Generate fs.c containing embedded file data
12
+ # Required context:
13
+ # - gems: Array of gem paths to embed
14
+ # - ruby_std_libs: Array of Ruby standard library paths to embed
15
+ # - bundler_config: Path to .bundle/config (if using Gemfile)
16
+ # Dependencies provide:
17
+ # - WorkDir.path
18
+ # - CopyProjectFiles.entrypoint_path
19
+ class MakeFsC < Taski::Task
20
+ exports :path
21
+
22
+ # File extensions to skip when embedding
23
+ SKIP_EXTENSIONS = %w[
24
+ .so .bundle .c .h .o .java .jar .gz .dat .sqlite3 .exe
25
+ .gem .out .png .jpg .jpeg .gif .bmp .ico .svg .webp .ttf .data
26
+ ].freeze
27
+
28
+ # Directory names to prune when traversing
29
+ PRUNE_DIRS = %w[.git ports logs spec .github docs exe _ruby].freeze
30
+
31
+ def run
32
+ @work_dir = WorkDir.path
33
+ @path = File.join(@work_dir, 'fs.c')
34
+
35
+ # Get original paths for Ruby standard library cache support
36
+ # When Ruby is restored from cache, standard library paths are from the original build
37
+ # We need to embed Ruby stdlib files with those original paths so the VFS can find them
38
+ # Project files and gems use current work_dir paths (no replacement needed)
39
+ @original_ruby_install_dir = InstallRuby.original_ruby_install_dir
40
+ @current_ruby_install_dir = InstallRuby.ruby_install_dir
41
+
42
+ # Initialize .kompoignore handler
43
+ project_dir = Taski.args.fetch(:project_dir, Taski.env.working_directory) || Taski.env.working_directory
44
+ @kompo_ignore = KompoIgnore.new(project_dir)
45
+ puts "Using .kompoignore from #{project_dir}" if @kompo_ignore.enabled?
46
+
47
+ @file_bytes = []
48
+ @paths = []
49
+ @file_sizes = [0]
50
+
51
+ group('Collecting files') do
52
+ embed_paths = collect_embed_paths
53
+
54
+ embed_paths.each do |embed_path|
55
+ expand_path = File.expand_path(embed_path)
56
+ unless File.exist?(expand_path)
57
+ warn "warn: #{expand_path} does not exist. Skipping."
58
+ next
59
+ end
60
+
61
+ if File.directory?(expand_path)
62
+ process_directory(expand_path)
63
+ else
64
+ add_file(expand_path)
65
+ end
66
+ end
67
+ puts "Collected #{@file_sizes.size - 1} files"
68
+ end
69
+
70
+ group('Generating fs.c') do
71
+ context = build_template_context
72
+
73
+ template_path = File.join(__dir__, '..', '..', 'fs.c.erb')
74
+ template = ERB.new(File.read(template_path))
75
+ File.write(@path, template.result(binding))
76
+ puts "Generated: fs.c (#{@file_bytes.size} bytes)"
77
+ end
78
+ end
79
+
80
+ def clean
81
+ return unless @path && File.exist?(@path)
82
+
83
+ FileUtils.rm_f(@path)
84
+ puts 'Cleaned up fs.c'
85
+ end
86
+
87
+ private
88
+
89
+ def collect_embed_paths
90
+ paths = []
91
+
92
+ # 1. Project files (entrypoint + additional files/directories specified by user)
93
+ # Created by: CopyProjectFiles
94
+ paths << CopyProjectFiles.entrypoint_path
95
+ paths += CopyProjectFiles.additional_paths
96
+
97
+ # 2. Gemfile and Gemfile.lock (if exists)
98
+ # Created by: CopyGemfile
99
+ if CopyGemfile.gemfile_exists
100
+ paths << File.join(@work_dir, 'Gemfile')
101
+ paths << File.join(@work_dir, 'Gemfile.lock')
102
+
103
+ # 3. Bundle directory (.bundle/config and bundle/ruby/X.Y.Z/gems/...)
104
+ # Created by: BundleInstall
105
+ paths << BundleInstall.bundler_config_path
106
+ paths << BundleInstall.bundle_ruby_dir
107
+ end
108
+
109
+ # 4. Ruby standard library
110
+ # Retrieved by: CheckStdlibs
111
+ paths += CheckStdlibs.paths
112
+
113
+ paths.compact
114
+ end
115
+
116
+ def process_directory(dir_path)
117
+ # Resolve base directory to ensure symlink safety
118
+ real_base = File.realpath(dir_path)
119
+
120
+ Find.find(dir_path) do |path|
121
+ # Prune certain directories
122
+ if File.directory?(path)
123
+ base = File.basename(path)
124
+ Find.prune if PRUNE_DIRS.any? { |d| base == d || path.end_with?("/#{d}") }
125
+ next
126
+ end
127
+
128
+ # Skip symlinks that escape the base directory (symlink traversal prevention)
129
+ if File.symlink?(path)
130
+ real_path = File.realpath(path)
131
+ unless real_path.start_with?(real_base)
132
+ warn "warn: Skipping symlink escaping base directory: #{path} -> #{real_path}"
133
+ next
134
+ end
135
+ end
136
+
137
+ # Skip certain file extensions
138
+ next if SKIP_EXTENSIONS.any? { |ext| path.end_with?(ext) }
139
+ next if path.end_with?('selenium-manager')
140
+
141
+ # Skip files matching .kompoignore patterns
142
+ next if should_ignore?(path)
143
+
144
+ add_file(path)
145
+ end
146
+ end
147
+
148
+ # Check if a file should be ignored based on .kompoignore patterns
149
+ # Only applies to files under work_dir (Ruby standard library is excluded)
150
+ def should_ignore?(absolute_path)
151
+ return false unless @kompo_ignore&.enabled?
152
+
153
+ # Only apply .kompoignore to work_dir files (project files and gems)
154
+ # Ruby standard library paths are outside work_dir and should not be filtered
155
+ return false unless absolute_path.start_with?(@work_dir)
156
+
157
+ relative_path = absolute_path.sub("#{@work_dir}/", '')
158
+ if @kompo_ignore.ignore?(relative_path)
159
+ puts "Ignoring (via .kompoignore): #{relative_path}"
160
+ true
161
+ else
162
+ false
163
+ end
164
+ end
165
+
166
+ def add_file(path)
167
+ # Keep original paths for VFS - the caching system already ensures
168
+ # the same work_dir path is reused across builds via metadata.json
169
+ # Ruby's $LOAD_PATH uses work_dir paths, so embedded files must match.
170
+ embedded_path = if @current_ruby_install_dir != @original_ruby_install_dir && path.start_with?(@current_ruby_install_dir)
171
+ # Ruby install dir path replacement for cache compatibility (when paths differ)
172
+ path.sub(@current_ruby_install_dir, @original_ruby_install_dir)
173
+ else
174
+ path
175
+ end
176
+
177
+ puts "#{path} -> #{embedded_path}" if path != embedded_path
178
+
179
+ # Use binread for binary-safe reading (preserves exact bytes without encoding conversion)
180
+ content = File.binread(path)
181
+ bytes = content.bytes
182
+ byte_size = content.bytesize
183
+ path_bytes = embedded_path.bytes << 0 # null-terminated
184
+
185
+ file = KompoFile.new(path_bytes, bytes)
186
+
187
+ @file_bytes.concat(file.bytes)
188
+ @paths.concat(file.path)
189
+ prev_size = @file_sizes.last
190
+ @file_sizes << (prev_size + byte_size)
191
+ end
192
+
193
+ def build_template_context
194
+ FsCTemplateContext.new(
195
+ work_dir: @work_dir
196
+ )
197
+ end
198
+
199
+ # Struct for fs.c template variables
200
+ FsCTemplateContext = Struct.new(:work_dir, keyword_init: true)
201
+ end
202
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'fileutils'
5
+
6
+ module Kompo
7
+ # Generate main.c from ERB template
8
+ # Required context:
9
+ # - exts: Array of [so_path, init_func] pairs from native gem builds
10
+ # Dependencies provide:
11
+ # - WorkDir.path
12
+ # - CopyProjectFiles.entrypoint_path
13
+ class MakeMainC < Taski::Task
14
+ exports :path
15
+
16
+ def run
17
+ work_dir = WorkDir.path
18
+ @path = File.join(work_dir, 'main.c')
19
+
20
+ return if File.exist?(@path)
21
+
22
+ template_path = File.join(__dir__, '..', '..', 'main.c.erb')
23
+ template = ERB.new(File.read(template_path))
24
+
25
+ # Build context object for ERB template
26
+ context = build_template_context
27
+
28
+ File.write(@path, template.result(binding))
29
+ puts 'Generated: main.c'
30
+ end
31
+
32
+ def clean
33
+ return unless @path && File.exist?(@path)
34
+
35
+ FileUtils.rm_f(@path)
36
+ puts 'Cleaned up main.c'
37
+ end
38
+
39
+ private
40
+
41
+ def build_template_context
42
+ project_dir = Taski.args.fetch(:project_dir, Taski.env.working_directory) || Taski.env.working_directory
43
+ work_dir = WorkDir.path
44
+ entrypoint = CopyProjectFiles.entrypoint_path
45
+
46
+ TemplateContext.new(
47
+ exts: BuildNativeGem.exts || [],
48
+ work_dir: work_dir,
49
+ work_dir_entrypoint: entrypoint,
50
+ project_dir: project_dir,
51
+ has_gemfile: CopyGemfile.gemfile_exists
52
+ )
53
+ end
54
+
55
+ # Simple struct to hold template variables
56
+ TemplateContext = Struct.new(
57
+ :exts,
58
+ :work_dir,
59
+ :work_dir_entrypoint,
60
+ :project_dir,
61
+ :has_gemfile,
62
+ keyword_init: true
63
+ )
64
+ end
65
+ end
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'shellwords'
5
+
6
+ module Kompo
7
+ # Section to compile the final binary.
8
+ # Switches implementation based on the current platform.
9
+ # Uses CollectDependencies's exported values for dependencies.
10
+ class Packing < Taski::Section
11
+ interfaces :output_path
12
+
13
+ def impl
14
+ macos? ? ForMacOS : ForLinux
15
+ end
16
+
17
+ # Common helper methods shared between macOS and Linux implementations
18
+ module CommonHelpers
19
+ private
20
+
21
+ def get_ruby_cflags(ruby_install_dir)
22
+ ruby_pc = File.join(ruby_install_dir, 'lib', 'pkgconfig', 'ruby.pc')
23
+ output, = Open3.capture2('pkg-config', '--cflags', ruby_pc, err: File::NULL)
24
+ Shellwords.split(output.chomp)
25
+ end
26
+
27
+ def get_ruby_mainlibs(ruby_install_dir)
28
+ ruby_pc = File.join(ruby_install_dir, 'lib', 'pkgconfig', 'ruby.pc')
29
+ output, = Open3.capture2('pkg-config', '--variable=MAINLIBS', ruby_pc, err: File::NULL)
30
+ output.chomp
31
+ end
32
+
33
+ def get_ldflags(work_dir, ruby_major_minor)
34
+ makefiles = Dir.glob("#{work_dir}/bundle/ruby/#{ruby_major_minor}.0/gems/*/ext/*/Makefile")
35
+ flags = makefiles.flat_map do |makefile|
36
+ content = File.read(makefile)
37
+ ldflags = content.scan(/^ldflags\s+= (.*)/).flatten
38
+ ldflags += content.scan(/^LDFLAGS\s+= (.*)/).flatten
39
+ ldflags.flat_map { |f| f.split(' ') }
40
+ end
41
+ flags.uniq.select { |f| f.start_with?('-L') }
42
+ end
43
+
44
+ def get_libpath(work_dir, ruby_major_minor)
45
+ makefiles = Dir.glob("#{work_dir}/bundle/ruby/#{ruby_major_minor}.0/gems/*/ext/*/Makefile")
46
+ makefiles.flat_map do |makefile|
47
+ content = File.read(makefile)
48
+ content.scan(/^LIBPATH = (.*)/).flatten
49
+ end.compact.flat_map { |p| p.split(' ') }.uniq
50
+ .reject { |p| p.start_with?('-Wl,-rpath,') || !p.start_with?('-L/') }
51
+ end
52
+
53
+ def get_extlibs(ruby_build_path, ruby_version)
54
+ exts_mk_files = Dir.glob(File.join(ruby_build_path, "ruby-#{ruby_version}", 'ext', '**', 'exts.mk'))
55
+ exts_mk_files.flat_map do |file|
56
+ File.read(file).scan(/^EXTLIBS\s+= (.*)/).flatten
57
+ end.compact.flat_map { |l| l.split(' ') }.uniq
58
+ end
59
+
60
+ def get_gem_libs(work_dir, ruby_major_minor)
61
+ makefiles = Dir.glob("#{work_dir}/bundle/ruby/#{ruby_major_minor}.0/gems/*/ext/*/Makefile")
62
+ makefiles.flat_map do |makefile|
63
+ File.read(makefile).scan(/^LIBS = (.*)/).flatten
64
+ end.compact.flat_map { |l| l.split(' ') }.uniq
65
+ .map { |l| l.start_with?('-l') ? l : "-l#{File.basename(l, '.a').delete_prefix('lib')}" }
66
+ end
67
+ end
68
+
69
+ # macOS implementation - compiles with clang and Homebrew paths
70
+ class ForMacOS < Taski::Task
71
+ include CommonHelpers
72
+
73
+ # macOS system libraries
74
+ SYSTEM_LIBS = %w[pthread m c].freeze
75
+ # macOS frameworks
76
+ FRAMEWORKS = %w[Foundation CoreFoundation Security].freeze
77
+
78
+ def run
79
+ work_dir = CollectDependencies.work_dir
80
+ deps = CollectDependencies.deps
81
+ ext_paths = CollectDependencies.ext_paths
82
+ enc_files = CollectDependencies.enc_files
83
+ @output_path = CollectDependencies.output_path
84
+
85
+ command = build_command(work_dir, deps, ext_paths, enc_files)
86
+
87
+ group('Compiling binary (macOS)') do
88
+ system(*command) or raise 'Failed to compile final binary'
89
+ puts "Binary size: #{File.size(@output_path) / 1024 / 1024} MB"
90
+ end
91
+
92
+ puts "Successfully created: #{@output_path}"
93
+ end
94
+
95
+ private
96
+
97
+ def build_command(work_dir, deps, ext_paths, enc_files)
98
+ ruby_static_lib = "-lruby.#{deps.ruby_major_minor}-static"
99
+
100
+ [
101
+ 'clang',
102
+ '-O3',
103
+ get_ruby_cflags(deps.ruby_install_dir),
104
+ # IMPORTANT: kompo_lib must come FIRST to override Homebrew-installed versions
105
+ "-L#{deps.kompo_lib}",
106
+ get_ldflags(work_dir, deps.ruby_major_minor),
107
+ "-L#{deps.ruby_lib}",
108
+ # Also add build path for static library lookup
109
+ "-L#{File.join(deps.ruby_build_path, "ruby-#{deps.ruby_version}")}",
110
+ # Add library paths for dependencies (Homebrew on macOS)
111
+ Shellwords.split(deps.deps_lib_paths),
112
+ get_libpath(work_dir, deps.ruby_major_minor),
113
+ '-fstack-protector-strong',
114
+ '-Wl,-dead_strip', # Remove unused code/data
115
+ '-Wl,-no_deduplicate', # Allow duplicate symbols from Ruby YJIT and kompo-vfs
116
+ '-Wl,-export_dynamic', # Export symbols to dynamic symbol table
117
+ deps.main_c,
118
+ deps.fs_c,
119
+ # Link kompo_wrap FIRST (before Ruby) to override libc symbols
120
+ '-lkompo_wrap',
121
+ ext_paths,
122
+ enc_files,
123
+ ruby_static_lib,
124
+ get_libs(deps.ruby_install_dir, work_dir, deps.ruby_build_path, deps.ruby_version, deps.ruby_major_minor),
125
+ '-o', @output_path
126
+ ].flatten
127
+ end
128
+
129
+ def get_libs(ruby_install_dir, work_dir, ruby_build_path, ruby_version, ruby_major_minor)
130
+ main_libs = get_ruby_mainlibs(ruby_install_dir)
131
+ ruby_std_gem_libs = get_extlibs(ruby_build_path, ruby_version)
132
+ gem_libs = get_gem_libs(work_dir, ruby_major_minor)
133
+
134
+ all_libs = [main_libs.split(' '), gem_libs, ruby_std_gem_libs].flatten
135
+ .select { |l| l.match?(/-l\w/) }.uniq
136
+ .reject { |l| l == '-ldl' } # macOS doesn't have libdl
137
+
138
+ # Separate system libs from other libs
139
+ other_libs = all_libs.reject { |l| SYSTEM_LIBS.any? { |sys| l == "-l#{sys}" } }
140
+
141
+ [
142
+ other_libs,
143
+ '-lkompo_fs',
144
+ # System libraries
145
+ SYSTEM_LIBS.map { |l| "-l#{l}" },
146
+ # Frameworks
147
+ FRAMEWORKS.flat_map { |f| ['-framework', f] }
148
+ ].flatten
149
+ end
150
+ end
151
+
152
+ # Linux implementation - compiles with gcc and pkg-config paths
153
+ class ForLinux < Taski::Task
154
+ include CommonHelpers
155
+
156
+ # Libraries that must be dynamically linked
157
+ DYN_LINK_LIBS = %w[pthread dl m c].freeze
158
+
159
+ def run
160
+ work_dir = CollectDependencies.work_dir
161
+ deps = CollectDependencies.deps
162
+ ext_paths = CollectDependencies.ext_paths
163
+ enc_files = CollectDependencies.enc_files
164
+ @output_path = CollectDependencies.output_path
165
+
166
+ command = build_command(work_dir, deps, ext_paths, enc_files)
167
+
168
+ group('Compiling binary (Linux)') do
169
+ system(*command) or raise 'Failed to compile final binary'
170
+ puts "Binary size: #{File.size(@output_path) / 1024 / 1024} MB"
171
+ end
172
+
173
+ puts "Successfully created: #{@output_path}"
174
+ end
175
+
176
+ private
177
+
178
+ def build_command(work_dir, deps, ext_paths, enc_files)
179
+ # Linux uses libruby-static.a (not libruby.X.Y-static.a like macOS)
180
+ ruby_static_lib = '-lruby-static'
181
+
182
+ [
183
+ 'gcc',
184
+ '-O3',
185
+ '-no-pie', # Required: Rust std lib is not built with PIC
186
+ get_ruby_cflags(deps.ruby_install_dir),
187
+ # IMPORTANT: kompo_lib must come FIRST to override system-installed versions
188
+ "-L#{deps.kompo_lib}",
189
+ get_ldflags(work_dir, deps.ruby_major_minor),
190
+ "-L#{deps.ruby_lib}",
191
+ # Also add build path for static library lookup
192
+ "-L#{File.join(deps.ruby_build_path, "ruby-#{deps.ruby_version}")}",
193
+ # Add library paths for dependencies (from pkg-config)
194
+ Shellwords.split(deps.deps_lib_paths),
195
+ get_libpath(work_dir, deps.ruby_major_minor),
196
+ '-fstack-protector-strong',
197
+ '-rdynamic', '-Wl,-export-dynamic',
198
+ deps.main_c,
199
+ deps.fs_c,
200
+ '-Wl,-Bstatic',
201
+ '-Wl,--start-group',
202
+ ext_paths,
203
+ enc_files,
204
+ ruby_static_lib,
205
+ get_libs(deps.ruby_install_dir, work_dir, deps.ruby_build_path, deps.ruby_version, deps.ruby_major_minor),
206
+ '-o', @output_path
207
+ ].flatten
208
+ end
209
+
210
+ def get_libs(ruby_install_dir, work_dir, ruby_build_path, ruby_version, ruby_major_minor)
211
+ main_libs = get_ruby_mainlibs(ruby_install_dir)
212
+ ruby_std_gem_libs = get_extlibs(ruby_build_path, ruby_version)
213
+ gem_libs = get_gem_libs(work_dir, ruby_major_minor)
214
+
215
+ dyn_link_libs = DYN_LINK_LIBS.map { |l| "-l#{l}" }
216
+
217
+ all_libs = [main_libs.split(' '), gem_libs, ruby_std_gem_libs].flatten
218
+ .select { |l| l.match?(/-l\w/) }.uniq
219
+
220
+ static_libs, dyn_libs = all_libs.partition { |l| !dyn_link_libs.include?(l) }
221
+
222
+ dyn_libs << '-lc'
223
+ dyn_libs.unshift('-Wl,-Bdynamic')
224
+
225
+ [static_libs, '-Wl,--end-group', '-lkompo_fs', '-lkompo_wrap', dyn_libs].flatten
226
+ end
227
+ end
228
+
229
+ private
230
+
231
+ def macos?
232
+ RUBY_PLATFORM.include?('darwin')
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+
5
+ module Kompo
6
+ # Section to get the ruby-build path.
7
+ # Switches implementation based on whether ruby-build is already installed.
8
+ class RubyBuildPath < Taski::Section
9
+ interfaces :path
10
+
11
+ def impl
12
+ ruby_build_installed? ? Installed : Install
13
+ end
14
+
15
+ # Use existing ruby-build installation
16
+ class Installed < Taski::Task
17
+ def run
18
+ path_output, = Open3.capture2('which', 'ruby-build', err: File::NULL)
19
+ @path = path_output.chomp
20
+ puts "ruby-build path: #{@path}"
21
+ version_output, = Open3.capture2(@path, '--version', err: File::NULL)
22
+ puts "ruby-build version: #{version_output.chomp}"
23
+ end
24
+ end
25
+
26
+ # Install ruby-build via git clone and return the path
27
+ class Install < Taski::Task
28
+ def run
29
+ puts 'ruby-build not found. Installing via git...'
30
+ install_dir = File.expand_path('~/.ruby-build')
31
+
32
+ if Dir.exist?(install_dir)
33
+ system('git', '-C', install_dir, 'pull', '--quiet')
34
+ else
35
+ system('git', 'clone', 'https://github.com/rbenv/ruby-build.git', install_dir)
36
+ end
37
+
38
+ @path = File.join(install_dir, 'bin', 'ruby-build')
39
+ raise 'Failed to install ruby-build' unless File.executable?(@path)
40
+
41
+ puts "ruby-build installed at: #{@path}"
42
+ version_output, = Open3.capture2(@path, '--version', err: File::NULL)
43
+ puts "ruby-build version: #{version_output.chomp}"
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def ruby_build_installed?
50
+ _, status = Open3.capture2('which', 'ruby-build', err: File::NULL)
51
+ status.success?
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+ require "json"
5
+ require "securerandom"
6
+ require "tmpdir"
7
+
8
+ module Kompo
9
+ # Create a temporary working directory and change into it
10
+ class WorkDir < Taski::Task
11
+ exports :path, :original_dir
12
+
13
+ # Marker file to identify Kompo-created work directories
14
+ MARKER_FILE = ".kompo_work_dir_marker"
15
+
16
+ def run
17
+ @original_dir = Dir.pwd
18
+
19
+ # Check if Ruby cache exists and use its work_dir path for $LOAD_PATH compatibility
20
+ ruby_version = Taski.args.fetch(:ruby_version, RUBY_VERSION)
21
+ kompo_cache = Taski.args.fetch(:kompo_cache, File.expand_path("~/.kompo/cache"))
22
+ cache_metadata_path = File.join(kompo_cache, ruby_version, "metadata.json")
23
+
24
+ if File.exist?(cache_metadata_path)
25
+ begin
26
+ metadata = JSON.parse(File.read(cache_metadata_path))
27
+ cached_work_dir = metadata["work_dir"]
28
+
29
+ if cached_work_dir
30
+ # Check if the directory exists and belongs to us (has marker) or doesn't exist at all
31
+ # In CI environments, the temp directory is cleaned between runs, so we recreate it
32
+ if Dir.exist?(cached_work_dir)
33
+ marker_path = File.join(cached_work_dir, MARKER_FILE)
34
+ if File.exist?(marker_path)
35
+ # Directory exists and has our marker - reuse it
36
+ @path = cached_work_dir
37
+ puts "Using cached work directory: #{@path}"
38
+ return
39
+ else
40
+ # Directory exists but wasn't created by Kompo - don't use it
41
+ warn "warn: #{cached_work_dir} exists but is not a Kompo work directory, creating new one"
42
+ end
43
+ else
44
+ # Directory doesn't exist - recreate it (common in CI after cache restore)
45
+ FileUtils.mkdir_p(cached_work_dir)
46
+ File.write(File.join(cached_work_dir, MARKER_FILE), "kompo-work-dir")
47
+ @path = cached_work_dir
48
+ puts "Recreated cached work directory: #{@path}"
49
+ return
50
+ end
51
+ end
52
+ rescue JSON::ParserError
53
+ # Fall through to create new work_dir
54
+ end
55
+ end
56
+
57
+ # No valid cache, create new work_dir
58
+ tmpdir = Dir.mktmpdir(SecureRandom.uuid)
59
+ # Resolve symlinks to get the real path
60
+ # On macOS, /var/folders is a symlink to /private/var/folders
61
+ # If we don't resolve this, paths won't match at runtime
62
+ @path = File.realpath(tmpdir)
63
+
64
+ # Create marker file to identify this as a Kompo work directory
65
+ File.write(File.join(@path, MARKER_FILE), "kompo-work-dir")
66
+
67
+ puts "Working directory: #{@path}"
68
+ end
69
+
70
+ def clean
71
+ return unless @path && Dir.exist?(@path)
72
+
73
+ # Only remove if marker file exists (confirms this is a Kompo work directory)
74
+ marker_path = File.join(@path, MARKER_FILE)
75
+ unless File.exist?(marker_path)
76
+ puts "Skipping cleanup: #{@path} is not a Kompo work directory"
77
+ return
78
+ end
79
+
80
+ FileUtils.rm_rf(@path)
81
+ puts "Cleaned up working directory: #{@path}"
82
+ end
83
+ end
84
+ end
data/lib/kompo/version.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kompo
4
- VERSION = "0.2.0"
4
+ VERSION = '0.3.0'
5
+ KOMPO_VFS_MIN_VERSION = '0.5.0'
5
6
  end