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,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "uri"
|
|
4
|
+
|
|
5
|
+
module Depot
|
|
6
|
+
module SourceResolver
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def resolve(source)
|
|
10
|
+
source = source.to_s
|
|
11
|
+
return source if !source.empty? && File.exist?(source)
|
|
12
|
+
|
|
13
|
+
fixture_match(source)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def url?(source)
|
|
17
|
+
uri = URI.parse(source.to_s)
|
|
18
|
+
uri.absolute? && !uri.scheme.to_s.empty?
|
|
19
|
+
rescue URI::InvalidURIError
|
|
20
|
+
false
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def https_url?(source)
|
|
24
|
+
uri = URI.parse(source.to_s)
|
|
25
|
+
uri.is_a?(URI::HTTPS)
|
|
26
|
+
rescue URI::InvalidURIError
|
|
27
|
+
false
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def fixture_match(source)
|
|
31
|
+
return nil if source.empty?
|
|
32
|
+
|
|
33
|
+
Dir[File.expand_path("../../fixtures/**/#{File.basename(source)}", __dir__)].first
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "open3"
|
|
5
|
+
require_relative "manifest_store"
|
|
6
|
+
require_relative "result"
|
|
7
|
+
|
|
8
|
+
module Depot
|
|
9
|
+
class Uninstaller
|
|
10
|
+
def self.uninstall(app_id)
|
|
11
|
+
new.uninstall(app_id)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(store: ManifestStore.new)
|
|
15
|
+
@store = store
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def uninstall(app_id)
|
|
19
|
+
manifest = @store.find(app_id)
|
|
20
|
+
return Result.err("No installed app found for #{app_id}") unless manifest
|
|
21
|
+
|
|
22
|
+
flatpak = uninstall_flatpak(manifest)
|
|
23
|
+
return flatpak unless flatpak.ok?
|
|
24
|
+
|
|
25
|
+
deleted = []
|
|
26
|
+
manifest.fetch("created_files", []).each do |path|
|
|
27
|
+
next unless safe_delete_file?(path)
|
|
28
|
+
|
|
29
|
+
FileUtils.rm_f(path)
|
|
30
|
+
deleted << path
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
manifest.fetch("created_dirs", []).reverse_each do |path|
|
|
34
|
+
next unless safe_delete_dir?(path)
|
|
35
|
+
|
|
36
|
+
FileUtils.rm_rf(path) if Dir.exist?(path)
|
|
37
|
+
rescue SystemCallError
|
|
38
|
+
nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@store.delete(app_id)
|
|
42
|
+
Result.ok({ "app_id" => app_id, "deleted_files" => deleted })
|
|
43
|
+
rescue SystemCallError => e
|
|
44
|
+
Result.err("Uninstall failed: #{e.message}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def uninstall_flatpak(manifest)
|
|
50
|
+
return Result.ok unless manifest["backend"] == "flatpak"
|
|
51
|
+
|
|
52
|
+
flatpak_id = manifest.dig("flatpak", "app_id") || manifest.dig("package", "Name")
|
|
53
|
+
return Result.err("Flatpak manifest is missing the Flatpak app ID.") if flatpak_id.to_s.empty?
|
|
54
|
+
return Result.err("Flatpak is not installed on this system.") unless command_available?("flatpak")
|
|
55
|
+
|
|
56
|
+
stdout, stderr, status = Open3.capture3("flatpak", "uninstall", "--user", "--noninteractive", "-y", flatpak_id)
|
|
57
|
+
return Result.ok if status.success?
|
|
58
|
+
|
|
59
|
+
message = [stderr, stdout].map(&:to_s).find { |text| text.strip != "" } || "flatpak uninstall failed"
|
|
60
|
+
Result.err("Flatpak uninstall failed: #{message.strip}")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def safe_delete_file?(path)
|
|
64
|
+
return false unless path.is_a?(String) && !path.empty?
|
|
65
|
+
return false unless File.file?(path) || File.symlink?(path)
|
|
66
|
+
|
|
67
|
+
expanded = File.expand_path(path)
|
|
68
|
+
allowed_roots.any? { |root| expanded.start_with?(root + File::SEPARATOR) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def safe_delete_dir?(path)
|
|
72
|
+
return false unless path.is_a?(String) && !path.empty?
|
|
73
|
+
|
|
74
|
+
expanded = File.expand_path(path)
|
|
75
|
+
expanded.start_with?(File.expand_path(Depot::Paths.apps_dir) + File::SEPARATOR)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def allowed_roots
|
|
79
|
+
@allowed_roots ||= [
|
|
80
|
+
Depot::Paths.data_dir,
|
|
81
|
+
Depot::Paths.desktop_entries_dir,
|
|
82
|
+
Depot::Paths.icon_root
|
|
83
|
+
].map { |path| File.expand_path(path) }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def command_available?(command)
|
|
87
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? { |dir| File.executable?(File.join(dir, command)) }
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "tmpdir"
|
|
7
|
+
require "uri"
|
|
8
|
+
require_relative "result"
|
|
9
|
+
require_relative "version"
|
|
10
|
+
|
|
11
|
+
module Depot
|
|
12
|
+
module UpdateDownloader
|
|
13
|
+
MAX_DOWNLOAD_BYTES = 2 * 1024 * 1024 * 1024
|
|
14
|
+
MAX_REDIRECTS = 5
|
|
15
|
+
OPEN_TIMEOUT_SECONDS = 15
|
|
16
|
+
READ_TIMEOUT_SECONDS = 60
|
|
17
|
+
KNOWN_SUFFIXES = [
|
|
18
|
+
".flatpakref",
|
|
19
|
+
".AppImage",
|
|
20
|
+
".appimage",
|
|
21
|
+
".tar.gz",
|
|
22
|
+
".tgz",
|
|
23
|
+
".tar.xz",
|
|
24
|
+
".txz",
|
|
25
|
+
".tar.zst",
|
|
26
|
+
".tzst",
|
|
27
|
+
".deb",
|
|
28
|
+
".rpm"
|
|
29
|
+
].freeze
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def download(url, max_bytes: MAX_DOWNLOAD_BYTES, redirects: MAX_REDIRECTS)
|
|
34
|
+
return Result.err("Update downloader requires a block.") unless block_given?
|
|
35
|
+
|
|
36
|
+
uri = parse_https_url(url)
|
|
37
|
+
return uri unless uri.ok?
|
|
38
|
+
|
|
39
|
+
Dir.mktmpdir("depot-update-") do |dir|
|
|
40
|
+
path = File.join(dir, "package#{suffix_for(uri.value.path)}")
|
|
41
|
+
result = download_to(uri.value, path, max_bytes:, redirects:)
|
|
42
|
+
return result unless result.ok?
|
|
43
|
+
|
|
44
|
+
yield path, result.value
|
|
45
|
+
end
|
|
46
|
+
rescue SystemCallError => e
|
|
47
|
+
Result.err("Could not download update: #{e.message}")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def https_url?(url)
|
|
51
|
+
parse_https_url(url).ok?
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def parse_https_url(url)
|
|
55
|
+
uri = URI.parse(url.to_s)
|
|
56
|
+
return Result.err("Update URL must use https://.") unless uri.is_a?(URI::HTTPS)
|
|
57
|
+
return Result.err("Update URL is missing a host.") if uri.host.to_s.empty?
|
|
58
|
+
|
|
59
|
+
Result.ok(uri)
|
|
60
|
+
rescue URI::InvalidURIError
|
|
61
|
+
Result.err("Update URL must be a valid https:// URL.")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def download_to(uri, path, max_bytes:, redirects:)
|
|
65
|
+
return Result.err("Update download redirected too many times.") if redirects.negative?
|
|
66
|
+
|
|
67
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
68
|
+
http.use_ssl = true
|
|
69
|
+
http.open_timeout = OPEN_TIMEOUT_SECONDS
|
|
70
|
+
http.read_timeout = READ_TIMEOUT_SECONDS
|
|
71
|
+
|
|
72
|
+
request = Net::HTTP::Get.new(uri)
|
|
73
|
+
request["User-Agent"] = "Depot/#{Depot::VERSION}"
|
|
74
|
+
result = nil
|
|
75
|
+
http.request(request) do |response|
|
|
76
|
+
result = case response
|
|
77
|
+
when Net::HTTPSuccess
|
|
78
|
+
stream_response(uri, response, path, max_bytes)
|
|
79
|
+
when Net::HTTPRedirection
|
|
80
|
+
follow_redirect(uri, response, path, max_bytes:, redirects:)
|
|
81
|
+
else
|
|
82
|
+
Result.err("Update download failed: HTTP #{response.code}")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
result || Result.err("Update download failed before Depot received a response.")
|
|
86
|
+
rescue Timeout::Error, IOError, SystemCallError => e
|
|
87
|
+
Result.err("Update download failed: #{e.message}")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def follow_redirect(uri, response, path, max_bytes:, redirects:)
|
|
91
|
+
location = response["location"].to_s
|
|
92
|
+
return Result.err("Update download redirected without a location.") if location.empty?
|
|
93
|
+
|
|
94
|
+
redirected = URI.join(uri, location)
|
|
95
|
+
return Result.err("Update redirects must stay on https://.") unless redirected.is_a?(URI::HTTPS)
|
|
96
|
+
|
|
97
|
+
download_to(redirected, path, max_bytes:, redirects: redirects - 1)
|
|
98
|
+
rescue URI::InvalidURIError
|
|
99
|
+
Result.err("Update download redirected to an invalid URL.")
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def stream_response(uri, response, path, max_bytes)
|
|
103
|
+
content_length = response["content-length"].to_i
|
|
104
|
+
if content_length.positive? && content_length > max_bytes
|
|
105
|
+
return Result.err("Update is too large to download safely (#{content_length} bytes).")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
digest = Digest::SHA256.new
|
|
109
|
+
bytes = 0
|
|
110
|
+
File.open(path, "wb") do |file|
|
|
111
|
+
response.read_body do |chunk|
|
|
112
|
+
bytes += chunk.bytesize
|
|
113
|
+
return Result.err("Update exceeded Depot's #{max_bytes} byte safety limit.") if bytes > max_bytes
|
|
114
|
+
|
|
115
|
+
digest.update(chunk)
|
|
116
|
+
file.write(chunk)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
return Result.err("Update download was empty.") if bytes.zero?
|
|
121
|
+
|
|
122
|
+
Result.ok(
|
|
123
|
+
{
|
|
124
|
+
"url" => uri.to_s,
|
|
125
|
+
"size" => bytes,
|
|
126
|
+
"sha256" => digest.hexdigest
|
|
127
|
+
}
|
|
128
|
+
)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def suffix_for(path)
|
|
132
|
+
basename = File.basename(path.to_s)
|
|
133
|
+
KNOWN_SUFFIXES.find { |suffix| basename.end_with?(suffix) } || File.extname(basename)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "installer"
|
|
6
|
+
require_relative "manifest_store"
|
|
7
|
+
require_relative "result"
|
|
8
|
+
require_relative "settings"
|
|
9
|
+
require_relative "source_resolver"
|
|
10
|
+
require_relative "update_downloader"
|
|
11
|
+
require_relative "uninstaller"
|
|
12
|
+
|
|
13
|
+
module Depot
|
|
14
|
+
class Updater
|
|
15
|
+
def self.update(app_id, options = {})
|
|
16
|
+
new.update(app_id, options)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.update_all(options = {})
|
|
20
|
+
new.update_all(options)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.set_source(app_id, url)
|
|
24
|
+
new.set_source(app_id, url)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def initialize(store: ManifestStore.new, settings: Settings.new)
|
|
28
|
+
@store = store
|
|
29
|
+
@settings = settings
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def records
|
|
33
|
+
@store.all.map { |manifest| update_record(manifest) }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def update(app_id, options = {})
|
|
37
|
+
return Result.err("Updates are disabled in Depot settings.") unless updates_enabled?(options)
|
|
38
|
+
|
|
39
|
+
manifest = @store.find(app_id)
|
|
40
|
+
return Result.err("No installed app found for #{app_id}") unless manifest
|
|
41
|
+
|
|
42
|
+
case manifest.fetch("backend", "")
|
|
43
|
+
when "flatpak"
|
|
44
|
+
update_flatpak(manifest)
|
|
45
|
+
else
|
|
46
|
+
reinstall_from_source(manifest, options)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def update_all(options = {})
|
|
51
|
+
return Result.err("Updates are disabled in Depot settings.") unless updates_enabled?(options)
|
|
52
|
+
|
|
53
|
+
results = records.map do |record|
|
|
54
|
+
next { "app_id" => record.fetch("app_id"), "ok" => false, "error" => record.fetch("status") } unless record.fetch("enabled")
|
|
55
|
+
|
|
56
|
+
result = update(record.fetch("app_id"), options)
|
|
57
|
+
{ "app_id" => record.fetch("app_id"), "ok" => result.ok?, "error" => result.error, "value" => result.value }
|
|
58
|
+
end
|
|
59
|
+
failed = results.reject { |result| result.fetch("ok") }
|
|
60
|
+
return Result.err("#{failed.length} updates failed.", warnings: failed.map { |result| "#{result.fetch("app_id")}: #{result.fetch("error")}" }) unless failed.empty?
|
|
61
|
+
|
|
62
|
+
Result.ok({ "updated" => results })
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def set_source(app_id, url)
|
|
66
|
+
manifest = @store.find(app_id)
|
|
67
|
+
return Result.err("No installed app found for #{app_id}") unless manifest
|
|
68
|
+
|
|
69
|
+
return Result.err("Update URL must use https://.") unless UpdateDownloader.https_url?(url)
|
|
70
|
+
|
|
71
|
+
update = manifest.fetch("update", {}).merge(
|
|
72
|
+
"source" => url,
|
|
73
|
+
"mechanism" => "url-download",
|
|
74
|
+
"status" => "ready",
|
|
75
|
+
"last_checked_at" => Time.now.utc.iso8601
|
|
76
|
+
)
|
|
77
|
+
@store.write(manifest.merge("update" => update))
|
|
78
|
+
Result.ok(@store.find(app_id))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def updates_enabled?(options)
|
|
84
|
+
return true if options[:force]
|
|
85
|
+
|
|
86
|
+
@settings.load.fetch("updates_enabled", true)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def update_record(manifest)
|
|
90
|
+
source = SourceResolver.resolve(manifest["install_source"])
|
|
91
|
+
update_url = update_url(manifest)
|
|
92
|
+
method = update_method(manifest)
|
|
93
|
+
enabled = method == "flatpak" || !source.nil? || !update_url.nil?
|
|
94
|
+
status = update_status(method, source, update_url, manifest)
|
|
95
|
+
{
|
|
96
|
+
"app_id" => manifest.fetch("app_id"),
|
|
97
|
+
"display_name" => manifest.fetch("display_name"),
|
|
98
|
+
"backend" => manifest.fetch("backend"),
|
|
99
|
+
"method" => method,
|
|
100
|
+
"enabled" => enabled,
|
|
101
|
+
"status" => status,
|
|
102
|
+
"last_updated_at" => manifest.dig("update", "last_updated_at")
|
|
103
|
+
}
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def update_method(manifest)
|
|
107
|
+
return "flatpak" if manifest.fetch("backend") == "flatpak"
|
|
108
|
+
return "url-download" if update_url(manifest)
|
|
109
|
+
|
|
110
|
+
"reinstall-source"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def update_status(method, source, update_url, manifest)
|
|
114
|
+
return "Ready" if method == "flatpak"
|
|
115
|
+
return "Ready from URL" if update_url
|
|
116
|
+
return "Ready" if source
|
|
117
|
+
|
|
118
|
+
raw_source = manifest.dig("update", "source") || manifest["install_source"]
|
|
119
|
+
if SourceResolver.url?(raw_source)
|
|
120
|
+
"Only https update URLs are supported"
|
|
121
|
+
else
|
|
122
|
+
"Original installer is missing"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def update_url(manifest)
|
|
127
|
+
source = manifest.dig("update", "source")
|
|
128
|
+
source = manifest["install_source"] if source.to_s.empty?
|
|
129
|
+
return nil unless SourceResolver.https_url?(source)
|
|
130
|
+
|
|
131
|
+
source
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def update_flatpak(manifest)
|
|
135
|
+
flatpak_id = manifest.dig("flatpak", "app_id") || manifest.dig("package", "Name")
|
|
136
|
+
return Result.err("Flatpak manifest is missing the Flatpak app ID.") if flatpak_id.to_s.empty?
|
|
137
|
+
return Result.err("Flatpak is not installed on this system.") unless command_available?("flatpak")
|
|
138
|
+
|
|
139
|
+
stdout, stderr, status = Open3.capture3("flatpak", "update", "--user", "--noninteractive", "-y", flatpak_id)
|
|
140
|
+
return Result.err("Flatpak update failed: #{command_message(stdout, stderr)}") unless status.success?
|
|
141
|
+
|
|
142
|
+
update_manifest(manifest, "flatpak", "updated")
|
|
143
|
+
Result.ok(@store.find(manifest.fetch("app_id")), warnings: [command_message(stdout, stderr)].reject(&:empty?))
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def reinstall_from_source(manifest, options)
|
|
147
|
+
url = update_url(manifest)
|
|
148
|
+
return reinstall_from_url(manifest, url, options) if url
|
|
149
|
+
|
|
150
|
+
source = SourceResolver.resolve(manifest["install_source"])
|
|
151
|
+
return Result.err("Original installer is missing: #{manifest["install_source"]}") unless source
|
|
152
|
+
|
|
153
|
+
uninstall = Uninstaller.new(store: @store).uninstall(manifest.fetch("app_id"))
|
|
154
|
+
return uninstall unless uninstall.ok?
|
|
155
|
+
|
|
156
|
+
install = Installer.new(store: @store, settings: @settings).install(source, settings: @settings.load.merge(options.fetch(:settings, {})))
|
|
157
|
+
return install unless install.ok?
|
|
158
|
+
|
|
159
|
+
updated = install.value
|
|
160
|
+
update_manifest(updated, "reinstall-source", "updated")
|
|
161
|
+
Result.ok(@store.find(updated.fetch("app_id")), warnings: install.warnings)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def reinstall_from_url(manifest, url, options)
|
|
165
|
+
UpdateDownloader.download(url) do |path, metadata|
|
|
166
|
+
inspection = Inspector.inspect(path)
|
|
167
|
+
return inspection unless inspection.ok?
|
|
168
|
+
|
|
169
|
+
compatible = compatible_update?(manifest, inspection.value)
|
|
170
|
+
return Result.err(compatible) unless compatible == true
|
|
171
|
+
|
|
172
|
+
uninstall = Uninstaller.new(store: @store).uninstall(manifest.fetch("app_id"))
|
|
173
|
+
return uninstall unless uninstall.ok?
|
|
174
|
+
|
|
175
|
+
install = Installer.new(store: @store, settings: @settings).install(path, settings: @settings.load.merge(options.fetch(:settings, {})))
|
|
176
|
+
return install unless install.ok?
|
|
177
|
+
|
|
178
|
+
updated = install.value
|
|
179
|
+
stored = @store.find(updated.fetch("app_id")) || updated
|
|
180
|
+
@store.write(stored.merge("install_source" => url))
|
|
181
|
+
update_manifest(
|
|
182
|
+
@store.find(updated.fetch("app_id")) || stored,
|
|
183
|
+
"url-download",
|
|
184
|
+
"updated",
|
|
185
|
+
"source" => url,
|
|
186
|
+
"last_download_sha256" => metadata.fetch("sha256"),
|
|
187
|
+
"last_download_size" => metadata.fetch("size")
|
|
188
|
+
)
|
|
189
|
+
Result.ok(@store.find(updated.fetch("app_id")), warnings: install.warnings)
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def compatible_update?(manifest, inspection)
|
|
194
|
+
expected = expected_formats(manifest.fetch("backend", ""))
|
|
195
|
+
return true if expected.include?(inspection.format)
|
|
196
|
+
|
|
197
|
+
"Downloaded update is #{inspection.format}, but #{manifest.fetch("display_name")} was installed as #{manifest.fetch("backend")}. Depot will not switch package families during an update."
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def expected_formats(backend)
|
|
201
|
+
case backend
|
|
202
|
+
when "appimage" then ["appimage"]
|
|
203
|
+
when "deb-portable" then ["deb"]
|
|
204
|
+
when "rpm-portable" then ["rpm"]
|
|
205
|
+
when "archive-portable" then %w[tar.gz tar.xz tar.zst]
|
|
206
|
+
when "flatpak" then ["flatpakref"]
|
|
207
|
+
else [backend]
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def update_manifest(manifest, method, status, extra = {})
|
|
212
|
+
stored = @store.find(manifest.fetch("app_id")) || manifest
|
|
213
|
+
update = stored.fetch("update", {}).merge(
|
|
214
|
+
"mechanism" => method,
|
|
215
|
+
"status" => status,
|
|
216
|
+
"last_checked_at" => Time.now.utc.iso8601,
|
|
217
|
+
"last_updated_at" => Time.now.utc.iso8601
|
|
218
|
+
).merge(extra)
|
|
219
|
+
@store.write(stored.merge("update" => update))
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def command_message(stdout, stderr)
|
|
223
|
+
[stderr, stdout].map(&:to_s).find { |text| text.strip != "" }.to_s.strip
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def command_available?(command)
|
|
227
|
+
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).any? { |dir| File.executable?(File.join(dir, command)) }
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
data/lib/depot/util.rb
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Depot
|
|
6
|
+
module Util
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def sha256(path)
|
|
10
|
+
Digest::SHA256.file(path).hexdigest
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def slug(value)
|
|
14
|
+
base = File.basename(value.to_s, ".*").downcase
|
|
15
|
+
base = base.gsub(/[^a-z0-9]+/, "-").gsub(/\A-+|-+\z/, "")
|
|
16
|
+
base.empty? ? "app" : base
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def unique_id(base, taken)
|
|
20
|
+
candidate = base
|
|
21
|
+
index = 2
|
|
22
|
+
while taken.include?(candidate)
|
|
23
|
+
candidate = "#{base}-#{index}"
|
|
24
|
+
index += 1
|
|
25
|
+
end
|
|
26
|
+
candidate
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def desktop_exec_quote(path)
|
|
30
|
+
%("#{path.gsub(/(["\\`$])/, '\\\\\\1')}")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
data/lib/depot.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "depot/version"
|
|
4
|
+
require_relative "depot/paths"
|
|
5
|
+
require_relative "depot/result"
|
|
6
|
+
require_relative "depot/settings"
|
|
7
|
+
require_relative "depot/manifest_store"
|
|
8
|
+
require_relative "depot/assets"
|
|
9
|
+
require_relative "depot/packages/archive"
|
|
10
|
+
require_relative "depot/packages/deb"
|
|
11
|
+
require_relative "depot/packages/rpm"
|
|
12
|
+
require_relative "depot/packages/flatpak_ref"
|
|
13
|
+
require_relative "depot/inspector"
|
|
14
|
+
require_relative "depot/installer"
|
|
15
|
+
require_relative "depot/uninstaller"
|
|
16
|
+
require_relative "depot/app_customizer"
|
|
17
|
+
require_relative "depot/doctor"
|
|
18
|
+
require_relative "depot/source_resolver"
|
|
19
|
+
require_relative "depot/sandbox"
|
|
20
|
+
require_relative "depot/update_downloader"
|
|
21
|
+
require_relative "depot/updater"
|
metadata
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: depot-linux
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- netizensnoopy
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rubyzip
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: toml
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rspec
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '3.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '3.0'
|
|
68
|
+
description: Install AppImage, .deb, .rpm, .flatpakref, tar.gz on Linux without root
|
|
69
|
+
email:
|
|
70
|
+
- netizensnoopy@users.noreply.github.com
|
|
71
|
+
executables:
|
|
72
|
+
- depot
|
|
73
|
+
extensions: []
|
|
74
|
+
extra_rdoc_files: []
|
|
75
|
+
files:
|
|
76
|
+
- Gemfile
|
|
77
|
+
- README.md
|
|
78
|
+
- Rakefile
|
|
79
|
+
- bin/depot
|
|
80
|
+
- bin/depot-gui
|
|
81
|
+
- bin/setup-rubyqt6
|
|
82
|
+
- fixtures/assets/download.png
|
|
83
|
+
- fixtures/flatpakrefs/org.qbittorrent.qBittorrent.flatpakref
|
|
84
|
+
- fixtures/rpms/Modrinth App-0.13.14-1.x86_64.rpm
|
|
85
|
+
- lib/depot.rb
|
|
86
|
+
- lib/depot/app_customizer.rb
|
|
87
|
+
- lib/depot/assets.rb
|
|
88
|
+
- lib/depot/backends/app_image.rb
|
|
89
|
+
- lib/depot/backends/archive.rb
|
|
90
|
+
- lib/depot/backends/deb.rb
|
|
91
|
+
- lib/depot/backends/flatpak_ref.rb
|
|
92
|
+
- lib/depot/backends/rpm.rb
|
|
93
|
+
- lib/depot/backends/support.rb
|
|
94
|
+
- lib/depot/cli.rb
|
|
95
|
+
- lib/depot/desktop_entry.rb
|
|
96
|
+
- lib/depot/doctor.rb
|
|
97
|
+
- lib/depot/gui/app.rb
|
|
98
|
+
- lib/depot/gui/drop_panel.rb
|
|
99
|
+
- lib/depot/gui/main_window.rb
|
|
100
|
+
- lib/depot/inspection.rb
|
|
101
|
+
- lib/depot/inspector.rb
|
|
102
|
+
- lib/depot/installer.rb
|
|
103
|
+
- lib/depot/manifest_store.rb
|
|
104
|
+
- lib/depot/packages/archive.rb
|
|
105
|
+
- lib/depot/packages/deb.rb
|
|
106
|
+
- lib/depot/packages/flatpak_ref.rb
|
|
107
|
+
- lib/depot/packages/rpm.rb
|
|
108
|
+
- lib/depot/paths.rb
|
|
109
|
+
- lib/depot/result.rb
|
|
110
|
+
- lib/depot/sandbox.rb
|
|
111
|
+
- lib/depot/settings.rb
|
|
112
|
+
- lib/depot/source_resolver.rb
|
|
113
|
+
- lib/depot/uninstaller.rb
|
|
114
|
+
- lib/depot/update_downloader.rb
|
|
115
|
+
- lib/depot/updater.rb
|
|
116
|
+
- lib/depot/util.rb
|
|
117
|
+
- lib/depot/version.rb
|
|
118
|
+
homepage: https://github.com/netizensnoopy/depot
|
|
119
|
+
licenses:
|
|
120
|
+
- MIT
|
|
121
|
+
metadata: {}
|
|
122
|
+
rdoc_options: []
|
|
123
|
+
require_paths:
|
|
124
|
+
- lib
|
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
126
|
+
requirements:
|
|
127
|
+
- - ">="
|
|
128
|
+
- !ruby/object:Gem::Version
|
|
129
|
+
version: '3.0'
|
|
130
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
131
|
+
requirements:
|
|
132
|
+
- - ">="
|
|
133
|
+
- !ruby/object:Gem::Version
|
|
134
|
+
version: '0'
|
|
135
|
+
requirements: []
|
|
136
|
+
rubygems_version: 3.6.9
|
|
137
|
+
specification_version: 4
|
|
138
|
+
summary: Unified Linux app installer
|
|
139
|
+
test_files: []
|