hyperion-rb 2.15.0 → 2.16.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: 89ad0e591a8a44e23597ab8aa800ea65437c3e90d77903f7b3433c36368c87de
4
- data.tar.gz: b8a17ba534c62e8aa9d1b91f23e7baebf5a49d3074617fbcf73a328363e76aa4
3
+ metadata.gz: 8ee8566aa14b81d85a43cb2bdfad406f4f026836d230efcee7d4c386f98a1af8
4
+ data.tar.gz: 0c982adc33ba39f5e3b50179edaea24e9c82de40e6b11220f024a9d663e2e434
5
5
  SHA512:
6
- metadata.gz: 7e0059eba0f4d925bab72a1a7c4371e4e74c7336e62ac26135d3450480bfe6979ade740f0b7c236bfac67592eec40662ba0fca72c761cc2e208b8063dc6c04bb
7
- data.tar.gz: 5e38e2d43c8c8dea1f1dfce54639c7ea11377905a7193b3e87199c78f7aacccbed30218ec49ba1e1f39317541f361501671ded4b0d0875503806cc3680a9a856
6
+ metadata.gz: 772d23dde2abe5b97ec1e943f46e7744fd932d57e8e8488a9374fdcf42dec6c2479d315cdd7dc6b389d5f68c83fe8973acad0940108092009c81d8c7e8dbbd4f
7
+ data.tar.gz: abed58d154aea89829e1b9ef74c31027fa15d451ea51ed13c6736ae452e1fd06bf030dcfc5bc9bfa83dcb413b87e365e6c600bf62ac875f3fec488b54b58e39a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,113 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.16.1 — 2026-05-04
4
+
5
+ ### 2.16.1-A — macOS Obj-C fork-safety guard
6
+
7
+ **Why.** Operators landing on 2.16.0's `preload false` mode on macOS
8
+ were still hitting a second fork+macOS crash, distinct from the
9
+ Network.framework deadlock that 2.16.0 addressed. The Obj-C runtime's
10
+ post-fork sanity check trips when a worker touches a Foundation
11
+ class that was mid-initialization in the master at fork time:
12
+
13
+ ```
14
+ objc[…]: +[NSCharacterSet initialize] may have been in progress in
15
+ another thread when fork() was called. We cannot safely call it or
16
+ ignore it in the fork() child process. Crashing instead.
17
+ ```
18
+
19
+ `NSCharacterSet` (and friends) load transitively through OpenSSL,
20
+ the system resolver, and several other native gems. The result with
21
+ many workers (`WEB_CONCURRENCY=0` on a multi-core box): every fresh
22
+ worker crashes on first Foundation touch, master respawns, ad
23
+ infinitum until graceful_timeout fires.
24
+
25
+ The escape hatch on macOS is the documented env var
26
+ `OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES`, but it MUST be in the
27
+ process environment before any Foundation class loads — setting it
28
+ from `config/hyperion.rb` (parsed after gems load) is too late.
29
+
30
+ **What 2.16.1-A ships.** `bin/hyperion` and `lib/hyperion.rb` set
31
+ `ENV['OBJC_DISABLE_INITIALIZE_FORK_SAFETY'] ||= 'YES'` on `darwin`
32
+ unconditionally — before any `require` brings in Foundation-touching
33
+ code. `||=` honours operators who've set the var explicitly. No-op
34
+ on Linux/BSD; affects only macOS.
35
+
36
+ This is a defense-in-depth change, complementary to `preload false`:
37
+ `preload false` keeps Network.framework's resolver state out of the
38
+ master; this guard keeps Obj-C runtime fork-checks from crashing
39
+ the workers when something else (OpenSSL etc.) still touches
40
+ Foundation. Together they make the macOS dev experience match Linux.
41
+
42
+ **Verification.**
43
+
44
+ - `bin/check` green.
45
+ - Hand-reproduced the `NSCharacterSet` worker-crash loop on macOS;
46
+ no longer reproduces in 2.16.1.
47
+
48
+ ## 2.16.0 — 2026-05-04
49
+
50
+ ### 2.16-A — `preload false`: macOS fork+resolver escape hatch
51
+
52
+ **Why.** On macOS, Hyperion's "always preload" model deadlocks
53
+ post-fork DNS resolution for any deployment whose master process
54
+ ends up touching `Network.framework` — typically through native
55
+ gems loaded transitively via `Bundler.require` (OpenSSL session
56
+ caches, observability agents, Foundation-backed clients). The
57
+ master's `Network.framework` path evaluator initializes against XPC
58
+ peers in `mDNSResponder`; after `fork()` those XPC connections are
59
+ invalid in the workers, and the next `getaddrinfo` call hangs
60
+ forever inside `nw_path_evaluator_evaluate` →
61
+ `nw_nat64_v4_address_requires_synthesis`. The symptom: workers
62
+ spin at 99% CPU, requests TCP-connect but never get a response,
63
+ no per-request log line ever fires.
64
+ `OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES` does not help — that
65
+ flag only covers Foundation / Obj-C runtime init, not
66
+ `Network.framework`.
67
+
68
+ The reason Puma users don't hit this: Puma without explicit
69
+ `preload_app! true` runs in non-preload mode, so the master is a
70
+ thin supervisor and each worker loads the Rack app post-fork
71
+ against a clean process. Hyperion shipped no such option — preload
72
+ was the only mode.
73
+
74
+ **What 2.16-A ships.**
75
+
76
+ 1. **`preload` config field** (default `true` to preserve current
77
+ behaviour). When `preload false` is set in `config/hyperion.rb`
78
+ or `--no-preload` is passed on the CLI, the master never loads
79
+ `config.ru` and never calls `Hyperion.warmup!`. Each worker
80
+ parses the rackup itself post-fork via the same
81
+ `Hyperion::CLI.load_rack_app` path the master used to use; the
82
+ admin middleware wrap is preserved per-worker. `--preload` is
83
+ the inverse for argv overrides.
84
+
85
+ 2. **Single-worker mode is exempt.** With `workers <= 1` there is
86
+ no fork to protect against, so `run_single` always loads the
87
+ app in-process — deferring the parse buys nothing and would
88
+ surface lifecycle hooks against an unloaded app.
89
+
90
+ 3. **Master/Worker plumbing.** `Master.new` gains a `rackup_path:`
91
+ kwarg; `Worker.new` mirrors it and lazy-parses if `@app` is
92
+ `nil` and a path was provided. Both keep their existing
93
+ contract for the preload path (master receives `app:`, workers
94
+ inherit via fork).
95
+
96
+ **Trade-off.** Non-preload loses copy-on-write — each worker pays
97
+ the full Rails-boot RSS independently. Steady-state memory is N×
98
+ higher and worker boot is slower. The escape hatch is meant for
99
+ operators who hit the macOS deadlock; Linux users should leave it
100
+ at the default (`preload true`).
101
+
102
+ **Verification.**
103
+
104
+ - `bin/check` green (81 examples, 0 failures).
105
+ - Hand-reproduced against a Rails 8.1 application with a typical
106
+ native-gem stack (OpenSSL, observability agent, multi-A-record
107
+ DNS host for the database). Without `preload false`: deadlock
108
+ reproduces deterministically; CPU pegs at 99% per worker; curl
109
+ times out. With `preload false`: requests return promptly.
110
+
3
111
  ## 2.15.0 — 2026-05-02
4
112
 
5
113
  ### 2.15-A — Fresh bench, README split, CI flake fix
data/bin/check ADDED
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env bash
2
+ # Hyperion health-check. Run this BEFORE claiming a change is "all green".
3
+ #
4
+ # Usage:
5
+ # bin/check # default: compile + syntax + smoke specs (~10s)
6
+ # bin/check --quick # same as default
7
+ # bin/check --full # compile + syntax + full rspec (perf excluded)
8
+ # bin/check --syntax-only # compile + ruby -wc on lib/
9
+ # bin/check spec/path/foo_spec.rb [more...] # compile + targeted specs
10
+ #
11
+ # Exits non-zero on the first failing stage. Hooks-friendly: stdout is
12
+ # stage-tagged so .claude/bin/run-bounded.sh shows the right slice on tail.
13
+ #
14
+ # Smoke set covers the request hot path: parser, request, response writer,
15
+ # connection lifecycle, server loop, runtime. Each spec runs in <1s on a
16
+ # warm bundler cache; the full set lands in ~5–10s total.
17
+
18
+ set -uo pipefail
19
+
20
+ cd "$(dirname "$0")/.."
21
+
22
+ mode="quick"
23
+ specs=()
24
+ if [ $# -gt 0 ]; then
25
+ case "$1" in
26
+ --quick) mode="quick"; shift ;;
27
+ --full) mode="full"; shift ;;
28
+ --syntax-only) mode="syntax" ;;
29
+ -h|--help)
30
+ sed -n '2,15p' "$0"
31
+ exit 0 ;;
32
+ *) mode="targeted"; specs=("$@") ;;
33
+ esac
34
+ fi
35
+
36
+ stage() { printf '\n=== %s ===\n' "$1"; }
37
+ fail() { printf '\n!!! FAIL: %s\n' "$1"; exit 1; }
38
+
39
+ # --- 1. Compile (always; cheap if already built) -----------------------------
40
+ stage "compile (rake compile)"
41
+ bundle exec rake compile || fail "rake compile"
42
+
43
+ # --- 2. Syntax check on lib/ (fast, catches typos before specs load) --------
44
+ stage "ruby -wc on lib/hyperion/**/*.rb"
45
+ syntax_errors=0
46
+ while IFS= read -r -d '' f; do
47
+ if ! ruby -wc "$f" >/dev/null 2>/tmp/hyperion-check-syntax.err; then
48
+ printf ' syntax: %s\n' "$f"
49
+ cat /tmp/hyperion-check-syntax.err
50
+ syntax_errors=$((syntax_errors + 1))
51
+ fi
52
+ done < <(find lib -name '*.rb' -type f -print0)
53
+ [ "$syntax_errors" -eq 0 ] || fail "$syntax_errors file(s) failed ruby -wc"
54
+
55
+ if [ "$mode" = "syntax" ]; then
56
+ printf '\nOK (compile + syntax only)\n'
57
+ exit 0
58
+ fi
59
+
60
+ # --- 3. Specs ---------------------------------------------------------------
61
+ case "$mode" in
62
+ quick)
63
+ stage "rspec smoke set"
64
+ bundle exec rspec --fail-fast \
65
+ spec/hyperion/c_parser_spec.rb \
66
+ spec/hyperion/parser_spec.rb \
67
+ spec/hyperion/request_spec.rb \
68
+ spec/hyperion/response_writer_spec.rb \
69
+ spec/hyperion/connection_spec.rb \
70
+ spec/hyperion/runtime_spec.rb \
71
+ spec/hyperion/server_spec.rb \
72
+ spec/hyperion/config_spec.rb \
73
+ || fail "smoke specs"
74
+ ;;
75
+ full)
76
+ stage "rspec (full; :perf excluded by spec_helper)"
77
+ bundle exec rspec --fail-fast || fail "full rspec"
78
+ ;;
79
+ targeted)
80
+ stage "rspec ${specs[*]}"
81
+ bundle exec rspec --fail-fast "${specs[@]}" || fail "targeted specs"
82
+ ;;
83
+ esac
84
+
85
+ printf '\nOK (mode=%s)\n' "$mode"
data/bin/hyperion CHANGED
@@ -1,6 +1,15 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
+ # 2.16.1 — macOS fork-safety. The Obj-C runtime's post-fork sanity check
5
+ # crashes any worker that touches a Foundation class which was mid-init
6
+ # in the master at fork time (e.g. NSCharacterSet, common as a side-effect
7
+ # of OpenSSL / system-resolver loads). The escape hatch is the env var
8
+ # below; it must be set before Foundation classes load. Set on darwin
9
+ # unconditionally — no-op elsewhere. `||=` so an operator who has set
10
+ # it explicitly (e.g. to `NO`) is honoured.
11
+ ENV['OBJC_DISABLE_INITIALIZE_FORK_SAFETY'] ||= 'YES' if RUBY_PLATFORM.include?('darwin')
12
+
4
13
  $LOAD_PATH.unshift(File.expand_path('../lib', __dir__))
5
14
  require 'hyperion/cli'
6
15
  Hyperion::CLI.run(ARGV)
data/lib/hyperion/cli.rb CHANGED
@@ -97,8 +97,6 @@ module Hyperion
97
97
  Hyperion.logger.info { { message: 'FiberLocal shim installed' } } if Hyperion::FiberLocal.installed?
98
98
  end
99
99
 
100
- app = load_rack_app(rackup)
101
- app = wrap_admin_middleware(app, config)
102
100
  workers = config.workers.zero? ? Etc.nprocessors : config.workers
103
101
 
104
102
  # 2.0 default flip (RFC A7): resolve the `h2.max_total_streams`
@@ -107,10 +105,30 @@ module Hyperion
107
105
  # (operator-requested unbounded).
108
106
  config.finalize!(workers: workers)
109
107
 
108
+ # 2.16 — preload toggle. In preload mode (default) the master
109
+ # parses config.ru once and workers inherit the loaded app via
110
+ # copy-on-write. In non-preload mode the master never touches
111
+ # the app; each worker parses post-fork. The non-preload path
112
+ # is the documented escape hatch for macOS getaddrinfo+fork
113
+ # deadlocks; it costs CoW (each worker pays the full boot RSS).
114
+ preload = config.preload != false
115
+ if preload
116
+ app = wrap_admin_middleware(load_rack_app(rackup), config)
117
+ else
118
+ app = nil
119
+ Hyperion.logger.info do
120
+ { message: 'preload disabled; each worker will parse rackup after fork',
121
+ rackup: File.expand_path(rackup) }
122
+ end
123
+ end
124
+
110
125
  if workers <= 1
126
+ # Single-mode always preloads — there's no fork to protect from
127
+ # global state poisoning, so deferring the parse buys nothing.
128
+ app ||= wrap_admin_middleware(load_rack_app(rackup), config)
111
129
  run_single(config, app)
112
130
  else
113
- run_cluster(config, app, workers)
131
+ run_cluster(config, app, workers, rackup_path: preload ? nil : File.expand_path(rackup))
114
132
  end
115
133
  end
116
134
 
@@ -258,6 +276,15 @@ WARNING: argv is visible via `ps`; prefer --admin-token-file PATH for production
258
276
  'Explicit `--preload-static` dirs still take effect.') do
259
277
  cli_opts[:auto_preload_static_disabled] = true
260
278
  end
279
+ # 2.16 — app preload toggle.
280
+ o.on('--[no-]preload',
281
+ 'Preload the Rack app in the master before fork (default ON). ' \
282
+ '--no-preload makes each worker parse config.ru post-fork; ' \
283
+ 'needed on macOS when native gems loaded in the master ' \
284
+ '(anything that touches Network.framework via XPC) ' \
285
+ 'deadlock getaddrinfo in workers post-fork.') do |v|
286
+ cli_opts[:preload] = v
287
+ end
261
288
  o.on('-h', '--help', 'show help') do
262
289
  puts o
263
290
  exit 0
@@ -345,20 +372,22 @@ WARNING: argv is visible via `ps`; prefer --admin-token-file PATH for production
345
372
  Hyperion.logger.flush_all
346
373
  end
347
374
 
348
- def self.run_cluster(config, app, workers)
375
+ def self.run_cluster(config, app, workers, rackup_path: nil)
349
376
  tls = build_tls_from_config(config)
350
377
  Master.new(host: config.host, port: config.port, app: app,
351
378
  workers: workers, tls: tls, thread_count: config.thread_count,
352
- read_timeout: config.read_timeout, config: config).run
379
+ read_timeout: config.read_timeout, config: config,
380
+ rackup_path: rackup_path).run
353
381
  end
354
382
 
355
383
  # Rack 3's parse_file returns a single app value; Rack 2 returned [app, options].
356
- # Normalize so we get just the app either way.
384
+ # Normalize so we get just the app either way. Used by both the preload
385
+ # path (master parses once, before fork) and the non-preload path
386
+ # (each worker parses post-fork) — see Worker#run.
357
387
  def self.load_rack_app(path)
358
388
  result = ::Rack::Builder.parse_file(path)
359
389
  result.is_a?(Array) ? result.first : result
360
390
  end
361
- private_class_method :load_rack_app
362
391
 
363
392
  def self.build_tls_from_config(config)
364
393
  return nil unless config.tls_cert || config.tls_key
@@ -610,7 +639,6 @@ WARNING: argv is visible via `ps`; prefer --admin-token-file PATH for production
610
639
  end
611
640
  AdminMiddleware.new(app, token: config.admin.token)
612
641
  end
613
- private_class_method :wrap_admin_middleware
614
642
 
615
643
  # Read the admin token from a file on disk. Refuses to load if the file
616
644
  # is missing, unreadable, or world-readable — the whole point of using a
@@ -71,7 +71,25 @@ module Hyperion
71
71
  # the `--no-preload-static` CLI flag; lets operators turn off
72
72
  # auto-warming on a Rails app while still keeping the option to
73
73
  # configure explicit dirs via `preload_static`.
74
- auto_preload_static_disabled: false
74
+ auto_preload_static_disabled: false,
75
+ # 2.16: app preload toggle. When true (default) the master loads
76
+ # `config.ru` once before forking — workers inherit the loaded app
77
+ # via copy-on-write, the canonical Hyperion model. When false, the
78
+ # master stays a thin supervisor and each worker parses `config.ru`
79
+ # itself post-fork. Mirrors Puma's `preload_app! false` mode.
80
+ #
81
+ # The non-preload mode is the documented escape hatch for macOS
82
+ # workloads where loading native gems in the master (anything that
83
+ # initializes Network.framework / CoreFoundation via XPC) leaves
84
+ # the post-fork resolver in a deadlocked state — `getaddrinfo`
85
+ # hangs forever in `nw_path_evaluator_evaluate`. Setting `preload
86
+ # false` keeps the master's address space free of those globals so
87
+ # workers fork from a clean slate.
88
+ #
89
+ # Trade-off: each worker pays the boot cost (CPU + RSS) on its own,
90
+ # so steady-state RSS is N× higher and worker boot is slower. Linux
91
+ # users should leave this true.
92
+ preload: true
75
93
  }.freeze
76
94
 
77
95
  HOOKS = %i[before_fork on_worker_boot on_worker_shutdown].freeze
@@ -68,10 +68,15 @@ module Hyperion
68
68
 
69
69
  def initialize(host:, port:, app:, workers: DEFAULT_WORKER_COUNT,
70
70
  read_timeout: Server::DEFAULT_READ_TIMEOUT_SECONDS, tls: nil,
71
- thread_count: Server::DEFAULT_THREAD_COUNT, config: nil)
71
+ thread_count: Server::DEFAULT_THREAD_COUNT, config: nil,
72
+ rackup_path: nil)
72
73
  @host = host
73
74
  @port = port
74
75
  @app = app
76
+ # 2.16 — non-preload mode: master holds a path; each worker parses
77
+ # post-fork. Mutually exclusive with @app (asserted by CLI flow:
78
+ # exactly one of the two is non-nil).
79
+ @rackup_path = rackup_path
75
80
  @workers = workers || Etc.nprocessors
76
81
  @read_timeout = read_timeout
77
82
  @tls = tls
@@ -118,7 +123,13 @@ module Hyperion
118
123
  # BEFORE we fork. Children inherit the warm memory via copy-on-write
119
124
  # so the first batch of requests on each fresh worker doesn't pay
120
125
  # the allocation/autoload tax.
121
- Hyperion.warmup!
126
+ #
127
+ # 2.16: skipped in non-preload mode. The whole point of `preload:
128
+ # false` is to keep the master's address space free of native-gem
129
+ # globals (OpenSSL session caches, Network.framework XPC state,
130
+ # etc.) so workers fork from a clean slate. Warming up here would
131
+ # re-introduce exactly the state we're trying to avoid.
132
+ Hyperion.warmup! if @app
122
133
 
123
134
  # `before_fork` runs ONCE in the master before any worker is forked.
124
135
  # Operators use it to close shared resources (DB pools, Redis sockets)
@@ -226,6 +237,10 @@ module Hyperion
226
237
  Signal.trap('TERM', 'DEFAULT')
227
238
  worker_args = {
228
239
  host: @host, port: @port, app: @app,
240
+ # 2.16 — propagate the deferred rackup path. Worker#run loads
241
+ # via Hyperion::CLI.load_rack_app + wraps admin middleware
242
+ # post-fork when @app is nil.
243
+ rackup_path: @rackup_path,
229
244
  read_timeout: @read_timeout, tls: @tls,
230
245
  thread_count: @thread_count, config: @config,
231
246
  worker_index: worker_index,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hyperion
4
- VERSION = '2.15.0'
4
+ VERSION = '2.16.1'
5
5
  end
@@ -30,7 +30,8 @@ module Hyperion
30
30
  io_uring: :off,
31
31
  max_in_flight_per_conn: nil,
32
32
  tls_handshake_rate_limit: :unlimited,
33
- preload_static_dirs: nil)
33
+ preload_static_dirs: nil,
34
+ rackup_path: nil)
34
35
  @host = host
35
36
  @port = port
36
37
  @app = app
@@ -57,6 +58,7 @@ module Hyperion
57
58
  @max_in_flight_per_conn = max_in_flight_per_conn
58
59
  @tls_handshake_rate_limit = tls_handshake_rate_limit
59
60
  @preload_static_dirs = preload_static_dirs
61
+ @rackup_path = rackup_path
60
62
  end
61
63
 
62
64
  def run
@@ -70,6 +72,19 @@ module Hyperion
70
72
  }
71
73
  end
72
74
 
75
+ # 2.16 — non-preload mode: master never loaded the Rack app, so we
76
+ # parse it here, post-fork. Native gems load against THIS process's
77
+ # address space, not the master's, which avoids the macOS
78
+ # Network.framework + fork getaddrinfo deadlock (where post-fork
79
+ # XPC peers are wedged forever in `nw_path_evaluator_evaluate`).
80
+ if @app.nil? && @rackup_path
81
+ require_relative 'cli'
82
+ @app = ::Hyperion::CLI.wrap_admin_middleware(
83
+ ::Hyperion::CLI.load_rack_app(@rackup_path),
84
+ @config
85
+ )
86
+ end
87
+
73
88
  server = Server.new(host: @host, port: @port, app: @app,
74
89
  read_timeout: @read_timeout, tls: @tls,
75
90
  thread_count: @thread_count,
data/lib/hyperion.rb CHANGED
@@ -1,5 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # 2.16.1 — macOS fork-safety. Mirrors the guard at the top of
4
+ # `bin/hyperion` so programmatic `require 'hyperion'` (specs,
5
+ # embedded uses) gets the same workaround. The Obj-C runtime's
6
+ # post-fork check crashes workers that touch a Foundation class
7
+ # which was mid-init in the master at fork time (NSCharacterSet
8
+ # is a frequent victim — it loads transitively via OpenSSL / system
9
+ # resolver paths). `||=` so an operator who has set the var
10
+ # explicitly is honoured. No-op on non-darwin platforms.
11
+ ENV['OBJC_DISABLE_INITIALIZE_FORK_SAFETY'] ||= 'YES' if RUBY_PLATFORM.include?('darwin')
12
+
3
13
  require_relative 'hyperion/version'
4
14
  require_relative 'hyperion/logger'
5
15
  require_relative 'hyperion/metrics'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyperion-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.15.0
4
+ version: 2.16.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrey Lobanov
@@ -154,6 +154,7 @@ files:
154
154
  - CHANGELOG.md
155
155
  - LICENSE
156
156
  - README.md
157
+ - bin/check
157
158
  - bin/hyperion
158
159
  - ext/hyperion_h2_codec/Cargo.lock
159
160
  - ext/hyperion_h2_codec/Cargo.toml