depot-linux 0.1.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 +7 -0
- data/Gemfile +10 -0
- data/README.md +56 -0
- data/Rakefile +10 -0
- data/bin/depot +8 -0
- data/bin/depot-gui +14 -0
- data/bin/setup-rubyqt6 +72 -0
- data/fixtures/assets/download.png +0 -0
- data/fixtures/flatpakrefs/org.qbittorrent.qBittorrent.flatpakref +10 -0
- data/fixtures/rpms/Modrinth App-0.13.14-1.x86_64.rpm +0 -0
- data/lib/depot/app_customizer.rb +152 -0
- data/lib/depot/assets.rb +11 -0
- data/lib/depot/backends/app_image.rb +210 -0
- data/lib/depot/backends/archive.rb +263 -0
- data/lib/depot/backends/deb.rb +265 -0
- data/lib/depot/backends/flatpak_ref.rb +123 -0
- data/lib/depot/backends/rpm.rb +280 -0
- data/lib/depot/backends/support.rb +39 -0
- data/lib/depot/cli.rb +344 -0
- data/lib/depot/desktop_entry.rb +37 -0
- data/lib/depot/doctor.rb +59 -0
- data/lib/depot/gui/app.rb +23 -0
- data/lib/depot/gui/drop_panel.rb +80 -0
- data/lib/depot/gui/main_window.rb +1196 -0
- data/lib/depot/inspection.rb +52 -0
- data/lib/depot/inspector.rb +387 -0
- data/lib/depot/installer.rb +54 -0
- data/lib/depot/manifest_store.rb +53 -0
- data/lib/depot/packages/archive.rb +188 -0
- data/lib/depot/packages/deb.rb +262 -0
- data/lib/depot/packages/flatpak_ref.rb +90 -0
- data/lib/depot/packages/rpm.rb +301 -0
- data/lib/depot/paths.rb +57 -0
- data/lib/depot/result.rb +13 -0
- data/lib/depot/sandbox.rb +285 -0
- data/lib/depot/settings.rb +43 -0
- data/lib/depot/source_resolver.rb +36 -0
- data/lib/depot/uninstaller.rb +90 -0
- data/lib/depot/update_downloader.rb +136 -0
- data/lib/depot/updater.rb +230 -0
- data/lib/depot/util.rb +33 -0
- data/lib/depot/version.rb +5 -0
- data/lib/depot.rb +21 -0
- metadata +139 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "../desktop_entry"
|
|
6
|
+
require_relative "../paths"
|
|
7
|
+
require_relative "../result"
|
|
8
|
+
require_relative "../packages/rpm"
|
|
9
|
+
require_relative "../util"
|
|
10
|
+
require_relative "support"
|
|
11
|
+
|
|
12
|
+
module Depot
|
|
13
|
+
module Backends
|
|
14
|
+
class Rpm
|
|
15
|
+
include Support
|
|
16
|
+
|
|
17
|
+
def initialize(store:)
|
|
18
|
+
@store = store
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def install(inspection, settings: {})
|
|
22
|
+
return Result.err("RPM backend cannot install #{inspection.format}") unless inspection.rpm?
|
|
23
|
+
|
|
24
|
+
Paths.ensure_base_dirs
|
|
25
|
+
package = RpmPackage.new(inspection.input)
|
|
26
|
+
return Result.err("Invalid RPM package.") unless package.valid?
|
|
27
|
+
|
|
28
|
+
app_id = Util.unique_id(Util.slug("#{package.display_name}-#{package.version_label}"), @store.ids)
|
|
29
|
+
app_dir = File.join(Paths.apps_dir, app_id)
|
|
30
|
+
root_dir = File.join(app_dir, "root")
|
|
31
|
+
FileUtils.mkdir_p(app_dir)
|
|
32
|
+
|
|
33
|
+
package.extract_to(root_dir)
|
|
34
|
+
|
|
35
|
+
desktop_source = package.primary_desktop_entry
|
|
36
|
+
desktop_contents = desktop_source && read_extracted_entry(root_dir, desktop_source)
|
|
37
|
+
display_name = desktop_name(desktop_contents) || package.display_name
|
|
38
|
+
preferred_icon = desktop_contents&.[]( /^Icon=(.+)$/, 1)&.strip
|
|
39
|
+
icon_name = install_icons(package, root_dir, app_id, preferred_icon)
|
|
40
|
+
executable = executable_for(package, root_dir, desktop_contents)
|
|
41
|
+
|
|
42
|
+
desktop_path = nil
|
|
43
|
+
if settings.fetch("desktop_integration", true) && executable
|
|
44
|
+
desktop_path = File.join(Paths.desktop_entries_dir, "depot-#{app_id}.desktop")
|
|
45
|
+
contents = desktop_contents ? rewrite_desktop(desktop_contents, root_dir, app_id, display_name, icon_name) : generated_desktop(app_id, display_name, executable, icon_name)
|
|
46
|
+
File.write(desktop_path, contents)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
warnings = inspection.warnings + portable_warnings(package, executable, desktop_source)
|
|
50
|
+
manifest = {
|
|
51
|
+
"schema_version" => 1,
|
|
52
|
+
"app_id" => app_id,
|
|
53
|
+
"display_name" => display_name,
|
|
54
|
+
"default_display_name" => display_name,
|
|
55
|
+
"backend" => "rpm-portable",
|
|
56
|
+
"install_source" => File.expand_path(inspection.input),
|
|
57
|
+
"source_sha256" => inspection.sha256,
|
|
58
|
+
"source_size" => inspection.size,
|
|
59
|
+
"installed_executable" => executable,
|
|
60
|
+
"desktop_entry" => desktop_path,
|
|
61
|
+
"icons" => installed_icon_paths(app_id, preferred_icon),
|
|
62
|
+
"default_icon_name" => icon_name,
|
|
63
|
+
"created_files" => [desktop_path].compact + installed_icon_paths(app_id, preferred_icon),
|
|
64
|
+
"created_dirs" => [app_dir],
|
|
65
|
+
"installed_at" => Time.now.utc.iso8601,
|
|
66
|
+
"package" => package.package_fields.merge("Requires" => package.requires),
|
|
67
|
+
"desktop_source" => desktop_source,
|
|
68
|
+
"portable_root" => root_dir,
|
|
69
|
+
"permissions" => {
|
|
70
|
+
"executable" => executable ? File.executable?(executable) : false,
|
|
71
|
+
"requires_sudo" => false,
|
|
72
|
+
"writes_outside_depot" => false,
|
|
73
|
+
"notes" => ["RPM payload extracted user-locally. RPM scriptlets were not executed."]
|
|
74
|
+
},
|
|
75
|
+
"sandbox" => {
|
|
76
|
+
"enabled" => false,
|
|
77
|
+
"preference" => settings.fetch("sandbox_preference", "ask")
|
|
78
|
+
},
|
|
79
|
+
"update" => {
|
|
80
|
+
"mechanism" => "manual",
|
|
81
|
+
"source" => File.expand_path(inspection.input)
|
|
82
|
+
},
|
|
83
|
+
"warnings" => warnings
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
manifest_path = @store.write(manifest)
|
|
87
|
+
refresh_desktop_caches if settings.fetch("desktop_integration", true)
|
|
88
|
+
Result.ok(manifest.merge("manifest_path" => manifest_path), warnings:)
|
|
89
|
+
rescue RpmPackage::FormatError, SystemCallError => e
|
|
90
|
+
Result.err("RPM install failed: #{e.message}")
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def read_extracted_entry(root_dir, entry)
|
|
96
|
+
path = File.join(root_dir, entry)
|
|
97
|
+
return nil unless File.file?(path)
|
|
98
|
+
|
|
99
|
+
File.read(path)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def desktop_name(contents)
|
|
103
|
+
return nil unless contents
|
|
104
|
+
|
|
105
|
+
line = contents.lines.find { |candidate| candidate.start_with?("Name=") }
|
|
106
|
+
line&.split("=", 2)&.last&.strip
|
|
107
|
+
rescue SystemCallError
|
|
108
|
+
nil
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def executable_for(package, root_dir, desktop_contents)
|
|
112
|
+
exec_line = desktop_contents&.lines&.find { |line| line.start_with?("Exec=") }
|
|
113
|
+
command = exec_line&.split("=", 2)&.last.to_s.strip
|
|
114
|
+
rewritten = rewrite_exec_value(command, root_dir)
|
|
115
|
+
first = first_exec_token(rewritten)
|
|
116
|
+
return first if first && File.exist?(first)
|
|
117
|
+
|
|
118
|
+
package.executable_candidates.map { |entry| File.join(root_dir, entry) }.find { |path| File.file?(path) && File.executable?(path) }
|
|
119
|
+
rescue SystemCallError
|
|
120
|
+
nil
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def generated_desktop(app_id, display_name, executable, icon_name)
|
|
124
|
+
DesktopEntry.new(app_id:, name: display_name, exec_path: executable, icon_name:).contents
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def rewrite_desktop(contents, root_dir, app_id, display_name, icon_name)
|
|
128
|
+
seen_depot_id = false
|
|
129
|
+
current_group = nil
|
|
130
|
+
lines = []
|
|
131
|
+
contents.lines.each do |line|
|
|
132
|
+
if line.start_with?("[")
|
|
133
|
+
lines << "X-Depot-AppID=#{app_id}\n" if current_group == "[Desktop Entry]" && !seen_depot_id
|
|
134
|
+
current_group = line.strip
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
lines << if line.start_with?("Name=") && current_group == "[Desktop Entry]"
|
|
138
|
+
"Name=#{display_name}\n"
|
|
139
|
+
elsif line.match?(/\AExec=/)
|
|
140
|
+
"Exec=#{rewrite_exec_value(line.split("=", 2).last.strip, root_dir)}\n"
|
|
141
|
+
elsif line.match?(/\ATryExec=/)
|
|
142
|
+
"TryExec=#{rewrite_exec_value(line.split("=", 2).last.strip, root_dir)}\n"
|
|
143
|
+
elsif line.start_with?("Icon=")
|
|
144
|
+
icon_name ? "Icon=#{icon_name}\n" : line
|
|
145
|
+
elsif line.start_with?("X-Depot-AppID=")
|
|
146
|
+
seen_depot_id = true
|
|
147
|
+
"X-Depot-AppID=#{app_id}\n"
|
|
148
|
+
else
|
|
149
|
+
line
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
lines << "X-Depot-AppID=#{app_id}\n" if current_group == "[Desktop Entry]" && !seen_depot_id
|
|
153
|
+
lines.join
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def rewrite_exec_value(value, root_dir)
|
|
157
|
+
token, rest = split_exec(value)
|
|
158
|
+
return value if token.to_s.empty?
|
|
159
|
+
|
|
160
|
+
mapped = if token.start_with?("/")
|
|
161
|
+
File.join(root_dir, token.delete_prefix("/"))
|
|
162
|
+
elsif token.include?("/")
|
|
163
|
+
File.join(root_dir, token)
|
|
164
|
+
else
|
|
165
|
+
candidate = find_executable(root_dir, token)
|
|
166
|
+
candidate || token
|
|
167
|
+
end
|
|
168
|
+
"#{Util.desktop_exec_quote(mapped)}#{rest}"
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def split_exec(value)
|
|
172
|
+
if value.start_with?('"')
|
|
173
|
+
closing = value.index('"', 1)
|
|
174
|
+
return [value[1...closing], value[(closing + 1)..].to_s] if closing
|
|
175
|
+
end
|
|
176
|
+
token, rest = value.split(/\s+/, 2)
|
|
177
|
+
[token, rest ? " #{rest}" : ""]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def first_exec_token(value)
|
|
181
|
+
token, = split_exec(value)
|
|
182
|
+
token
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def find_executable(root_dir, token)
|
|
186
|
+
Dir.glob(File.join(root_dir, "**", token)).find { |path| File.file?(path) && File.executable?(path) }
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def install_icons(package, root_dir, app_id, preferred)
|
|
190
|
+
icons = package.icon_entries
|
|
191
|
+
icons = fallback_icon_entries(package, preferred) if icons.empty?
|
|
192
|
+
icons.sort_by { |entry| icon_score(entry, preferred) }.first(8).each do |entry|
|
|
193
|
+
ext = File.extname(entry).downcase
|
|
194
|
+
next unless [".png", ".svg", ".xpm"].include?(ext)
|
|
195
|
+
|
|
196
|
+
source = File.join(root_dir, entry)
|
|
197
|
+
next unless File.file?(source)
|
|
198
|
+
|
|
199
|
+
target_dir = File.join(Paths.icon_root, icon_theme_size(entry, source), "apps")
|
|
200
|
+
FileUtils.mkdir_p(target_dir)
|
|
201
|
+
icon_install_names(app_id, preferred).each do |name|
|
|
202
|
+
FileUtils.cp(source, File.join(target_dir, "#{name}#{ext}"))
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
icon_paths(app_id).any? ? app_id : nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def fallback_icon_entries(package, preferred)
|
|
209
|
+
preferred_base = File.basename(preferred.to_s, ".*")
|
|
210
|
+
candidates = package.image_entries
|
|
211
|
+
scored = candidates.select do |entry|
|
|
212
|
+
base = File.basename(entry, ".*")
|
|
213
|
+
entry.match?(/(^|\/)(icon|logo|app|code)[^\/]*\.(png|svg|xpm)\z/i) ||
|
|
214
|
+
(!preferred_base.empty? && (base == preferred_base || entry.downcase.include?(preferred_base.downcase)))
|
|
215
|
+
end
|
|
216
|
+
scored.empty? ? candidates : scored
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def icon_score(entry, preferred)
|
|
220
|
+
base = File.basename(entry, ".*")
|
|
221
|
+
[
|
|
222
|
+
preferred.to_s.empty? || base != preferred ? 1 : 0,
|
|
223
|
+
entry.include?("/256x256/") ? 0 : 1,
|
|
224
|
+
entry.include?("/512x512/") ? 0 : 1,
|
|
225
|
+
entry.match?(/(^|\/)(icon|logo|app|code)[^\/]*\.(png|svg|xpm)\z/i) ? 0 : 1,
|
|
226
|
+
-entry.length
|
|
227
|
+
]
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def icon_theme_size(entry, source)
|
|
231
|
+
match = entry.match(RpmPackage::HICOLOR_ICON_PATH)
|
|
232
|
+
return match[1] if match
|
|
233
|
+
return "scalable" if File.extname(entry).downcase == ".svg"
|
|
234
|
+
return png_size(source) if File.extname(entry).downcase == ".png"
|
|
235
|
+
|
|
236
|
+
"256x256"
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def png_size(path)
|
|
240
|
+
File.open(path, "rb") do |file|
|
|
241
|
+
header = file.read(24)
|
|
242
|
+
return "256x256" unless header&.start_with?("\x89PNG\r\n\x1A\n".b)
|
|
243
|
+
|
|
244
|
+
width, height = header.byteslice(16, 8).unpack("NN")
|
|
245
|
+
return "256x256" if width.to_i <= 0 || height.to_i <= 0
|
|
246
|
+
|
|
247
|
+
"#{width}x#{height}"
|
|
248
|
+
end
|
|
249
|
+
rescue SystemCallError
|
|
250
|
+
"256x256"
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def icon_paths(app_id)
|
|
254
|
+
Dir.glob(File.join(Paths.icon_root, "*", "apps", "#{app_id}.{png,svg,xpm}"), File::FNM_EXTGLOB)
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def installed_icon_paths(app_id, preferred)
|
|
258
|
+
icon_install_names(app_id, preferred).flat_map do |name|
|
|
259
|
+
Dir.glob(File.join(Paths.icon_root, "*", "apps", "#{name}.{png,svg,xpm}"), File::FNM_EXTGLOB)
|
|
260
|
+
end.uniq
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def icon_install_names(app_id, preferred)
|
|
264
|
+
names = [app_id]
|
|
265
|
+
preferred = preferred.to_s
|
|
266
|
+
names << preferred if preferred.match?(/\A[a-zA-Z0-9_.-]+\z/)
|
|
267
|
+
names.uniq
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def portable_warnings(package, executable, desktop_source)
|
|
271
|
+
warnings = ["Depot installed this RPM in portable extraction mode and did not use rpm, dnf, zypper, sudo, or root scriptlets."]
|
|
272
|
+
warnings << "RPM scriptlets were found and were not executed: #{package.scriptlets.join(", ")}." unless package.scriptlets.empty?
|
|
273
|
+
warnings << "No desktop launcher was found; Depot generated one." unless desktop_source
|
|
274
|
+
warnings << "No executable could be confidently selected." unless executable
|
|
275
|
+
warnings
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require_relative "../paths"
|
|
6
|
+
|
|
7
|
+
module Depot
|
|
8
|
+
module Backends
|
|
9
|
+
module Support
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def command_available?(command)
|
|
13
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? { |dir| File.executable?(File.join(dir, command)) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def refresh_desktop_caches
|
|
17
|
+
if command_available?("gtk-update-icon-cache") && Dir.exist?(Paths.icon_root)
|
|
18
|
+
system("gtk-update-icon-cache", "-f", "-t", Paths.icon_root, out: File::NULL, err: File::NULL)
|
|
19
|
+
end
|
|
20
|
+
if command_available?("update-desktop-database") && Dir.exist?(Paths.desktop_entries_dir)
|
|
21
|
+
system("update-desktop-database", Paths.desktop_entries_dir, out: File::NULL, err: File::NULL)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def write_shell_launcher(path, command, *args)
|
|
26
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
27
|
+
escaped = ([command] + args).map { |part| Shellwords.escape(part.to_s) }.join(" ")
|
|
28
|
+
File.write(path, "#!/bin/sh\nexec #{escaped} \"$@\"\n")
|
|
29
|
+
File.chmod(0o755, path)
|
|
30
|
+
path
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def desktop_name_from(contents)
|
|
34
|
+
line = contents.to_s.lines.find { |candidate| candidate.start_with?("Name=") }
|
|
35
|
+
line&.split("=", 2)&.last&.strip
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/depot/cli.rb
ADDED
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require_relative "../depot"
|
|
6
|
+
|
|
7
|
+
module Depot
|
|
8
|
+
class CLI
|
|
9
|
+
def initialize(stdout: $stdout, stderr: $stderr)
|
|
10
|
+
@stdout = stdout
|
|
11
|
+
@stderr = stderr
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def run(argv)
|
|
15
|
+
command = argv.shift
|
|
16
|
+
case command
|
|
17
|
+
when "inspect"
|
|
18
|
+
inspect_command(argv)
|
|
19
|
+
when "install"
|
|
20
|
+
install_command(argv)
|
|
21
|
+
when "list"
|
|
22
|
+
list_command(argv)
|
|
23
|
+
when "info"
|
|
24
|
+
info_command(argv)
|
|
25
|
+
when "uninstall", "remove"
|
|
26
|
+
uninstall_command(argv)
|
|
27
|
+
when "update"
|
|
28
|
+
update_command(argv)
|
|
29
|
+
when "update-source"
|
|
30
|
+
update_source_command(argv)
|
|
31
|
+
when "sandbox"
|
|
32
|
+
sandbox_command(argv)
|
|
33
|
+
when "doctor"
|
|
34
|
+
doctor_command(argv)
|
|
35
|
+
when "settings"
|
|
36
|
+
settings_command(argv)
|
|
37
|
+
when "-h", "--help", nil
|
|
38
|
+
help
|
|
39
|
+
0
|
|
40
|
+
when "-v", "--version"
|
|
41
|
+
@stdout.puts "Depot #{Depot::VERSION}"
|
|
42
|
+
0
|
|
43
|
+
else
|
|
44
|
+
@stderr.puts "Unknown command: #{command}"
|
|
45
|
+
help(@stderr)
|
|
46
|
+
1
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def inspect_command(argv)
|
|
53
|
+
json = argv.delete("--json")
|
|
54
|
+
input = argv.shift
|
|
55
|
+
return usage_error("Usage: depot inspect [--json] PATH_OR_URL") unless input
|
|
56
|
+
|
|
57
|
+
result = Inspector.inspect(input)
|
|
58
|
+
return print_error(result) unless result.ok?
|
|
59
|
+
|
|
60
|
+
if json
|
|
61
|
+
@stdout.puts JSON.pretty_generate(result.value.to_h)
|
|
62
|
+
else
|
|
63
|
+
inspection = result.value
|
|
64
|
+
@stdout.puts "Input: #{inspection.input}"
|
|
65
|
+
@stdout.puts "Format: #{inspection.format} (#{inspection.confidence})"
|
|
66
|
+
@stdout.puts "Name: #{inspection.display_name}"
|
|
67
|
+
@stdout.puts "Size: #{inspection.size || "unknown"}"
|
|
68
|
+
@stdout.puts "SHA-256: #{inspection.sha256 || "unknown"}"
|
|
69
|
+
@stdout.puts "Executable: #{inspection.executable ? "yes" : "no"}"
|
|
70
|
+
print_deb_metadata(inspection) if inspection.deb?
|
|
71
|
+
print_archive_metadata(inspection) if inspection.archive?
|
|
72
|
+
print_rpm_metadata(inspection) if inspection.rpm?
|
|
73
|
+
print_flatpakref_metadata(inspection) if inspection.flatpakref?
|
|
74
|
+
print_list("Warnings", inspection.warnings)
|
|
75
|
+
print_list("Risks", inspection.risks)
|
|
76
|
+
end
|
|
77
|
+
0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def install_command(argv)
|
|
81
|
+
json = argv.delete("--json")
|
|
82
|
+
no_desktop = argv.delete("--no-desktop")
|
|
83
|
+
input = argv.shift
|
|
84
|
+
return usage_error("Usage: depot install [--json] [--no-desktop] PATH") unless input
|
|
85
|
+
|
|
86
|
+
settings = {}
|
|
87
|
+
settings["desktop_integration"] = false if no_desktop
|
|
88
|
+
result = Installer.install(input, settings:)
|
|
89
|
+
return print_error(result) unless result.ok?
|
|
90
|
+
|
|
91
|
+
manifest = result.value
|
|
92
|
+
if json
|
|
93
|
+
@stdout.puts JSON.pretty_generate(manifest)
|
|
94
|
+
else
|
|
95
|
+
@stdout.puts "Installed #{manifest.fetch("display_name")} as #{manifest.fetch("app_id")}"
|
|
96
|
+
@stdout.puts "Executable: #{manifest.fetch("installed_executable")}"
|
|
97
|
+
@stdout.puts "Desktop entry: #{manifest["desktop_entry"] || "disabled"}"
|
|
98
|
+
@stdout.puts "Manifest: #{manifest.fetch("manifest_path")}"
|
|
99
|
+
print_list("Warnings", result.warnings)
|
|
100
|
+
end
|
|
101
|
+
0
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def list_command(_argv)
|
|
105
|
+
apps = ManifestStore.new.all
|
|
106
|
+
if apps.empty?
|
|
107
|
+
@stdout.puts "No Depot apps installed."
|
|
108
|
+
else
|
|
109
|
+
apps.each do |manifest|
|
|
110
|
+
@stdout.puts "#{manifest.fetch("app_id")}\t#{manifest.fetch("display_name")}\t#{manifest.fetch("backend")}"
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
0
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def info_command(argv)
|
|
117
|
+
json = argv.delete("--json")
|
|
118
|
+
app_id = argv.shift
|
|
119
|
+
return usage_error("Usage: depot info [--json] APP_ID") unless app_id
|
|
120
|
+
|
|
121
|
+
manifest = ManifestStore.new.find(app_id)
|
|
122
|
+
return usage_error("No installed app found for #{app_id}") unless manifest
|
|
123
|
+
|
|
124
|
+
if json
|
|
125
|
+
@stdout.puts JSON.pretty_generate(manifest)
|
|
126
|
+
else
|
|
127
|
+
@stdout.puts "App: #{manifest.fetch("display_name")} (#{manifest.fetch("app_id")})"
|
|
128
|
+
@stdout.puts "Backend: #{manifest.fetch("backend")}"
|
|
129
|
+
@stdout.puts "Source: #{manifest.fetch("install_source")}"
|
|
130
|
+
@stdout.puts "Update source: #{manifest.dig("update", "source") || "none"}"
|
|
131
|
+
@stdout.puts "Sandbox: #{Sandbox.summary(manifest)}"
|
|
132
|
+
@stdout.puts "Executable: #{manifest.fetch("installed_executable")}"
|
|
133
|
+
@stdout.puts "Desktop entry: #{manifest["desktop_entry"] || "none"}"
|
|
134
|
+
@stdout.puts "Installed: #{manifest.fetch("installed_at")}"
|
|
135
|
+
print_list("Created files", manifest.fetch("created_files", []))
|
|
136
|
+
print_list("Warnings", manifest.fetch("warnings", []))
|
|
137
|
+
end
|
|
138
|
+
0
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def uninstall_command(argv)
|
|
142
|
+
app_id = argv.shift
|
|
143
|
+
return usage_error("Usage: depot uninstall APP_ID") unless app_id
|
|
144
|
+
|
|
145
|
+
result = Uninstaller.uninstall(app_id)
|
|
146
|
+
return print_error(result) unless result.ok?
|
|
147
|
+
|
|
148
|
+
@stdout.puts "Uninstalled #{app_id}"
|
|
149
|
+
print_list("Deleted files", result.value.fetch("deleted_files", []))
|
|
150
|
+
0
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def update_command(argv)
|
|
154
|
+
json = argv.delete("--json")
|
|
155
|
+
all = argv.delete("--all")
|
|
156
|
+
app_id = argv.shift
|
|
157
|
+
return usage_error("Usage: depot update [--json] APP_ID | --all") unless all || app_id
|
|
158
|
+
|
|
159
|
+
result = all ? Updater.update_all : Updater.update(app_id)
|
|
160
|
+
return print_error(result) unless result.ok?
|
|
161
|
+
|
|
162
|
+
if json
|
|
163
|
+
@stdout.puts JSON.pretty_generate(result.value)
|
|
164
|
+
elsif all
|
|
165
|
+
@stdout.puts "Updated all available apps."
|
|
166
|
+
else
|
|
167
|
+
manifest = result.value
|
|
168
|
+
@stdout.puts "Updated #{manifest.fetch("display_name")} (#{manifest.fetch("app_id")})"
|
|
169
|
+
end
|
|
170
|
+
0
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def update_source_command(argv)
|
|
174
|
+
app_id = argv.shift
|
|
175
|
+
url = argv.shift
|
|
176
|
+
return usage_error("Usage: depot update-source APP_ID HTTPS_URL") unless app_id && url
|
|
177
|
+
|
|
178
|
+
result = Updater.set_source(app_id, url)
|
|
179
|
+
return print_error(result) unless result.ok?
|
|
180
|
+
|
|
181
|
+
@stdout.puts "Set update source for #{app_id}."
|
|
182
|
+
0
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def sandbox_command(argv)
|
|
186
|
+
app_id = argv.shift
|
|
187
|
+
mode = argv.shift
|
|
188
|
+
return usage_error("Usage: depot sandbox APP_ID [inherit|enabled|disabled]") unless app_id
|
|
189
|
+
|
|
190
|
+
manifest = ManifestStore.new.find(app_id)
|
|
191
|
+
return usage_error("No installed app found for #{app_id}") unless manifest
|
|
192
|
+
|
|
193
|
+
if mode.nil?
|
|
194
|
+
@stdout.puts Sandbox.summary(manifest)
|
|
195
|
+
return 0
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
result = Sandbox.set(app_id, { "mode" => mode })
|
|
199
|
+
return print_error(result) unless result.ok?
|
|
200
|
+
|
|
201
|
+
@stdout.puts "Set sandbox for #{app_id} to #{result.value.fetch("sandbox").fetch("mode")}."
|
|
202
|
+
0
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def settings_command(argv)
|
|
206
|
+
settings = Settings.new
|
|
207
|
+
values = settings.load
|
|
208
|
+
if argv.empty?
|
|
209
|
+
@stdout.puts JSON.pretty_generate(values)
|
|
210
|
+
return 0
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
if argv.first == "set"
|
|
214
|
+
key = argv[1]
|
|
215
|
+
value = argv[2]
|
|
216
|
+
return usage_error("Usage: depot settings set KEY VALUE") unless key && value
|
|
217
|
+
|
|
218
|
+
parsed = case value
|
|
219
|
+
when "true" then true
|
|
220
|
+
when "false" then false
|
|
221
|
+
else value
|
|
222
|
+
end
|
|
223
|
+
saved = settings.save(values.merge(key => parsed))
|
|
224
|
+
@stdout.puts JSON.pretty_generate(saved)
|
|
225
|
+
return 0
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
usage_error("Usage: depot settings [set KEY VALUE]")
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def doctor_command(argv)
|
|
232
|
+
json = argv.delete("--json")
|
|
233
|
+
report = Doctor.new.report
|
|
234
|
+
if json
|
|
235
|
+
@stdout.puts JSON.pretty_generate(report)
|
|
236
|
+
return 0
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
@stdout.puts "Depot doctor"
|
|
240
|
+
@stdout.puts "Tools:"
|
|
241
|
+
report.fetch("tools").each do |tool, present|
|
|
242
|
+
@stdout.puts " - #{tool}: #{present ? "found" : "missing"}"
|
|
243
|
+
end
|
|
244
|
+
@stdout.puts "Paths:"
|
|
245
|
+
report.fetch("paths").each do |path, present|
|
|
246
|
+
@stdout.puts " - #{path}: #{present ? "ok" : "missing"}"
|
|
247
|
+
end
|
|
248
|
+
manifests = report.fetch("manifests")
|
|
249
|
+
if manifests.empty?
|
|
250
|
+
@stdout.puts "Manifests: none installed"
|
|
251
|
+
else
|
|
252
|
+
@stdout.puts "Manifests:"
|
|
253
|
+
manifests.each do |manifest|
|
|
254
|
+
status = manifest.fetch("ok") ? "ok" : manifest.fetch("issues").join(", ")
|
|
255
|
+
@stdout.puts " - #{manifest.fetch("app_id")}: #{status}"
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
0
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def print_error(result)
|
|
262
|
+
@stderr.puts result.error
|
|
263
|
+
print_list("Warnings", result.warnings, io: @stderr)
|
|
264
|
+
1
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def usage_error(message)
|
|
268
|
+
@stderr.puts message
|
|
269
|
+
1
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def print_list(title, items, io: @stdout)
|
|
273
|
+
return if items.nil? || items.empty?
|
|
274
|
+
|
|
275
|
+
io.puts "#{title}:"
|
|
276
|
+
items.each { |item| io.puts " - #{item}" }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def print_deb_metadata(inspection)
|
|
280
|
+
metadata = inspection.metadata
|
|
281
|
+
@stdout.puts "Package: #{metadata["package"] || "unknown"}"
|
|
282
|
+
@stdout.puts "Version: #{metadata["version"] || "unknown"}"
|
|
283
|
+
@stdout.puts "Architecture: #{metadata["architecture"] || "unknown"}"
|
|
284
|
+
@stdout.puts "Debian control archive: #{metadata["control_archive"] || "unknown"}"
|
|
285
|
+
@stdout.puts "Debian data archive: #{metadata["data_archive"] || "unknown"}"
|
|
286
|
+
print_list("Maintainer scripts", metadata.fetch("maintainer_scripts", []))
|
|
287
|
+
print_list("Desktop entries", metadata.fetch("desktop_entries", []))
|
|
288
|
+
print_list("Executable candidates", metadata.fetch("executable_candidates", []))
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def print_archive_metadata(inspection)
|
|
292
|
+
metadata = inspection.metadata
|
|
293
|
+
@stdout.puts "Archive format: #{metadata["archive_format"] || inspection.format}"
|
|
294
|
+
@stdout.puts "Archive root: #{metadata["archive_root"] || "mixed"}"
|
|
295
|
+
print_list("Desktop entries", metadata.fetch("desktop_entries", []))
|
|
296
|
+
print_list("Executable candidates", metadata.fetch("executable_candidates", []))
|
|
297
|
+
print_list("Installer-like scripts", metadata.fetch("script_entries", []))
|
|
298
|
+
print_list("Source/build markers", metadata.fetch("source_markers", []))
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def print_rpm_metadata(inspection)
|
|
302
|
+
metadata = inspection.metadata
|
|
303
|
+
version = [metadata["version"], metadata["release"]].compact.join("-")
|
|
304
|
+
@stdout.puts "Package: #{metadata["package"] || "unknown"}"
|
|
305
|
+
@stdout.puts "Version: #{version.empty? ? "unknown" : version}"
|
|
306
|
+
@stdout.puts "Architecture: #{metadata["architecture"] || "unknown"}"
|
|
307
|
+
@stdout.puts "RPM payload: #{metadata["payload_format"] || "unknown"} / #{metadata["payload_compressor"] || "unknown"}"
|
|
308
|
+
print_list("RPM requirements", metadata.fetch("requires", []))
|
|
309
|
+
print_list("RPM scriptlets", metadata.fetch("scriptlets", []))
|
|
310
|
+
print_list("Desktop entries", metadata.fetch("desktop_entries", []))
|
|
311
|
+
print_list("Executable candidates", metadata.fetch("executable_candidates", []))
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def print_flatpakref_metadata(inspection)
|
|
315
|
+
metadata = inspection.metadata
|
|
316
|
+
@stdout.puts "Flatpak ID: #{metadata["name"] || "unknown"}"
|
|
317
|
+
@stdout.puts "Title: #{metadata["title"] || inspection.display_name}"
|
|
318
|
+
@stdout.puts "Branch: #{metadata["branch"] || "master"}"
|
|
319
|
+
@stdout.puts "Remote URL: #{metadata["url"] || "unknown"}"
|
|
320
|
+
@stdout.puts "Suggested remote: #{metadata["suggest_remote_name"] || "none"}"
|
|
321
|
+
@stdout.puts "Runtime ref: #{metadata["is_runtime"] ? "yes" : "no"}"
|
|
322
|
+
@stdout.puts "Embedded GPG key: #{metadata["gpg_key_present"] ? "yes" : "no"}"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def help(io = @stdout)
|
|
326
|
+
io.puts <<~HELP
|
|
327
|
+
Depot #{Depot::VERSION}
|
|
328
|
+
|
|
329
|
+
Usage:
|
|
330
|
+
depot inspect [--json] PATH_OR_URL
|
|
331
|
+
depot install [--json] [--no-desktop] PATH
|
|
332
|
+
depot list
|
|
333
|
+
depot info [--json] APP_ID
|
|
334
|
+
depot uninstall APP_ID
|
|
335
|
+
depot update [--json] APP_ID
|
|
336
|
+
depot update [--json] --all
|
|
337
|
+
depot update-source APP_ID HTTPS_URL
|
|
338
|
+
depot sandbox APP_ID [inherit|enabled|disabled]
|
|
339
|
+
depot doctor [--json]
|
|
340
|
+
depot settings [set KEY VALUE]
|
|
341
|
+
HELP
|
|
342
|
+
end
|
|
343
|
+
end
|
|
344
|
+
end
|