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,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Depot
|
|
4
|
+
Inspection = Struct.new(
|
|
5
|
+
:input,
|
|
6
|
+
:format,
|
|
7
|
+
:confidence,
|
|
8
|
+
:display_name,
|
|
9
|
+
:sha256,
|
|
10
|
+
:size,
|
|
11
|
+
:executable,
|
|
12
|
+
:metadata,
|
|
13
|
+
:warnings,
|
|
14
|
+
:risks,
|
|
15
|
+
keyword_init: true
|
|
16
|
+
) do
|
|
17
|
+
def appimage?
|
|
18
|
+
format == "appimage"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def deb?
|
|
22
|
+
format == "deb"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def archive?
|
|
26
|
+
%w[tar.gz tar.xz tar.zst].include?(format)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def rpm?
|
|
30
|
+
format == "rpm"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def flatpakref?
|
|
34
|
+
format == "flatpakref"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_h
|
|
38
|
+
{
|
|
39
|
+
"input" => input,
|
|
40
|
+
"format" => format,
|
|
41
|
+
"confidence" => confidence,
|
|
42
|
+
"display_name" => display_name,
|
|
43
|
+
"sha256" => sha256,
|
|
44
|
+
"size" => size,
|
|
45
|
+
"executable" => executable,
|
|
46
|
+
"metadata" => metadata,
|
|
47
|
+
"warnings" => warnings,
|
|
48
|
+
"risks" => risks
|
|
49
|
+
}
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
require_relative "packages/archive"
|
|
5
|
+
require_relative "packages/deb"
|
|
6
|
+
require_relative "packages/flatpak_ref"
|
|
7
|
+
require_relative "inspection"
|
|
8
|
+
require_relative "packages/rpm"
|
|
9
|
+
require_relative "result"
|
|
10
|
+
require_relative "util"
|
|
11
|
+
|
|
12
|
+
module Depot
|
|
13
|
+
class Inspector
|
|
14
|
+
APPIMAGE_EXT = /\.appimage\z/i
|
|
15
|
+
DEB_EXT = /\.deb\z/i
|
|
16
|
+
RPM_EXT = /\.rpm\z/i
|
|
17
|
+
FLATPAKREF_EXT = /\.flatpakref\z/i
|
|
18
|
+
ARCHIVE_EXT = /\.(tar\.gz|tgz|tar\.xz|txz|tar\.zst|tzst)\z/i
|
|
19
|
+
ELF_MAGIC = "\x7FELF".b.freeze
|
|
20
|
+
|
|
21
|
+
def self.inspect(input, checksum: true)
|
|
22
|
+
new.inspect(input, checksum:)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def inspect(input, checksum: true)
|
|
26
|
+
source = input.to_s
|
|
27
|
+
uri = parse_uri(source)
|
|
28
|
+
return inspect_url(uri) if uri&.absolute?
|
|
29
|
+
return Result.err("Path does not exist: #{source}") unless File.exist?(source)
|
|
30
|
+
return Result.err("Input is not a regular file: #{source}") unless File.file?(source)
|
|
31
|
+
|
|
32
|
+
Result.ok(file_inspection(source, checksum:))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def inspect_url(uri)
|
|
38
|
+
warnings = ["URL installs are planned; this build installs local files through available backends."]
|
|
39
|
+
inspection = Inspection.new(
|
|
40
|
+
input: uri.to_s,
|
|
41
|
+
format: format_from_name(uri.path),
|
|
42
|
+
confidence: "low",
|
|
43
|
+
display_name: File.basename(uri.path),
|
|
44
|
+
sha256: nil,
|
|
45
|
+
size: nil,
|
|
46
|
+
executable: false,
|
|
47
|
+
metadata: {},
|
|
48
|
+
warnings:,
|
|
49
|
+
risks: ["Depot cannot verify remote content until it is downloaded."]
|
|
50
|
+
)
|
|
51
|
+
Result.ok(inspection, warnings:)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def file_inspection(path, checksum: true)
|
|
55
|
+
extension_match = path.match?(APPIMAGE_EXT)
|
|
56
|
+
deb_extension_match = path.match?(DEB_EXT)
|
|
57
|
+
rpm_extension_match = path.match?(RPM_EXT)
|
|
58
|
+
flatpakref_extension_match = path.match?(FLATPAKREF_EXT)
|
|
59
|
+
archive_extension_match = path.match?(ARCHIVE_EXT)
|
|
60
|
+
elf = elf?(path)
|
|
61
|
+
deb_package = deb_extension_match ? DebPackage.new(path) : nil
|
|
62
|
+
deb_valid = deb_package&.valid?
|
|
63
|
+
rpm_package = rpm_extension_match ? RpmPackage.new(path) : nil
|
|
64
|
+
rpm_valid = rpm_package&.valid?
|
|
65
|
+
flatpak_ref = flatpakref_extension_match ? FlatpakRef.new(path) : nil
|
|
66
|
+
flatpakref_valid = flatpak_ref&.valid?
|
|
67
|
+
archive_package = archive_extension_match ? ArchivePackage.new(path) : nil
|
|
68
|
+
archive_valid = archive_package&.valid?
|
|
69
|
+
warnings = []
|
|
70
|
+
risks = []
|
|
71
|
+
metadata = {}
|
|
72
|
+
display_name = if archive_valid
|
|
73
|
+
archive_package.display_name
|
|
74
|
+
elsif rpm_valid
|
|
75
|
+
rpm_package.display_name
|
|
76
|
+
elsif flatpakref_valid
|
|
77
|
+
flatpak_ref.display_name
|
|
78
|
+
elsif rpm_extension_match
|
|
79
|
+
File.basename(path, ".rpm")
|
|
80
|
+
elsif flatpakref_extension_match
|
|
81
|
+
File.basename(path, ".flatpakref")
|
|
82
|
+
elsif archive_extension_match
|
|
83
|
+
File.basename(path).sub(/\.(tar\.gz|tgz|tar\.xz|txz|tar\.zst|tzst)\z/i, "")
|
|
84
|
+
else
|
|
85
|
+
File.basename(path, File.extname(path))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
format = if extension_match && elf
|
|
89
|
+
"appimage"
|
|
90
|
+
elsif extension_match
|
|
91
|
+
warnings << "File name looks like an AppImage, but the ELF header was not found."
|
|
92
|
+
"appimage"
|
|
93
|
+
elsif deb_valid
|
|
94
|
+
"deb"
|
|
95
|
+
elsif deb_extension_match
|
|
96
|
+
warnings << "File name looks like a Debian package, but the archive structure was not recognized."
|
|
97
|
+
"deb"
|
|
98
|
+
elsif rpm_valid
|
|
99
|
+
"rpm"
|
|
100
|
+
elsif rpm_extension_match
|
|
101
|
+
warnings << "File name looks like an RPM package, but the RPM header was not recognized."
|
|
102
|
+
"rpm"
|
|
103
|
+
elsif flatpakref_valid
|
|
104
|
+
"flatpakref"
|
|
105
|
+
elsif flatpakref_extension_match
|
|
106
|
+
warnings << "File name looks like a Flatpak reference, but the file structure was not recognized."
|
|
107
|
+
"flatpakref"
|
|
108
|
+
elsif archive_valid
|
|
109
|
+
archive_package.format
|
|
110
|
+
elsif archive_extension_match
|
|
111
|
+
warnings << "File name looks like a tar archive, but the archive could not be read."
|
|
112
|
+
archive_package&.format || "tar.gz"
|
|
113
|
+
elsif elf
|
|
114
|
+
warnings << "File is an ELF executable, but does not use the .AppImage extension."
|
|
115
|
+
"elf"
|
|
116
|
+
else
|
|
117
|
+
"unknown"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
confidence = if extension_match && elf
|
|
121
|
+
"high"
|
|
122
|
+
elsif deb_valid
|
|
123
|
+
"high"
|
|
124
|
+
elsif archive_valid
|
|
125
|
+
"high"
|
|
126
|
+
elsif rpm_valid
|
|
127
|
+
"high"
|
|
128
|
+
elsif flatpakref_valid
|
|
129
|
+
"high"
|
|
130
|
+
elsif extension_match || deb_extension_match || rpm_extension_match || flatpakref_extension_match || archive_extension_match || elf
|
|
131
|
+
"medium"
|
|
132
|
+
else
|
|
133
|
+
"low"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
risks << "Installing may execute the AppImage runtime to extract desktop metadata." if format == "appimage"
|
|
137
|
+
if format == "deb"
|
|
138
|
+
deb_metadata = deb_valid ? deb_metadata(deb_package) : {}
|
|
139
|
+
metadata.merge!(deb_metadata)
|
|
140
|
+
warnings.concat(deb_warnings(deb_metadata))
|
|
141
|
+
risks.concat(deb_risks(deb_metadata))
|
|
142
|
+
end
|
|
143
|
+
if archive_format?(format)
|
|
144
|
+
archive_metadata = archive_valid ? archive_metadata(archive_package) : {}
|
|
145
|
+
metadata.merge!(archive_metadata)
|
|
146
|
+
warnings.concat(archive_warnings(archive_metadata))
|
|
147
|
+
risks.concat(archive_risks(archive_metadata))
|
|
148
|
+
end
|
|
149
|
+
if format == "rpm"
|
|
150
|
+
rpm_metadata = rpm_valid ? rpm_metadata(rpm_package) : {}
|
|
151
|
+
metadata.merge!(rpm_metadata)
|
|
152
|
+
warnings.concat(rpm_warnings(rpm_metadata))
|
|
153
|
+
risks.concat(rpm_risks(rpm_metadata))
|
|
154
|
+
end
|
|
155
|
+
if format == "flatpakref"
|
|
156
|
+
flatpakref_metadata = flatpakref_valid ? flatpakref_metadata(flatpak_ref) : {}
|
|
157
|
+
metadata.merge!(flatpakref_metadata)
|
|
158
|
+
warnings.concat(flatpakref_warnings(flatpakref_metadata))
|
|
159
|
+
risks.concat(flatpakref_risks(flatpakref_metadata))
|
|
160
|
+
end
|
|
161
|
+
risks << "This format does not have an installer backend yet." unless %w[appimage deb rpm flatpakref tar.gz tar.xz tar.zst].include?(format)
|
|
162
|
+
|
|
163
|
+
Inspection.new(
|
|
164
|
+
input: path,
|
|
165
|
+
format:,
|
|
166
|
+
confidence:,
|
|
167
|
+
display_name:,
|
|
168
|
+
sha256: checksum ? Util.sha256(path) : nil,
|
|
169
|
+
size: File.size(path),
|
|
170
|
+
executable: File.executable?(path),
|
|
171
|
+
metadata: {
|
|
172
|
+
"extension_appimage" => extension_match,
|
|
173
|
+
"extension_deb" => deb_extension_match,
|
|
174
|
+
"extension_rpm" => rpm_extension_match,
|
|
175
|
+
"extension_flatpakref" => flatpakref_extension_match,
|
|
176
|
+
"extension_archive" => archive_extension_match,
|
|
177
|
+
"elf" => elf
|
|
178
|
+
}.merge(metadata),
|
|
179
|
+
warnings:,
|
|
180
|
+
risks:
|
|
181
|
+
)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def elf?(path)
|
|
185
|
+
File.open(path, "rb") { |file| file.read(4) == ELF_MAGIC }
|
|
186
|
+
rescue SystemCallError
|
|
187
|
+
false
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def format_from_name(name)
|
|
191
|
+
return "appimage" if name.match?(APPIMAGE_EXT)
|
|
192
|
+
return "deb" if name.match?(DEB_EXT)
|
|
193
|
+
return "rpm" if name.match?(RPM_EXT)
|
|
194
|
+
return "flatpakref" if name.match?(FLATPAKREF_EXT)
|
|
195
|
+
return ArchivePackage.new(name).format if name.match?(ARCHIVE_EXT)
|
|
196
|
+
|
|
197
|
+
"unknown"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def archive_format?(format)
|
|
201
|
+
%w[tar.gz tar.xz tar.zst].include?(format)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def deb_metadata(package)
|
|
205
|
+
fields = package.control_fields
|
|
206
|
+
{
|
|
207
|
+
"debian_binary" => package.debian_binary,
|
|
208
|
+
"ar_members" => package.ar_members,
|
|
209
|
+
"control_archive" => package.control_archive_name,
|
|
210
|
+
"data_archive" => package.data_archive_name,
|
|
211
|
+
"package" => fields["Package"],
|
|
212
|
+
"version" => fields["Version"],
|
|
213
|
+
"architecture" => fields["Architecture"],
|
|
214
|
+
"maintainer" => fields["Maintainer"],
|
|
215
|
+
"description" => fields["Description"],
|
|
216
|
+
"depends" => fields["Depends"],
|
|
217
|
+
"homepage" => fields["Homepage"],
|
|
218
|
+
"maintainer_scripts" => package.maintainer_scripts,
|
|
219
|
+
"desktop_entries" => package.desktop_entries,
|
|
220
|
+
"primary_desktop_entry" => package.primary_desktop_entry,
|
|
221
|
+
"icon_count" => package.icon_entries.size,
|
|
222
|
+
"executable_candidates" => package.executable_entries.first(12),
|
|
223
|
+
"data_entry_count" => package.data_members.size
|
|
224
|
+
}
|
|
225
|
+
rescue DebPackage::FormatError => e
|
|
226
|
+
{ "deb_error" => e.message }
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def deb_warnings(metadata)
|
|
230
|
+
warnings = [
|
|
231
|
+
"This is a Debian package. Debian packages are usually designed for Debian-based distributions and may not behave correctly on every Linux distribution."
|
|
232
|
+
]
|
|
233
|
+
scripts = metadata.fetch("maintainer_scripts", [])
|
|
234
|
+
warnings << "Maintainer scripts are present and will not be executed in Depot portable mode: #{scripts.join(", ")}." unless scripts.empty?
|
|
235
|
+
dependencies = dependency_names(metadata["depends"])
|
|
236
|
+
unless dependencies.empty?
|
|
237
|
+
warnings << "Dependencies are declared and are not automatically installed in portable mode: #{dependency_summary(dependencies)}."
|
|
238
|
+
end
|
|
239
|
+
warnings
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def deb_risks(metadata)
|
|
243
|
+
risks = [
|
|
244
|
+
"Depot installs Debian packages by portable extraction, not by registering them with apt or dpkg.",
|
|
245
|
+
"Some Debian packages assume system paths, services, users, or libraries that may not exist outside Debian-family systems."
|
|
246
|
+
]
|
|
247
|
+
risks << "No desktop launcher was found; Depot may not be able to integrate this package cleanly." unless metadata["primary_desktop_entry"]
|
|
248
|
+
risks
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def dependency_names(depends)
|
|
252
|
+
depends.to_s.split(",").map do |dependency|
|
|
253
|
+
dependency.split("|").first.to_s.strip.sub(/\s*\(.+\)\z/, "")
|
|
254
|
+
end.reject(&:empty?).uniq
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def dependency_summary(dependencies)
|
|
258
|
+
shown = dependencies.first(6).join(", ")
|
|
259
|
+
extra = dependencies.length - 6
|
|
260
|
+
extra.positive? ? "#{dependencies.length} dependencies, including #{shown}, and #{extra} more" : shown
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def archive_metadata(package)
|
|
264
|
+
{
|
|
265
|
+
"archive_format" => package.format,
|
|
266
|
+
"archive_root" => package.common_root,
|
|
267
|
+
"desktop_entries" => package.desktop_entries,
|
|
268
|
+
"primary_desktop_entry" => package.primary_desktop_entry,
|
|
269
|
+
"icon_count" => package.image_entries.size,
|
|
270
|
+
"executable_candidates" => package.executable_candidates,
|
|
271
|
+
"script_entries" => package.script_entries,
|
|
272
|
+
"source_markers" => package.source_markers,
|
|
273
|
+
"data_entry_count" => package.members.size
|
|
274
|
+
}
|
|
275
|
+
rescue ArchivePackage::FormatError => e
|
|
276
|
+
{ "archive_error" => e.message }
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def archive_warnings(metadata)
|
|
280
|
+
warnings = ["This archive is not a formal Linux package. Depot will infer the app layout and will not run installer scripts."]
|
|
281
|
+
scripts = metadata.fetch("script_entries", [])
|
|
282
|
+
markers = metadata.fetch("source_markers", [])
|
|
283
|
+
warnings << "Installer-like scripts were found and will not be executed: #{scripts.first(6).join(", ")}." unless scripts.empty?
|
|
284
|
+
warnings << "Source/build markers were found: #{markers.join(", ")}." unless markers.empty?
|
|
285
|
+
warnings
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def archive_risks(metadata)
|
|
289
|
+
risks = ["Tar archives can contain arbitrary layouts, so Depot uses portable extraction and desktop inference."]
|
|
290
|
+
risks << "No desktop launcher was found; Depot will generate one if it can identify an executable." unless metadata["primary_desktop_entry"]
|
|
291
|
+
risks << "No executable candidate was found; install may not be launchable." if metadata.fetch("executable_candidates", []).empty?
|
|
292
|
+
risks
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def rpm_metadata(package)
|
|
296
|
+
fields = package.package_fields
|
|
297
|
+
{
|
|
298
|
+
"package" => fields["Name"],
|
|
299
|
+
"version" => fields["Version"],
|
|
300
|
+
"release" => fields["Release"],
|
|
301
|
+
"architecture" => fields["Architecture"],
|
|
302
|
+
"summary" => fields["Summary"],
|
|
303
|
+
"description" => fields["Description"],
|
|
304
|
+
"license" => fields["License"],
|
|
305
|
+
"url" => fields["URL"],
|
|
306
|
+
"payload_format" => fields["PayloadFormat"],
|
|
307
|
+
"payload_compressor" => fields["PayloadCompressor"],
|
|
308
|
+
"requires" => package.requires,
|
|
309
|
+
"scriptlets" => package.scriptlets,
|
|
310
|
+
"desktop_entries" => package.desktop_entries,
|
|
311
|
+
"primary_desktop_entry" => package.primary_desktop_entry,
|
|
312
|
+
"icon_count" => package.icon_entries.size,
|
|
313
|
+
"executable_candidates" => package.executable_candidates,
|
|
314
|
+
"data_entry_count" => package.file_entries.size
|
|
315
|
+
}
|
|
316
|
+
rescue RpmPackage::FormatError => e
|
|
317
|
+
{ "rpm_error" => e.message }
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def rpm_warnings(metadata)
|
|
321
|
+
warnings = [
|
|
322
|
+
"This is an RPM package. RPM packages are usually designed for RPM-based distributions and may not behave correctly on every Linux distribution."
|
|
323
|
+
]
|
|
324
|
+
scriptlets = metadata.fetch("scriptlets", [])
|
|
325
|
+
warnings << "RPM scriptlets are present and will not be executed in Depot portable mode: #{scriptlets.join(", ")}." unless scriptlets.empty?
|
|
326
|
+
requirements = rpm_requirement_names(metadata["requires"])
|
|
327
|
+
unless requirements.empty?
|
|
328
|
+
warnings << "RPM requirements are declared and are not automatically installed in portable mode: #{dependency_summary(requirements)}."
|
|
329
|
+
end
|
|
330
|
+
warnings
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def rpm_risks(metadata)
|
|
334
|
+
risks = [
|
|
335
|
+
"Depot installs RPM packages by portable extraction, not by registering them with rpm, dnf, or zypper.",
|
|
336
|
+
"Some RPM packages assume system paths, services, users, or libraries that may not exist outside RPM-family systems."
|
|
337
|
+
]
|
|
338
|
+
risks << "No desktop launcher was found; Depot may not be able to integrate this package cleanly." unless metadata["primary_desktop_entry"]
|
|
339
|
+
risks
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def rpm_requirement_names(requires)
|
|
343
|
+
Array(requires).map do |requirement|
|
|
344
|
+
requirement.to_s.sub(/\s*\(.+\)\z/, "")
|
|
345
|
+
end.reject { |name| name.empty? || name.start_with?("rpmlib(") }.uniq
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def flatpakref_metadata(ref)
|
|
349
|
+
{
|
|
350
|
+
"name" => ref.name,
|
|
351
|
+
"title" => ref.title,
|
|
352
|
+
"branch" => ref.branch,
|
|
353
|
+
"url" => ref.url,
|
|
354
|
+
"suggest_remote_name" => ref.remote_name,
|
|
355
|
+
"is_runtime" => ref.runtime?,
|
|
356
|
+
"runtime_repo" => ref.runtime_repo,
|
|
357
|
+
"gpg_key_present" => ref.gpg_key?,
|
|
358
|
+
"fields" => ref.fields
|
|
359
|
+
}
|
|
360
|
+
rescue FlatpakRef::FormatError => e
|
|
361
|
+
{ "flatpakref_error" => e.message }
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def flatpakref_warnings(metadata)
|
|
365
|
+
warnings = ["This Flatpak reference will be installed through the system Flatpak tool into the user Flatpak installation."]
|
|
366
|
+
warnings << "This ref points to a runtime; Depot currently focuses on Flatpak application refs." if metadata["is_runtime"]
|
|
367
|
+
warnings << "No GPG key is embedded in this ref." unless metadata["gpg_key_present"]
|
|
368
|
+
warnings
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def flatpakref_risks(metadata)
|
|
372
|
+
risks = [
|
|
373
|
+
"Flatpak may download the application, runtime dependencies, and remote metadata from #{metadata["url"] || "the configured remote"}.",
|
|
374
|
+
"Flatpak manages sandboxing, updates, exported launchers, and uninstall behavior for this app."
|
|
375
|
+
]
|
|
376
|
+
risks << "A runtime repo may be added or used: #{metadata["runtime_repo"]}." if metadata["runtime_repo"]
|
|
377
|
+
risks
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def parse_uri(value)
|
|
381
|
+
uri = URI.parse(value)
|
|
382
|
+
uri if uri.scheme && uri.host
|
|
383
|
+
rescue URI::InvalidURIError
|
|
384
|
+
nil
|
|
385
|
+
end
|
|
386
|
+
end
|
|
387
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "backends/app_image"
|
|
5
|
+
require_relative "backends/archive"
|
|
6
|
+
require_relative "backends/deb"
|
|
7
|
+
require_relative "backends/flatpak_ref"
|
|
8
|
+
require_relative "backends/rpm"
|
|
9
|
+
require_relative "inspector"
|
|
10
|
+
require_relative "manifest_store"
|
|
11
|
+
require_relative "result"
|
|
12
|
+
require_relative "sandbox"
|
|
13
|
+
require_relative "settings"
|
|
14
|
+
|
|
15
|
+
module Depot
|
|
16
|
+
class Installer
|
|
17
|
+
def self.install(input, options = {})
|
|
18
|
+
new.install(input, options)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(store: ManifestStore.new, settings: Settings.new)
|
|
22
|
+
@store = store
|
|
23
|
+
@settings = settings
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def install(input, options = {})
|
|
27
|
+
inspection_result = Inspector.inspect(input)
|
|
28
|
+
return inspection_result unless inspection_result.ok?
|
|
29
|
+
|
|
30
|
+
inspection = inspection_result.value
|
|
31
|
+
merged_settings = @settings.load.merge(options.fetch(:settings, {}))
|
|
32
|
+
result = case inspection.format
|
|
33
|
+
when "appimage"
|
|
34
|
+
Backends::AppImage.new(store: @store).install(inspection, settings: merged_settings)
|
|
35
|
+
when "deb"
|
|
36
|
+
Backends::Deb.new(store: @store).install(inspection, settings: merged_settings)
|
|
37
|
+
when "tar.gz", "tar.xz", "tar.zst"
|
|
38
|
+
Backends::Archive.new(store: @store).install(inspection, settings: merged_settings)
|
|
39
|
+
when "rpm"
|
|
40
|
+
Backends::Rpm.new(store: @store).install(inspection, settings: merged_settings)
|
|
41
|
+
when "flatpakref"
|
|
42
|
+
Backends::FlatpakRefBackend.new(store: @store).install(inspection, settings: merged_settings)
|
|
43
|
+
else
|
|
44
|
+
Result.err("No installer backend is available for detected format: #{inspection.format}")
|
|
45
|
+
end
|
|
46
|
+
return result unless result.ok?
|
|
47
|
+
|
|
48
|
+
sandboxed = Sandbox.apply(result.value, settings: merged_settings, store: @store)
|
|
49
|
+
return sandboxed unless sandboxed.ok?
|
|
50
|
+
|
|
51
|
+
Result.ok(sandboxed.value, warnings: result.warnings)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require_relative "paths"
|
|
6
|
+
|
|
7
|
+
module Depot
|
|
8
|
+
class ManifestStore
|
|
9
|
+
attr_reader :dir
|
|
10
|
+
|
|
11
|
+
def initialize(dir = Paths.manifests_dir)
|
|
12
|
+
@dir = dir
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def all
|
|
16
|
+
Dir.glob(File.join(dir, "*.json")).sort.filter_map { |path| read_file(path) }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def ids
|
|
20
|
+
all.map { |manifest| manifest.fetch("app_id") }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def find(app_id)
|
|
24
|
+
path = manifest_path(app_id)
|
|
25
|
+
return nil unless File.exist?(path)
|
|
26
|
+
|
|
27
|
+
read_file(path)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def write(manifest)
|
|
31
|
+
FileUtils.mkdir_p(dir)
|
|
32
|
+
path = manifest_path(manifest.fetch("app_id"))
|
|
33
|
+
File.write(path, JSON.pretty_generate(manifest) + "\n")
|
|
34
|
+
path
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def delete(app_id)
|
|
38
|
+
FileUtils.rm_f(manifest_path(app_id))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def manifest_path(app_id)
|
|
42
|
+
File.join(dir, "#{app_id}.json")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def read_file(path)
|
|
48
|
+
JSON.parse(File.read(path))
|
|
49
|
+
rescue JSON::ParserError
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|