ruby_everywhere 0.7.0 → 0.8.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.
- checksums.yaml +4 -4
- data/exe/every +2 -13
- data/exe/rbe +2 -13
- data/lib/everywhere/boot.rb +9 -2
- data/lib/everywhere/builders/android.rb +16 -42
- data/lib/everywhere/builders/base.rb +53 -0
- data/lib/everywhere/builders/desktop.rb +7 -23
- data/lib/everywhere/builders/ios.rb +49 -52
- data/lib/everywhere/builders/native_sources.rb +38 -0
- data/lib/everywhere/child_processes.rb +4 -4
- data/lib/everywhere/child_supervision.rb +172 -0
- data/lib/everywhere/clock.rb +12 -0
- data/lib/everywhere/commands/build.rb +20 -9
- data/lib/everywhere/commands/dev.rb +59 -284
- data/lib/everywhere/commands/doctor.rb +6 -6
- data/lib/everywhere/commands/install.rb +1 -0
- data/lib/everywhere/commands/platform/auth_status.rb +1 -0
- data/lib/everywhere/commands/platform/build.rb +5 -6
- data/lib/everywhere/commands/platform/login.rb +4 -4
- data/lib/everywhere/commands/platform/logout.rb +1 -0
- data/lib/everywhere/commands/platform/runner.rb +62 -7
- data/lib/everywhere/commands/preview.rb +24 -97
- data/lib/everywhere/commands/publish.rb +2 -0
- data/lib/everywhere/commands/release.rb +14 -11
- data/lib/everywhere/commands/shell_dir.rb +2 -0
- data/lib/everywhere/commands/updates_keygen.rb +25 -1
- data/lib/everywhere/config/app.rb +126 -0
- data/lib/everywhere/config/auth.rb +108 -0
- data/lib/everywhere/config/data.rb +50 -0
- data/lib/everywhere/config/deep_linking.rb +107 -0
- data/lib/everywhere/config/desktop_ui.rb +153 -0
- data/lib/everywhere/config/mobile.rb +211 -0
- data/lib/everywhere/config/native_desktop.rb +168 -0
- data/lib/everywhere/config/native_mobile.rb +337 -0
- data/lib/everywhere/config/shell.rb +57 -0
- data/lib/everywhere/config/updates.rb +63 -0
- data/lib/everywhere/config.rb +30 -1423
- data/lib/everywhere/desktop_dev_app.rb +138 -0
- data/lib/everywhere/dock/state.rb +3 -1
- data/lib/everywhere/entrypoint.rb +24 -0
- data/lib/everywhere/error.rb +8 -0
- data/lib/everywhere/framework.rb +2 -2
- data/lib/everywhere/host.rb +11 -0
- data/lib/everywhere/ignore.rb +11 -5
- data/lib/everywhere/line_pump.rb +3 -1
- data/lib/everywhere/minisign.rb +1 -0
- data/lib/everywhere/native_platform.rb +44 -0
- data/lib/everywhere/paths.rb +23 -7
- data/lib/everywhere/platform/client.rb +19 -1
- data/lib/everywhere/platform/snapshot.rb +6 -4
- data/lib/everywhere/plist.rb +17 -0
- data/lib/everywhere/png.rb +1 -0
- data/lib/everywhere/raw_tty.rb +51 -0
- data/lib/everywhere/s3.rb +1 -0
- data/lib/everywhere/shell_pages.rb +109 -0
- data/lib/everywhere/tab_filter.rb +36 -0
- data/lib/everywhere/task_pool.rb +3 -4
- data/lib/everywhere/ui.rb +6 -1
- data/lib/everywhere/version.rb +6 -4
- data/lib/everywhere.rb +1 -2
- data/support/mobile/ios/App.xcodeproj/project.pbxproj +0 -2
- data/support/release/macos/notarize.sh +3 -0
- metadata +24 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require_relative "desktop_assets"
|
|
6
|
+
require_relative "host"
|
|
7
|
+
require_relative "icon"
|
|
8
|
+
require_relative "paths"
|
|
9
|
+
require_relative "plist"
|
|
10
|
+
require_relative "ui"
|
|
11
|
+
|
|
12
|
+
module Everywhere
|
|
13
|
+
# What `every dev` stages under ~/.rubyeverywhere so a live `cargo run` shell
|
|
14
|
+
# looks like the app being built: the flat assets folder, the Dock icon, and
|
|
15
|
+
# on macOS a throwaway .app around the dev binary — plus the command that
|
|
16
|
+
# links the two together and launches it.
|
|
17
|
+
#
|
|
18
|
+
# None of this is packaging. `every build` writes the real, complete bundle;
|
|
19
|
+
# this is the subset a dev session needs, restaged on every launch so
|
|
20
|
+
# editing an asset and pressing `d` is enough to see it.
|
|
21
|
+
class DesktopDevApp
|
|
22
|
+
def initialize(config:)
|
|
23
|
+
@config = config
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# native/desktop/assets/** → the staged flat folder the running shell
|
|
27
|
+
# reads. Runs the same compiler `every build` does, so an asset that
|
|
28
|
+
# resolves in dev resolves in the packaged app too. Returns
|
|
29
|
+
# [dir, errors]; the dir is returned even when the app ships no assets,
|
|
30
|
+
# because the shell only has to cope with it not existing.
|
|
31
|
+
def stage_desktop_assets
|
|
32
|
+
dest = Everywhere::Paths.desktop_assets_dir(@config.bundle_id)
|
|
33
|
+
source = File.join(@config.root, "native", "desktop", "assets")
|
|
34
|
+
_names, errors = Everywhere::DesktopAssets.write!(source, dest)
|
|
35
|
+
[dest, errors]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# A minimal .app around the dev binary, so macOS has a CFBundleName to
|
|
39
|
+
# read. Without it every menu that names the app says "example-shell" —
|
|
40
|
+
# the crate name — because an unbundled process has nothing else to go on.
|
|
41
|
+
# (Setting NSProcessInfo's processName at runtime does NOT fix this: the
|
|
42
|
+
# menu bar and the predefined About/Hide/Quit items are built from the
|
|
43
|
+
# bundle, and macOS has already decided by the time any Rust runs.)
|
|
44
|
+
#
|
|
45
|
+
# Only the plist matters here. No icon is stamped: the shell sets the Dock
|
|
46
|
+
# icon at runtime from NATIVE_ICON_PATH, because Tauri applies its own
|
|
47
|
+
# compiled-in icon after launch and would win over a bundle icns anyway.
|
|
48
|
+
# `every build` writes the real, complete plist — this is the dev subset.
|
|
49
|
+
def stage_dev_bundle
|
|
50
|
+
app = File.join(Everywhere::Paths.desktop_dev_app_dir(@config.bundle_id),
|
|
51
|
+
"#{dev_bundle_name}.app")
|
|
52
|
+
macos = File.join(app, "Contents", "MacOS")
|
|
53
|
+
FileUtils.mkdir_p(macos)
|
|
54
|
+
File.write(File.join(app, "Contents", "Info.plist"), dev_info_plist)
|
|
55
|
+
[app, File.join(macos, dev_exe_name)]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Display names are free-form (spaces, punctuation, emoji all fine), but
|
|
59
|
+
# this one becomes a directory name — so the separators, and a leading dot
|
|
60
|
+
# that would hide or escape the bundle, are the only things taken out.
|
|
61
|
+
def dev_bundle_name
|
|
62
|
+
cleaned = @config.name.to_s.gsub(%r{[/\\]}, "-").sub(/\A\.+/, "").strip
|
|
63
|
+
cleaned.empty? ? "App" : cleaned
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Spaces are legal in CFBundleExecutable but a nuisance in every shell
|
|
67
|
+
# command that names it, so the executable follows the packaged app's
|
|
68
|
+
# convention and drops them.
|
|
69
|
+
def dev_exe_name = dev_bundle_name.gsub(/\s+/, "")
|
|
70
|
+
|
|
71
|
+
def dev_info_plist
|
|
72
|
+
name = Everywhere::Plist.escape(@config.name)
|
|
73
|
+
<<~PLIST
|
|
74
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
75
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
76
|
+
<plist version="1.0">
|
|
77
|
+
<dict>
|
|
78
|
+
<key>CFBundleName</key><string>#{name}</string>
|
|
79
|
+
<key>CFBundleDisplayName</key><string>#{name}</string>
|
|
80
|
+
<key>CFBundleExecutable</key><string>#{Everywhere::Plist.escape(dev_exe_name)}</string>
|
|
81
|
+
<key>CFBundleIdentifier</key><string>#{Everywhere::Plist.escape(@config.bundle_id)}</string>
|
|
82
|
+
<key>CFBundlePackageType</key><string>APPL</string>
|
|
83
|
+
<key>CFBundleShortVersionString</key><string>#{Everywhere::Plist.escape(@config.version)}</string>
|
|
84
|
+
<key>NSHighResolutionCapable</key><true/>
|
|
85
|
+
</dict>
|
|
86
|
+
</plist>
|
|
87
|
+
PLIST
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# The Dock icon for the running shell. `cargo run` produces a bare binary
|
|
91
|
+
# with no .app around it, so macOS has nothing to read an icon from and
|
|
92
|
+
# Tauri's compiled-in placeholder wins — which is how you end up staring
|
|
93
|
+
# at the gem's icon while testing your own app.
|
|
94
|
+
#
|
|
95
|
+
# Shaped with the same Big Sur inset + squircle the packaged .icns gets
|
|
96
|
+
# (Icon.macos_master), so the Dock looks identical in dev and in a release
|
|
97
|
+
# build rather than subtly rounder in one of them. Returns "" when the app
|
|
98
|
+
# has no icon — the shell reads that as "leave it alone".
|
|
99
|
+
def stage_desktop_icon
|
|
100
|
+
source = @config.icon
|
|
101
|
+
return "" unless source && File.exist?(source)
|
|
102
|
+
|
|
103
|
+
dest = File.join(Everywhere::Paths.desktop_icon_dir(@config.bundle_id), "icon.png")
|
|
104
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
105
|
+
FileUtils.cp(source, dest) unless Everywhere::Icon.macos_master(source, dest)
|
|
106
|
+
dest
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
# A Dock icon is never worth failing a dev session over.
|
|
109
|
+
UI.warn("couldn't prepare the Dock icon (#{e.class}) — using the default")
|
|
110
|
+
""
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# macOS runs the shell from inside a throwaway .app so it has a name;
|
|
114
|
+
# everywhere else `cargo run` is still the whole story.
|
|
115
|
+
#
|
|
116
|
+
# Three steps in one command rather than three Ruby calls, because the
|
|
117
|
+
# link has to happen between the compile and the launch and we want ONE
|
|
118
|
+
# supervised child: cargo's progress relays into the dock, and exec then
|
|
119
|
+
# replaces it with the app, so the pid we're tracking is the window.
|
|
120
|
+
#
|
|
121
|
+
# A hard link, not a copy: same volume (both under ~/.rubyeverywhere), so
|
|
122
|
+
# it's free and instant, where copying a debug Tauri binary is ~100MB on
|
|
123
|
+
# every launch. cargo replaces the file on rebuild rather than writing
|
|
124
|
+
# through it, so the link is remade each time — hence `ln -f`.
|
|
125
|
+
def desktop_command(prepared)
|
|
126
|
+
return "cargo run" unless Host.darwin?
|
|
127
|
+
|
|
128
|
+
_app, exe = stage_dev_bundle
|
|
129
|
+
built = File.join(prepared.target_dir, "debug", "example-shell")
|
|
130
|
+
[
|
|
131
|
+
"cargo build",
|
|
132
|
+
"{ ln -f #{built.shellescape} #{exe.shellescape} 2>/dev/null || " \
|
|
133
|
+
"cp -f #{built.shellescape} #{exe.shellescape}; }",
|
|
134
|
+
"exec #{exe.shellescape}"
|
|
135
|
+
].join(" && ")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../clock"
|
|
4
|
+
|
|
3
5
|
module Everywhere
|
|
4
6
|
class Dock
|
|
5
7
|
# What the dock knows about each thing `every dev` is running. Pure data
|
|
@@ -22,7 +24,7 @@ module Everywhere
|
|
|
22
24
|
def busy? = BUSY.include?(state)
|
|
23
25
|
end
|
|
24
26
|
|
|
25
|
-
def initialize(keys, clock: -> {
|
|
27
|
+
def initialize(keys, clock: -> { Clock.monotonic })
|
|
26
28
|
@clock = clock
|
|
27
29
|
@lock = Mutex.new
|
|
28
30
|
ordered = ORDER & keys
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "cli"
|
|
4
|
+
require_relative "ui"
|
|
5
|
+
|
|
6
|
+
module Everywhere
|
|
7
|
+
# The body of both executables — `every` and `rbe` are the same CLI under two
|
|
8
|
+
# names, so exe/* stay one-liners that call in here.
|
|
9
|
+
module Entrypoint
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def run(argv = ARGV)
|
|
13
|
+
Dry::CLI.new(Everywhere::CLI).call(arguments: argv)
|
|
14
|
+
rescue Interrupt
|
|
15
|
+
# ^C is an answer, not a crash: no backtrace, and the shell's own convention
|
|
16
|
+
# for "killed by SIGINT".
|
|
17
|
+
exit 130
|
|
18
|
+
rescue Everywhere::Error => e
|
|
19
|
+
# Everywhere::Fatal is a SystemExit, not an Error, so a die! that already
|
|
20
|
+
# printed its own line passes straight through here.
|
|
21
|
+
Everywhere::UI.die!(e.message)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
# Required on its own by every file that raises it, so each stays loadable
|
|
5
|
+
# standalone (the packaged runtime requires them piecemeal, without going
|
|
6
|
+
# through everywhere.rb).
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
end
|
data/lib/everywhere/framework.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "error"
|
|
4
|
+
|
|
3
5
|
module Everywhere
|
|
4
6
|
# Framework adapters keep both the CLI (dev/build) and the packaged runtime
|
|
5
7
|
# (boot) framework-agnostic. Each adapter owns two sides of one framework:
|
|
@@ -223,6 +225,4 @@ module Everywhere
|
|
|
223
225
|
def env_exports = { "HANAMI_ENV" => "production", "RACK_ENV" => "production" }
|
|
224
226
|
end
|
|
225
227
|
end
|
|
226
|
-
|
|
227
|
-
class Error < StandardError; end
|
|
228
228
|
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
# The machine the CLI is running on — never the machine it's building for
|
|
5
|
+
# (that's the --target, see Config.os_of and the receipt).
|
|
6
|
+
module Host
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def darwin? = RUBY_PLATFORM.include?("darwin")
|
|
10
|
+
end
|
|
11
|
+
end
|
data/lib/everywhere/ignore.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Everywhere
|
|
4
|
-
# Shared source-exclusion rules,
|
|
5
|
-
# style) when
|
|
4
|
+
# Shared source-exclusion rules: the built-in DEFAULTS, plus `.everywhereignore`
|
|
5
|
+
# (dockerignore style) appended when the app has one. ONE source of truth so the
|
|
6
6
|
# build receipt's source checksum (Receipt) and the Platform snapshot
|
|
7
7
|
# (`every platform build`, later) describe exactly the same set of files — a
|
|
8
8
|
# checksum that didn't match what actually ships would be worse than none.
|
|
@@ -15,7 +15,8 @@ module Everywhere
|
|
|
15
15
|
#
|
|
16
16
|
# Rules are evaluated top to bottom and the LAST one that matches a path wins
|
|
17
17
|
# (dockerignore semantics), so ordering matters: put `!` re-includes after the
|
|
18
|
-
# broad exclude they carve out of
|
|
18
|
+
# broad exclude they carve out of — and the app's file, coming last, can
|
|
19
|
+
# re-include anything DEFAULTS excluded.
|
|
19
20
|
class Ignore
|
|
20
21
|
FILE = ".everywhereignore"
|
|
21
22
|
FLAGS = File::FNM_PATHNAME | File::FNM_DOTMATCH
|
|
@@ -29,12 +30,17 @@ module Everywhere
|
|
|
29
30
|
*.key *.pem *.crt *.p12 *.mobileprovision
|
|
30
31
|
].freeze
|
|
31
32
|
|
|
33
|
+
# The app's file is layered ON TOP of DEFAULTS rather than replacing them:
|
|
34
|
+
# a project that adds one line to skip a fixture dir must not thereby start
|
|
35
|
+
# shipping .env and *.p12. Last match wins, so a user who really wants a
|
|
36
|
+
# defaulted-out file back can still `!` it in.
|
|
32
37
|
def self.for(root)
|
|
33
38
|
path = File.join(root, FILE)
|
|
39
|
+
return new(parse(DEFAULTS.join("\n"))) unless File.exist?(path)
|
|
40
|
+
|
|
34
41
|
# UTF-8 explicitly (and scrubbed): under LANG=C the bytes come back
|
|
35
42
|
# US-ASCII and a non-ASCII path pattern makes the matching raise.
|
|
36
|
-
|
|
37
|
-
new(parse(source))
|
|
43
|
+
new(parse(DEFAULTS.join("\n")) + parse(File.read(path, encoding: "UTF-8").scrub))
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
# → [{ negated:, pattern: }, ...] in file order. Blank lines and whole-line
|
data/lib/everywhere/line_pump.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "clock"
|
|
4
|
+
|
|
3
5
|
module Everywhere
|
|
4
6
|
# Turns the raw byte stream off a pty master into whole lines.
|
|
5
7
|
#
|
|
@@ -25,7 +27,7 @@ module Everywhere
|
|
|
25
27
|
# "Overwrite? [Yn]" — would be invisible for as long as it mattered.
|
|
26
28
|
IDLE_FLUSH = 0.2
|
|
27
29
|
|
|
28
|
-
def initialize(idle_flush: IDLE_FLUSH, clock: -> {
|
|
30
|
+
def initialize(idle_flush: IDLE_FLUSH, clock: -> { Clock.monotonic })
|
|
29
31
|
# Bytes in, text out. A pty hands over BINARY, and a chunk boundary can
|
|
30
32
|
# land in the middle of a multi-byte character, so buffering and splitting
|
|
31
33
|
# happen on bytes — UTF-8 is self-synchronizing, so a newline is never
|
data/lib/everywhere/minisign.rb
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
class << self
|
|
5
|
+
# The desktop shell's marker. WKWebView APPENDS
|
|
6
|
+
# `applicationNameForUserAgent` to the real system UA, so this arrives
|
|
7
|
+
# alongside a normal Safari UA rather than replacing it — hence a pattern
|
|
8
|
+
# rather than a prefix check. The version is absent when everywhere.yml
|
|
9
|
+
# declares none.
|
|
10
|
+
#
|
|
11
|
+
# macOS only for now: the marker is set in macos_webview_configuration, and
|
|
12
|
+
# Windows/Linux have no append-only equivalent (their user_agent() REPLACES
|
|
13
|
+
# the string, and fabricating a whole UA is worse than not marking it).
|
|
14
|
+
# Desktop only ships macOS today; revisit when it doesn't.
|
|
15
|
+
DESKTOP_UA = %r{\bRubyEverywhere(?:/[\w.\-]+)? \((?:macos|windows|linux)\)}
|
|
16
|
+
|
|
17
|
+
MOBILE_PLATFORMS = %i[ios android].freeze
|
|
18
|
+
|
|
19
|
+
# :ios, :android, :desktop or nil, from a User-Agent string. Present from
|
|
20
|
+
# the very first request, before any JS has run. Shared by the view helpers
|
|
21
|
+
# and the auth middleware.
|
|
22
|
+
#
|
|
23
|
+
# Mobile is checked FIRST and the order matters: the mobile shells prepend
|
|
24
|
+
# the same "RubyEverywhere/<version> (<os>)" marker to Hotwire Native's own,
|
|
25
|
+
# so a mobile UA matches DESKTOP_UA too.
|
|
26
|
+
def native_platform_of(user_agent)
|
|
27
|
+
ua = user_agent.to_s
|
|
28
|
+
return :ios if ua.include?("Hotwire Native iOS")
|
|
29
|
+
return :android if ua.include?("Hotwire Native Android")
|
|
30
|
+
return :desktop if ua.match?(DESKTOP_UA)
|
|
31
|
+
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Just the phone shells. Anything that leans on a mobile-only affordance —
|
|
36
|
+
# the OAuth handoff to ASWebAuthenticationSession / Custom Tabs, biometric
|
|
37
|
+
# gating — has to ask for this rather than "is this a native shell", because
|
|
38
|
+
# the desktop shell answers yes to that and has neither.
|
|
39
|
+
def mobile_platform_of(user_agent)
|
|
40
|
+
platform = native_platform_of(user_agent)
|
|
41
|
+
platform if MOBILE_PLATFORMS.include?(platform)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/everywhere/paths.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "error"
|
|
3
4
|
require_relative "ui"
|
|
4
5
|
|
|
5
6
|
module Everywhere
|
|
@@ -97,7 +98,7 @@ module Everywhere
|
|
|
97
98
|
# tmpdir) on purpose: a stable project path keeps Xcode's incremental state
|
|
98
99
|
# valid across builds, and users can open the stamped project in Xcode.
|
|
99
100
|
def ios_work_dir(bundle_id)
|
|
100
|
-
File.join(cache_dir, "ios", bundle_id)
|
|
101
|
+
File.join(cache_dir, "ios", segment!(bundle_id))
|
|
101
102
|
end
|
|
102
103
|
|
|
103
104
|
# Where xcodebuild writes DerivedData for iOS shell builds. Shared across
|
|
@@ -118,7 +119,7 @@ module Everywhere
|
|
|
118
119
|
# incremental state is path-sensitive, and the stamped project opens in
|
|
119
120
|
# Android Studio unmodified.
|
|
120
121
|
def android_work_dir(application_id)
|
|
121
|
-
File.join(cache_dir, "android", application_id)
|
|
122
|
+
File.join(cache_dir, "android", segment!(application_id))
|
|
122
123
|
end
|
|
123
124
|
|
|
124
125
|
# GRADLE_USER_HOME for shell builds. Redirected here so the multi-GB Gradle
|
|
@@ -144,7 +145,7 @@ module Everywhere
|
|
|
144
145
|
# stays warm across projects. Persistent for the same reason the mobile
|
|
145
146
|
# work dirs are: a cold Tauri build is minutes where a warm one is seconds.
|
|
146
147
|
def desktop_work_dir(bundle_id)
|
|
147
|
-
File.join(cache_dir, "desktop", bundle_id)
|
|
148
|
+
File.join(cache_dir, "desktop", segment!(bundle_id))
|
|
148
149
|
end
|
|
149
150
|
|
|
150
151
|
# CARGO_TARGET_DIR for a stamped shell. Per-app rather than shared, because
|
|
@@ -152,7 +153,7 @@ module Everywhere
|
|
|
152
153
|
# every other app's — pointing them at one target dir would make cargo
|
|
153
154
|
# rebuild the world on every alternating build.
|
|
154
155
|
def desktop_target_dir(bundle_id)
|
|
155
|
-
File.join(cache_dir, "desktop-target", bundle_id)
|
|
156
|
+
File.join(cache_dir, "desktop-target", segment!(bundle_id))
|
|
156
157
|
end
|
|
157
158
|
|
|
158
159
|
# Where `every dev` keeps the throwaway .app wrapper it runs the shell
|
|
@@ -161,14 +162,14 @@ module Everywhere
|
|
|
161
162
|
# menu bar, the About/Hide/Quit items and the Dock. The wrapper exists only
|
|
162
163
|
# to carry an Info.plist; the binary inside it is a hard link to cargo's.
|
|
163
164
|
def desktop_dev_app_dir(bundle_id)
|
|
164
|
-
File.join(cache_dir, "desktop-app", bundle_id)
|
|
165
|
+
File.join(cache_dir, "desktop-app", segment!(bundle_id))
|
|
165
166
|
end
|
|
166
167
|
|
|
167
168
|
# Where `every dev` stages the app's Dock icon for the running shell. The
|
|
168
169
|
# shaped master is derived from app.icon, so it's a build product, not the
|
|
169
170
|
# app's own file — it belongs in the cache next to the rest of them.
|
|
170
171
|
def desktop_icon_dir(bundle_id)
|
|
171
|
-
File.join(cache_dir, "desktop-icon", bundle_id)
|
|
172
|
+
File.join(cache_dir, "desktop-icon", segment!(bundle_id))
|
|
172
173
|
end
|
|
173
174
|
|
|
174
175
|
# Where `every dev` stages native/desktop/assets for the running shell.
|
|
@@ -178,7 +179,7 @@ module Everywhere
|
|
|
178
179
|
# in dev. Staging runs the same DesktopAssets compiler both ways, so the two
|
|
179
180
|
# can't drift. Keyed by bundle id like the mobile work dirs.
|
|
180
181
|
def desktop_assets_dir(bundle_id)
|
|
181
|
-
File.join(cache_dir, "desktop-assets", bundle_id)
|
|
182
|
+
File.join(cache_dir, "desktop-assets", segment!(bundle_id))
|
|
182
183
|
end
|
|
183
184
|
|
|
184
185
|
# Where cargo writes the shell's build output (CARGO_TARGET_DIR) for apps on
|
|
@@ -199,5 +200,20 @@ module Everywhere
|
|
|
199
200
|
def app_bundle_dir
|
|
200
201
|
File.join(cache_dir, "bundle")
|
|
201
202
|
end
|
|
203
|
+
|
|
204
|
+
# Bundle ids and application ids come from the app's everywhere.yml and are
|
|
205
|
+
# used as ONE path segment under the cache — directories the builders rm_rf
|
|
206
|
+
# wholesale. Config#identity_errors rejects a malformed id before a build
|
|
207
|
+
# starts; this is the backstop for every other caller. Raise, not die!:
|
|
208
|
+
# Paths is reachable from the packaged app's runtime.
|
|
209
|
+
def segment!(value)
|
|
210
|
+
segment = value.to_s
|
|
211
|
+
if segment.empty? || segment.include?("/") || segment.include?("\\") || segment.include?("..")
|
|
212
|
+
raise Everywhere::Error, "#{segment.inspect} can't be a directory name (app.bundle_id in " \
|
|
213
|
+
"config/everywhere.yml must be reverse-DNS, like com.example.app)"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
segment
|
|
217
|
+
end
|
|
202
218
|
end
|
|
203
219
|
end
|
|
@@ -8,6 +8,7 @@ begin
|
|
|
8
8
|
rescue LoadError
|
|
9
9
|
nil
|
|
10
10
|
end
|
|
11
|
+
require_relative "../error"
|
|
11
12
|
require_relative "../version"
|
|
12
13
|
|
|
13
14
|
module Everywhere
|
|
@@ -92,7 +93,7 @@ module Everywhere
|
|
|
92
93
|
when Net::HTTPRedirection
|
|
93
94
|
raise Error, "too many redirects downloading #{url}" if redirects <= 0
|
|
94
95
|
|
|
95
|
-
return download(res["location"], dest, redirects: redirects - 1)
|
|
96
|
+
return download(redirect_target(uri, res["location"]), dest, redirects: redirects - 1)
|
|
96
97
|
when Net::HTTPSuccess
|
|
97
98
|
File.open(dest, "wb") { |f| res.read_body { |chunk| f.write(chunk) } }
|
|
98
99
|
else
|
|
@@ -107,6 +108,23 @@ module Everywhere
|
|
|
107
108
|
|
|
108
109
|
private
|
|
109
110
|
|
|
111
|
+
# A signed-blob redirect is expected to leave the platform host, but it
|
|
112
|
+
# may never leave TLS: an http:// hop would put the artifact — and the
|
|
113
|
+
# snapshot or credentials inside it — on the wire in the clear. Only a
|
|
114
|
+
# base that was already http (`every platform --url http://localhost`)
|
|
115
|
+
# has nothing to downgrade from. Location is resolved against the current
|
|
116
|
+
# hop, which is also what makes a relative one work.
|
|
117
|
+
def redirect_target(from, location)
|
|
118
|
+
target = URI.join(from, location.to_s)
|
|
119
|
+
if from.scheme == "https" && target.scheme != "https"
|
|
120
|
+
raise Error, "refusing an https → #{target.scheme} redirect to #{target.host}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
target.to_s
|
|
124
|
+
rescue URI::Error => e
|
|
125
|
+
raise Error, "bad redirect from #{from.host} (#{e.message})"
|
|
126
|
+
end
|
|
127
|
+
|
|
110
128
|
def http(uri, &block)
|
|
111
129
|
Net::HTTP.start(uri.hostname, uri.port,
|
|
112
130
|
use_ssl: uri.scheme == "https", open_timeout: OPEN_TIMEOUT, &block)
|
|
@@ -39,10 +39,12 @@ module Everywhere
|
|
|
39
39
|
|
|
40
40
|
def add(tar, root, rel)
|
|
41
41
|
abs = File.join(root, rel)
|
|
42
|
-
# lstat
|
|
43
|
-
# deleted bundle path, a checked-in link to an absent
|
|
44
|
-
#
|
|
45
|
-
|
|
42
|
+
# lstat, and never follow: File.stat would read through the link, so a
|
|
43
|
+
# dangling one (a deleted bundle path, a checked-in link to an absent
|
|
44
|
+
# sibling repo) raises Errno::ENOENT and aborts the whole snapshot —
|
|
45
|
+
# and a LIVE one pointing outside the tree (link → ~/.ssh/id_rsa) would
|
|
46
|
+
# pack and upload its target's contents under an innocent name.
|
|
47
|
+
return Everywhere::UI.warn("skipping symlink #{rel}") if File.lstat(abs).symlink?
|
|
46
48
|
|
|
47
49
|
stat = File.stat(abs)
|
|
48
50
|
tar.add_file_simple(rel, stat.mode & 0o777, stat.size) do |io|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Everywhere
|
|
4
|
+
# Escaping for the plists and entitlements the CLI writes by hand (there are
|
|
5
|
+
# four: `every build`'s Info.plist, `every dev`'s dev bundle, and the iOS
|
|
6
|
+
# builder's entitlements + export options). Every value interpolated into them
|
|
7
|
+
# comes from everywhere.yml or from Apple, so an unescaped `&` or `<` is at
|
|
8
|
+
# best a plist macOS refuses to parse and at worst extra keys of someone
|
|
9
|
+
# else's choosing.
|
|
10
|
+
module Plist
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
def escape(value)
|
|
14
|
+
value.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/everywhere/png.rb
CHANGED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "shellout"
|
|
4
|
+
|
|
5
|
+
module Everywhere
|
|
6
|
+
# A terminal that reports keys as they're pressed, put back the way it was
|
|
7
|
+
# found on the way out.
|
|
8
|
+
#
|
|
9
|
+
# io/console's getch holds full raw mode while blocked, which clears OPOST
|
|
10
|
+
# and stair-steps every log line we relay; stty -icanon -echo disables only
|
|
11
|
+
# line buffering and echo, leaving output processing (and Ctrl-C as SIGINT)
|
|
12
|
+
# intact.
|
|
13
|
+
module RawTty
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def with
|
|
17
|
+
# stty is POSIX-only, and the non-pty spawn path otherwise works on
|
|
18
|
+
# Windows: without it the menu simply stays line-buffered (press d,
|
|
19
|
+
# then Enter) rather than the session dying on Errno::ENOENT.
|
|
20
|
+
saved_tty = saved_tty_state
|
|
21
|
+
system("stty", "-icanon", "-echo") if saved_tty
|
|
22
|
+
trap_exit_signals
|
|
23
|
+
yield
|
|
24
|
+
ensure
|
|
25
|
+
system("stty", saved_tty) if saved_tty
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# The tty settings to put back on the way out, or nil when there's no
|
|
29
|
+
# usable stty to read them with — the one signal both the raw-mode setup
|
|
30
|
+
# and its restore key off.
|
|
31
|
+
def saved_tty_state
|
|
32
|
+
return nil unless Shellout.tool?("stty")
|
|
33
|
+
|
|
34
|
+
state = `stty -g`.chomp
|
|
35
|
+
state.empty? ? nil : state
|
|
36
|
+
rescue StandardError
|
|
37
|
+
nil
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# The dock hides the cursor and the key loop puts the tty in -icanon
|
|
41
|
+
# -echo; a SIGTERM that skipped our ensure blocks would leave the user's
|
|
42
|
+
# shell in that state. exit(1) from a trap unwinds normally, which is all
|
|
43
|
+
# this needs to do — a handler must stay this small, because anything that
|
|
44
|
+
# takes a lock (Console, the dock) raises ThreadError in trap context.
|
|
45
|
+
def trap_exit_signals
|
|
46
|
+
%w[TERM HUP].each { |sig| Signal.trap(sig) { exit(1) } }
|
|
47
|
+
rescue ArgumentError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|