cruise 0.1.0-x86_64-linux-gnu → 0.2.0-x86_64-linux-gnu
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/Cargo.lock +827 -0
- data/Cargo.toml +3 -0
- data/README.md +223 -0
- data/Rakefile +12 -5
- data/cruise.gemspec +11 -5
- data/ext/cruise/Cargo.toml +22 -0
- data/ext/cruise/build.rs +15 -0
- data/ext/cruise/cbindgen.toml +23 -0
- data/ext/cruise/cruise.c +158 -0
- data/ext/cruise/extconf.rb +113 -0
- data/ext/cruise/src/ffi.rs +124 -0
- data/ext/cruise/src/lib.rs +11 -0
- data/ext/cruise/src/watcher.rs +196 -0
- data/lib/cruise/3.2/cruise.so +0 -0
- data/lib/cruise/3.3/cruise.so +0 -0
- data/lib/cruise/3.4/cruise.so +0 -0
- data/lib/cruise/4.0/cruise.so +0 -0
- data/lib/cruise/event.rb +20 -0
- data/lib/cruise/version.rb +1 -1
- data/lib/cruise/watcher.rb +34 -0
- data/lib/cruise.rb +37 -6
- metadata +18 -5
data/Cargo.toml
ADDED
data/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>Cruise</h1>
|
|
3
|
+
<h4>A fast, OS-native file watcher for Ruby.</h4>
|
|
4
|
+
|
|
5
|
+
<p>
|
|
6
|
+
<a href="https://rubygems.org/gems/cruise"><img alt="Gem Version" src="https://img.shields.io/gem/v/cruise"></a>
|
|
7
|
+
<a href="https://github.com/marcoroth/cruise/blob/main/LICENSE.txt"><img alt="License" src="https://img.shields.io/github/license/marcoroth/cruise"></a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p>A file system watcher built on native OS events, not polling.<br/>Uses FSEvents on macOS and inotify on Linux.</p>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
Cruise was originally built to power file watching in the [Herb](https://herb-tools.dev) dev server, none of the existing Ruby watchers were quite reliable enough for the realtime hot reloading that a local development server and workflow needs. It also works standalone for any project that needs to react to file changes.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
**Add to your Gemfile:**
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "cruise"
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Or install directly:**
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
gem install cruise
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Precompiled native gems are available for macOS and Linux. If a precompiled gem isn't available for your platform, it will compile from source (requires Rust).
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### Basic Example
|
|
34
|
+
|
|
35
|
+
Watch a directory for changes:
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
require "cruise"
|
|
39
|
+
|
|
40
|
+
Cruise.watch("app/views") do |event|
|
|
41
|
+
puts "#{event.kind}: #{event.path}"
|
|
42
|
+
end
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The callback receives a `Cruise::Event` with two attributes:
|
|
46
|
+
|
|
47
|
+
| Attribute | Description |
|
|
48
|
+
|--------------|----------------------------------------------------------------------------------------|
|
|
49
|
+
| `event.path` | Absolute path to the changed file |
|
|
50
|
+
| `event.kind` | One of: `"created"`, `"modified"`, `"renamed"`, `"removed"`, `"accessed"`, `"changed"` |
|
|
51
|
+
|
|
52
|
+
### Multiple Directories
|
|
53
|
+
|
|
54
|
+
Watch several paths at once:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
Cruise.watch("app/views", "app/components", "lib/templates") do |event|
|
|
58
|
+
puts event.inspect
|
|
59
|
+
# => #<Cruise::Event kind="modified" path="/app/views/users/show.html.erb">
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Ruby Threads
|
|
64
|
+
|
|
65
|
+
Cruise releases the GVL while waiting for filesystem events, so Ruby threads run freely:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
Thread.new { do_background_work }
|
|
69
|
+
|
|
70
|
+
Cruise.watch("src") do |event|
|
|
71
|
+
puts event
|
|
72
|
+
end
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Glob Filtering
|
|
76
|
+
|
|
77
|
+
Only receive events for files matching a pattern:
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
Cruise.watch("app/views", glob: "**/*.html.erb") do |event|
|
|
81
|
+
puts event
|
|
82
|
+
end
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Multiple patterns:
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
Cruise.watch("app", glob: ["**/*.html.erb", "**/*.html"]) do |event|
|
|
89
|
+
puts event
|
|
90
|
+
end
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Debounce
|
|
94
|
+
|
|
95
|
+
Configure the debounce interval (default: 100ms):
|
|
96
|
+
|
|
97
|
+
```ruby
|
|
98
|
+
Cruise.watch("src", debounce: 0.5) do |event|
|
|
99
|
+
puts event
|
|
100
|
+
end
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Proc Callback
|
|
104
|
+
|
|
105
|
+
You can also pass a `Proc` via the `callback:` keyword:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
handler = proc { |event| puts event }
|
|
109
|
+
|
|
110
|
+
Cruise.watch("app/views", callback: handler)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Stopping
|
|
114
|
+
|
|
115
|
+
The watcher runs in a blocking loop. Use `Interrupt` to stop it cleanly:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
begin
|
|
119
|
+
Cruise.watch("app") do |event|
|
|
120
|
+
# process event
|
|
121
|
+
end
|
|
122
|
+
rescue Interrupt
|
|
123
|
+
puts "Stopped."
|
|
124
|
+
end
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Async / Fibers
|
|
128
|
+
|
|
129
|
+
`Cruise.watch` cooperates with Ruby's fiber scheduler, the API is identical whether you're on threads or fibers, with no separate "async mode." It waits on an internal pipe via `IO#wait_readable`, which yields to other fibers when a scheduler is active (for example inside [`Async`](https://github.com/socketry/async)) and otherwise blocks the thread with the GVL released.
|
|
130
|
+
|
|
131
|
+
```ruby
|
|
132
|
+
require "async"
|
|
133
|
+
require "cruise"
|
|
134
|
+
|
|
135
|
+
Async do |task|
|
|
136
|
+
task.async do
|
|
137
|
+
Cruise.watch("app/views", glob: "**/*.html.erb") do |event|
|
|
138
|
+
reload(event.path)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
task.async do
|
|
143
|
+
start_dev_server
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Cruise doesn't depend on `async`, it only relies on the standard `IO#wait_readable` hook, so any `Fiber::Scheduler` works.
|
|
149
|
+
|
|
150
|
+
#### Lower-level watcher
|
|
151
|
+
|
|
152
|
+
For custom event loops or manually-driven fibers, `Cruise::Watcher` exposes a readable `IO` and a non-blocking `#poll`:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
watcher = Cruise::Watcher.new("app/views", glob: "**/*.erb", debounce: 0.1)
|
|
156
|
+
|
|
157
|
+
begin
|
|
158
|
+
loop do
|
|
159
|
+
watcher.io.wait_readable
|
|
160
|
+
watcher.io.read_nonblock(4096, exception: false)
|
|
161
|
+
|
|
162
|
+
while (event = watcher.poll)
|
|
163
|
+
handle(event)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
ensure
|
|
167
|
+
watcher.close
|
|
168
|
+
end
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`Cruise::Watcher.new` takes the same paths keyword arguments as `Cruise.watch`, it's the watcher `Cruise.watch` runs internally.
|
|
172
|
+
|
|
173
|
+
## How It Works
|
|
174
|
+
|
|
175
|
+
Cruise is a Ruby C extension that links a Rust static library (`libcruise.a`) built around the [notify](https://github.com/notify-rs/notify) crate. The Rust core exposes a small C ABI (generated with [cbindgen](https://github.com/mozilla/cbindgen)) and knows nothing about Ruby. A thin hand-written C wrapper bridges that ABI to Ruby objects via the Ruby C API.
|
|
176
|
+
|
|
177
|
+
1. `Cruise.watch` sets up a [notify](https://github.com/notify-rs/notify) watcher with event debouncing (100ms)
|
|
178
|
+
2. A background thread monitors filesystem events using the OS-native API
|
|
179
|
+
3. The main loop calls `rb_thread_call_without_gvl` to wait without blocking Ruby
|
|
180
|
+
4. When an event arrives, the GVL is re-acquired and your callback is invoked
|
|
181
|
+
|
|
182
|
+
### Platform Backends
|
|
183
|
+
|
|
184
|
+
| Platform | Backend | API |
|
|
185
|
+
|----------|----------|--------------------------|
|
|
186
|
+
| macOS | FSEvents | `CoreServices` framework |
|
|
187
|
+
| Linux | inotify | `inotify_init1` syscall |
|
|
188
|
+
|
|
189
|
+
All backends watch recursively by default.
|
|
190
|
+
|
|
191
|
+
## Prior Art
|
|
192
|
+
|
|
193
|
+
Cruise builds on ideas from the Ruby ecosystem's existing file watchers:
|
|
194
|
+
|
|
195
|
+
- **[`listen`](https://github.com/guard/listen)** the long-standing workhorse behind Guard, Rails, and much of the ecosystem. Wraps [rb-fsevent](https://github.com/guard/rb-fsevent) (macOS) and [rb-inotify](https://github.com/guard/rb-inotify) (Linux), with a polling fallback for other platforms and network shares.
|
|
196
|
+
- **[`guard`](https://github.com/guard/guard)** a command-line tool, built on listen, that runs tasks (tests, reloads, builds) in response to file changes.
|
|
197
|
+
- **[`io-watch`](https://github.com/socketry/io-watch)** a newer, deliberately minimal library from the [socketry](https://github.com/socketry) project, offering one simple, unified directory-watching interface across platforms without per-platform multiplexing in application code.
|
|
198
|
+
|
|
199
|
+
Cruise shares `io-watch`'s preference for a single, native-events-only interface over platform multiplexing. On top of that it focuses on what a hot-reload loop needs: built-in debouncing, glob and event-kind filtering, and precompiled native gems so there's nothing to build at install time on supported platforms. It deliberately has no polling adapter, so filesystems where native events aren't delivered fall outside its scope, reach for [listen](https://github.com/guard/listen) there.
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
**Requirements:** Rust toolchain, Ruby 3.2+
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
git clone https://github.com/marcoroth/cruise
|
|
207
|
+
cd cruise
|
|
208
|
+
bundle install
|
|
209
|
+
bundle exec rake compile
|
|
210
|
+
bundle exec rake test
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Cross-compilation
|
|
214
|
+
|
|
215
|
+
Cruise uses [rake-compiler](https://github.com/rake-compiler/rake-compiler) and [rake-compiler-dock](https://github.com/rake-compiler/rake-compiler-dock) for building native gems:
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
bundle exec rake gem:native
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT License. See [LICENSE.txt](LICENSE.txt).
|
data/Rakefile
CHANGED
|
@@ -6,13 +6,12 @@ require "rake/testtask"
|
|
|
6
6
|
Rake::TestTask.new(:test) do |t|
|
|
7
7
|
t.libs << "test"
|
|
8
8
|
t.libs << "lib"
|
|
9
|
+
t.libs << "ext"
|
|
9
10
|
t.test_files = FileList["test/**/*_test.rb"]
|
|
10
11
|
end
|
|
11
12
|
|
|
12
13
|
begin
|
|
13
14
|
require "rake/extensiontask"
|
|
14
|
-
require "rb_sys"
|
|
15
|
-
require "rb_sys/extensiontask"
|
|
16
15
|
|
|
17
16
|
PLATFORMS = [
|
|
18
17
|
"aarch64-linux-gnu",
|
|
@@ -31,11 +30,15 @@ begin
|
|
|
31
30
|
"arm64-darwin" => "arm64-darwin",
|
|
32
31
|
"x86_64-darwin" => "x86_64-darwin",
|
|
33
32
|
"x86_64-linux-gnu" => "x86_64-linux",
|
|
34
|
-
"x86_64-linux-musl" => "x86_64-linux-musl"
|
|
33
|
+
"x86_64-linux-musl" => "x86_64-linux-musl",
|
|
35
34
|
}.freeze
|
|
36
35
|
|
|
37
|
-
|
|
36
|
+
Rake::ExtensionTask.new do |ext|
|
|
37
|
+
ext.name = "cruise"
|
|
38
|
+
ext.source_pattern = "*.{c,h}"
|
|
39
|
+
ext.ext_dir = "ext/cruise"
|
|
38
40
|
ext.lib_dir = "lib/cruise"
|
|
41
|
+
ext.gem_spec = Gem::Specification.load("cruise.gemspec")
|
|
39
42
|
ext.cross_compile = true
|
|
40
43
|
ext.cross_platform = PLATFORMS
|
|
41
44
|
end
|
|
@@ -60,10 +63,13 @@ begin
|
|
|
60
63
|
|
|
61
64
|
desc "Build the native gem for #{platform}"
|
|
62
65
|
task platform => "prepare" do
|
|
66
|
+
require "rb_sys"
|
|
67
|
+
|
|
63
68
|
rb_sys_platform = RB_SYS_PLATFORM_MAP.fetch(platform)
|
|
64
69
|
|
|
65
70
|
RakeCompilerDock.sh(
|
|
66
|
-
"
|
|
71
|
+
"export RCD_PLATFORM=#{platform} && " \
|
|
72
|
+
"bundle --local && rake native:#{platform} gem RUBY_CC_VERSION='#{ENV.fetch("RUBY_CC_VERSION", nil)}'",
|
|
67
73
|
platform: platform,
|
|
68
74
|
image: "rbsys/#{rb_sys_platform}:#{RbSys::VERSION}"
|
|
69
75
|
)
|
|
@@ -79,4 +85,5 @@ rescue LoadError => e
|
|
|
79
85
|
end
|
|
80
86
|
end
|
|
81
87
|
|
|
88
|
+
task test: :compile
|
|
82
89
|
task default: [:compile, :test]
|
data/cruise.gemspec
CHANGED
|
@@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = ["Marco Roth"]
|
|
9
9
|
spec.email = ["marco.roth@intergga.ch"]
|
|
10
10
|
|
|
11
|
-
spec.summary = "A fast, native file watcher for Ruby"
|
|
12
|
-
spec.description = "Cruise is a
|
|
11
|
+
spec.summary = "A fast, OS-native file watcher for Ruby"
|
|
12
|
+
spec.description = "Cruise is a file system watcher built on native OS events. Uses FSEvents on macOS and inotify on Linux."
|
|
13
13
|
spec.homepage = "https://github.com/marcoroth/cruise"
|
|
14
14
|
spec.license = "MIT"
|
|
15
15
|
|
|
@@ -19,10 +19,18 @@ Gem::Specification.new do |spec|
|
|
|
19
19
|
spec.files = Dir[
|
|
20
20
|
"cruise.gemspec",
|
|
21
21
|
"LICENSE.txt",
|
|
22
|
+
"README.md",
|
|
22
23
|
"Cargo.toml",
|
|
24
|
+
"Cargo.lock",
|
|
23
25
|
"Rakefile",
|
|
24
26
|
"lib/**/*.rb",
|
|
25
|
-
"ext
|
|
27
|
+
"ext/cruise/build.rs",
|
|
28
|
+
"ext/cruise/Cargo.toml",
|
|
29
|
+
"ext/cruise/cbindgen.toml",
|
|
30
|
+
"ext/cruise/cruise.c",
|
|
31
|
+
"ext/cruise/extconf.rb",
|
|
32
|
+
"ext/cruise/include/**/*.h",
|
|
33
|
+
"ext/cruise/src/**/*.rs"
|
|
26
34
|
]
|
|
27
35
|
|
|
28
36
|
spec.extensions = ["ext/cruise/extconf.rb"]
|
|
@@ -33,6 +41,4 @@ Gem::Specification.new do |spec|
|
|
|
33
41
|
spec.metadata["changelog_uri"] = "#{spec.homepage}/releases"
|
|
34
42
|
spec.metadata["source_code_uri"] = spec.homepage
|
|
35
43
|
spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
|
|
36
|
-
|
|
37
|
-
spec.add_dependency "rb_sys"
|
|
38
44
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "cruise"
|
|
3
|
+
version = "0.2.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
authors = ["Marco Roth <marco.roth@intergga.ch>"]
|
|
6
|
+
description = "A fast, OS-native file watcher for Ruby"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
publish = false
|
|
9
|
+
|
|
10
|
+
[lib]
|
|
11
|
+
name = "cruise"
|
|
12
|
+
path = "src/lib.rs"
|
|
13
|
+
crate-type = ["staticlib"]
|
|
14
|
+
|
|
15
|
+
[dependencies]
|
|
16
|
+
notify = { version = "7", features = ["macos_fsevent"] }
|
|
17
|
+
notify-debouncer-full = "0.4"
|
|
18
|
+
globset = "0.4"
|
|
19
|
+
libc = "0.2"
|
|
20
|
+
|
|
21
|
+
[build-dependencies]
|
|
22
|
+
cbindgen = "0.28"
|
data/ext/cruise/build.rs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
use std::env;
|
|
2
|
+
use std::fs;
|
|
3
|
+
use std::path::PathBuf;
|
|
4
|
+
|
|
5
|
+
fn main() {
|
|
6
|
+
let crate_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
|
|
7
|
+
let include_dir = PathBuf::from(&crate_dir).join("include");
|
|
8
|
+
let header_path = include_dir.join("cruise.h");
|
|
9
|
+
|
|
10
|
+
let _ = fs::create_dir_all(&include_dir);
|
|
11
|
+
|
|
12
|
+
if let Ok(bindings) = cbindgen::generate(&crate_dir) {
|
|
13
|
+
bindings.write_to_file(&header_path);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
language = "C"
|
|
2
|
+
header = """/* Generated by cbindgen, do not edit manually */
|
|
3
|
+
|
|
4
|
+
#include <stdbool.h>
|
|
5
|
+
#include <stdint.h>
|
|
6
|
+
#include <stddef.h>"""
|
|
7
|
+
include_guard = "CRUISE_H"
|
|
8
|
+
no_includes = true
|
|
9
|
+
|
|
10
|
+
[export]
|
|
11
|
+
include = [
|
|
12
|
+
"CruiseEvent",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[enum]
|
|
16
|
+
rename_variants = "ScreamingSnakeCase"
|
|
17
|
+
prefix_with_name = true
|
|
18
|
+
|
|
19
|
+
[fn]
|
|
20
|
+
sort_by = "None"
|
|
21
|
+
|
|
22
|
+
[parse]
|
|
23
|
+
parse_deps = false
|
data/ext/cruise/cruise.c
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#include <ruby.h>
|
|
2
|
+
#include <ruby/encoding.h>
|
|
3
|
+
#include <string.h>
|
|
4
|
+
#include "include/cruise.h"
|
|
5
|
+
|
|
6
|
+
static VALUE rb_mCruise;
|
|
7
|
+
static VALUE rb_cWatcher;
|
|
8
|
+
static VALUE rb_cEvent;
|
|
9
|
+
|
|
10
|
+
static VALUE make_utf8_string(const char *cstring) {
|
|
11
|
+
return rb_enc_str_new_cstr(cstring, rb_utf8_encoding());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static void fill_c_strings(VALUE array, const char **buffer) {
|
|
15
|
+
long length = RARRAY_LEN(array);
|
|
16
|
+
|
|
17
|
+
for (long index = 0; index < length; index++) {
|
|
18
|
+
VALUE string = rb_ary_entry(array, index);
|
|
19
|
+
|
|
20
|
+
buffer[index] = StringValueCStr(string);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static void watcher_free(void *pointer) {
|
|
25
|
+
if (pointer) {
|
|
26
|
+
cruise_watcher_free((struct Watcher *) pointer);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static size_t watcher_size(const void *pointer) {
|
|
31
|
+
(void) pointer;
|
|
32
|
+
|
|
33
|
+
return sizeof(void *);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static const rb_data_type_t watcher_type = {
|
|
37
|
+
.wrap_struct_name = "Cruise::Watcher",
|
|
38
|
+
.function = {
|
|
39
|
+
.dfree = watcher_free,
|
|
40
|
+
.dsize = watcher_size,
|
|
41
|
+
},
|
|
42
|
+
.flags = RUBY_TYPED_FREE_IMMEDIATELY,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
static VALUE watcher_alloc(VALUE klass) {
|
|
46
|
+
return TypedData_Wrap_Struct(klass, &watcher_type, NULL);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static struct Watcher *get_watcher(VALUE self) {
|
|
50
|
+
struct Watcher *watcher;
|
|
51
|
+
|
|
52
|
+
TypedData_Get_Struct(self, struct Watcher, &watcher_type, watcher);
|
|
53
|
+
|
|
54
|
+
return watcher;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
static VALUE watcher_initialize_native(VALUE self, VALUE paths, VALUE debounce, VALUE globs, VALUE only_kinds) {
|
|
58
|
+
long path_count = RARRAY_LEN(paths);
|
|
59
|
+
long glob_count = RARRAY_LEN(globs);
|
|
60
|
+
long only_count = RARRAY_LEN(only_kinds);
|
|
61
|
+
|
|
62
|
+
const char **path_pointers = path_count > 0 ? ALLOCA_N(const char *, path_count) : NULL;
|
|
63
|
+
const char **glob_pointers = glob_count > 0 ? ALLOCA_N(const char *, glob_count) : NULL;
|
|
64
|
+
const char **only_pointers = only_count > 0 ? ALLOCA_N(const char *, only_count) : NULL;
|
|
65
|
+
|
|
66
|
+
if (path_pointers) fill_c_strings(paths, path_pointers);
|
|
67
|
+
if (glob_pointers) fill_c_strings(globs, glob_pointers);
|
|
68
|
+
if (only_pointers) fill_c_strings(only_kinds, only_pointers);
|
|
69
|
+
|
|
70
|
+
int read_fd = -1;
|
|
71
|
+
char *error = NULL;
|
|
72
|
+
|
|
73
|
+
struct Watcher *watcher = cruise_watcher_new(
|
|
74
|
+
path_pointers, (size_t) path_count,
|
|
75
|
+
NUM2DBL(debounce),
|
|
76
|
+
glob_pointers, (size_t) glob_count,
|
|
77
|
+
only_pointers, (size_t) only_count,
|
|
78
|
+
&read_fd,
|
|
79
|
+
&error
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
if (!watcher) {
|
|
83
|
+
VALUE message = error ? make_utf8_string(error) : rb_str_new_cstr("Failed to create watcher");
|
|
84
|
+
|
|
85
|
+
if (error) cruise_string_free(error);
|
|
86
|
+
|
|
87
|
+
const char *message_cstr = StringValueCStr(message);
|
|
88
|
+
VALUE error_class = rb_eRuntimeError;
|
|
89
|
+
|
|
90
|
+
if (strstr(message_cstr, "does not exist") || strstr(message_cstr, "Invalid glob pattern")) {
|
|
91
|
+
error_class = rb_eArgError;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
rb_raise(error_class, "%s", message_cstr);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
RTYPEDDATA_DATA(self) = watcher;
|
|
98
|
+
|
|
99
|
+
VALUE io = rb_funcall(rb_cIO, rb_intern("for_fd"), 2, INT2NUM(read_fd), rb_str_new_cstr("r"));
|
|
100
|
+
rb_iv_set(self, "@io", io);
|
|
101
|
+
|
|
102
|
+
return self;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// watcher.io
|
|
106
|
+
static VALUE watcher_io(VALUE self) {
|
|
107
|
+
return rb_iv_get(self, "@io");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// watcher.poll
|
|
111
|
+
static VALUE watcher_poll(VALUE self) {
|
|
112
|
+
struct Watcher *watcher = get_watcher(self);
|
|
113
|
+
|
|
114
|
+
if (!watcher) return Qnil;
|
|
115
|
+
|
|
116
|
+
CruiseEvent event = { NULL, NULL };
|
|
117
|
+
|
|
118
|
+
if (!cruise_watcher_poll(watcher, &event)) return Qnil;
|
|
119
|
+
|
|
120
|
+
VALUE path = make_utf8_string(event.path);
|
|
121
|
+
VALUE kind = make_utf8_string(event.kind);
|
|
122
|
+
|
|
123
|
+
cruise_string_free(event.path);
|
|
124
|
+
cruise_string_free(event.kind);
|
|
125
|
+
|
|
126
|
+
return rb_funcall(rb_cEvent, rb_intern("new"), 2, path, kind);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// watcher.close
|
|
130
|
+
static VALUE watcher_close(VALUE self) {
|
|
131
|
+
struct Watcher *watcher = get_watcher(self);
|
|
132
|
+
|
|
133
|
+
if (watcher) {
|
|
134
|
+
cruise_watcher_free(watcher);
|
|
135
|
+
RTYPEDDATA_DATA(self) = NULL;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
VALUE io = rb_iv_get(self, "@io");
|
|
139
|
+
|
|
140
|
+
if (!NIL_P(io) && !RTEST(rb_funcall(io, rb_intern("closed?"), 0))) {
|
|
141
|
+
rb_funcall(io, rb_intern("close"), 0);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return Qnil;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
void Init_cruise(void) {
|
|
148
|
+
rb_mCruise = rb_define_module("Cruise");
|
|
149
|
+
rb_cEvent = rb_const_get(rb_mCruise, rb_intern("Event"));
|
|
150
|
+
|
|
151
|
+
rb_cWatcher = rb_define_class_under(rb_mCruise, "Watcher", rb_cObject);
|
|
152
|
+
|
|
153
|
+
rb_define_alloc_func(rb_cWatcher, watcher_alloc);
|
|
154
|
+
rb_define_private_method(rb_cWatcher, "initialize_native", watcher_initialize_native, 4);
|
|
155
|
+
rb_define_method(rb_cWatcher, "io", watcher_io, 0);
|
|
156
|
+
rb_define_method(rb_cWatcher, "poll", watcher_poll, 0);
|
|
157
|
+
rb_define_method(rb_cWatcher, "close", watcher_close, 0);
|
|
158
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mkmf"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
crate_dir = __dir__
|
|
7
|
+
root_dir = File.expand_path("../..", __dir__)
|
|
8
|
+
|
|
9
|
+
unless system("cargo --version > /dev/null 2>&1")
|
|
10
|
+
abort <<~MESSAGE
|
|
11
|
+
|
|
12
|
+
ERROR: Rust toolchain not found.
|
|
13
|
+
|
|
14
|
+
cruise requires the Rust toolchain to compile from source.
|
|
15
|
+
|
|
16
|
+
Install Rust: https://rustup.rs
|
|
17
|
+
|
|
18
|
+
MESSAGE
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
RUST_TARGETS = {
|
|
22
|
+
"aarch64-linux-gnu" => "aarch64-unknown-linux-gnu",
|
|
23
|
+
"aarch64-linux-musl" => "aarch64-unknown-linux-musl",
|
|
24
|
+
"arm-linux-gnu" => "armv7-unknown-linux-gnueabihf",
|
|
25
|
+
"arm-linux-musl" => "armv7-unknown-linux-musleabihf",
|
|
26
|
+
"arm64-darwin" => "aarch64-apple-darwin",
|
|
27
|
+
"x86_64-darwin" => "x86_64-apple-darwin",
|
|
28
|
+
"x86_64-linux-gnu" => "x86_64-unknown-linux-gnu",
|
|
29
|
+
"x86_64-linux-musl" => "x86_64-unknown-linux-musl",
|
|
30
|
+
"x86-linux-gnu" => "i686-unknown-linux-gnu",
|
|
31
|
+
"x86-linux-musl" => "i686-unknown-linux-musl",
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
cross_compiling = ENV.key?("RUBY_CC_VERSION")
|
|
35
|
+
target_platform = ENV.fetch("CARGO_BUILD_TARGET", nil)
|
|
36
|
+
|
|
37
|
+
if cross_compiling && target_platform.nil?
|
|
38
|
+
rcd_platform = ENV.fetch("RCD_PLATFORM", "")
|
|
39
|
+
target_platform = RUST_TARGETS[rcd_platform]
|
|
40
|
+
|
|
41
|
+
if target_platform.nil?
|
|
42
|
+
ruby_platform = RbConfig::CONFIG["arch"]
|
|
43
|
+
target_platform = RUST_TARGETS.values.find { |t| ruby_platform.include?(t.split("-").first) }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
workspace_target_dir = File.join(root_dir, "target")
|
|
48
|
+
crate_target_dir = File.join(crate_dir, "target")
|
|
49
|
+
|
|
50
|
+
if target_platform
|
|
51
|
+
puts "cruise: Cross-compiling Rust for target: #{target_platform}"
|
|
52
|
+
system("rustup target add #{target_platform}") || warn("cruise: Failed to add Rust target #{target_platform}")
|
|
53
|
+
|
|
54
|
+
cargo_args = "--release --target #{target_platform}"
|
|
55
|
+
else
|
|
56
|
+
puts "cruise: Compiling Rust library for native platform..."
|
|
57
|
+
|
|
58
|
+
cargo_args = "--release"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
unless system("cd #{root_dir} && cargo build #{cargo_args}")
|
|
62
|
+
abort "ERROR: Failed to compile cruise from Rust source."
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
lib_dir = if target_platform
|
|
66
|
+
[workspace_target_dir, crate_target_dir]
|
|
67
|
+
.map { |dir| File.join(dir, target_platform, "release") }
|
|
68
|
+
.find { |dir| Dir.exist?(dir) } || File.join(crate_target_dir, target_platform, "release")
|
|
69
|
+
else
|
|
70
|
+
[workspace_target_dir, crate_target_dir]
|
|
71
|
+
.map { |dir| File.join(dir, "release") }
|
|
72
|
+
.find { |dir| Dir.exist?(dir) } || File.join(crate_target_dir, "release")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
host_os = target_platform || RbConfig::CONFIG["host_os"]
|
|
76
|
+
|
|
77
|
+
lib_name = case host_os
|
|
78
|
+
when /darwin/ then "libcruise.dylib"
|
|
79
|
+
when /mingw|mswin|windows/ then "cruise.dll"
|
|
80
|
+
else "libcruise.so"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
static_lib = File.join(lib_dir, "libcruise.a")
|
|
84
|
+
|
|
85
|
+
if File.exist?(static_lib)
|
|
86
|
+
puts "cruise: Static library found at #{static_lib}"
|
|
87
|
+
$LDFLAGS << " #{static_lib}"
|
|
88
|
+
else
|
|
89
|
+
lib_path = File.join(lib_dir, lib_name)
|
|
90
|
+
|
|
91
|
+
unless File.exist?(lib_path)
|
|
92
|
+
abort "ERROR: cruise library not found at #{static_lib} or #{lib_path}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
puts "cruise: Shared library found at #{lib_path} (dynamic)"
|
|
96
|
+
|
|
97
|
+
$LDFLAGS << " -L#{lib_dir} -lcruise"
|
|
98
|
+
|
|
99
|
+
if host_os.match?(/darwin|linux/)
|
|
100
|
+
$LDFLAGS << " -Wl,-rpath,#{lib_dir}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
case host_os
|
|
105
|
+
when /darwin/
|
|
106
|
+
$LDFLAGS << " -framework CoreServices -framework CoreFoundation -framework Security"
|
|
107
|
+
when /linux/
|
|
108
|
+
$LDFLAGS << " -lpthread -ldl -lm"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
$CFLAGS << " -I#{File.join(__dir__, "include")}"
|
|
112
|
+
|
|
113
|
+
create_makefile("cruise/cruise")
|