Skip to content

Commit e582778

Browse files
authored
Added --ide-path option and other optimizations (#12)
- Added short flags - Raise error if too many redirects - Optimized build detection - Removed info about `--brew` flag
1 parent 482ae6c commit e582778

4 files changed

Lines changed: 87 additions & 80 deletions

File tree

jb_updater/src/jb_updater/cli.cr

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ module JBUpdater
1515
property include_bundled : Bool
1616
property install_ids : Array(String)
1717
property product : String?
18+
property ide_path : String?
1819
property brew_patch : Bool?
1920
property upgrade_ide : Bool
2021
property ide_downloads_host : String
@@ -35,6 +36,7 @@ module JBUpdater
3536
@upgrade_ide = false
3637
@ide_downloads_host = "download-cdn.jetbrains.com"
3738
@arch = nil
39+
@ide_path = nil
3840
end
3941
end
4042

@@ -44,17 +46,20 @@ module JBUpdater
4446
OptionParser.parse do |parser|
4547
parser.banner = "Usage: jb_updater [options]"
4648
parser.on("--plugins-dir DIR", "Plugins directory") { |v| opts.plugins_dir = v }
47-
parser.on("--build BUILD", "IDE build") { |v| opts.build = v }
48-
parser.on("--dry-run", "Dry run") { opts.dry_run = true }
49-
parser.on("--list", "List plugins") { opts.list = true }
50-
parser.on("--install-plugin IDS", "Install plugins (comma-separated)") { |v| opts.install_ids = v.split(',') }
49+
parser.on("-b", "--build BUILD", "IDE build") { |v| opts.build = v }
50+
parser.on("-d", "--dry-run", "Dry run") { opts.dry_run = true }
51+
parser.on("-l", "--list", "List plugins") { opts.list = true }
52+
parser.on("-i", "--install-plugin IDS", "Install plugins (comma-separated)") { |v| opts.install_ids = v.split(',') }
5153
parser.on("--product NAME", "IDE product name (e.g., RubyMine or RubyMine2025.2)") do |v|
5254
opts.product = v
5355
end
5456
parser.on("--arch ARCH", "Architecture (arm or intel); default: autodetect") do |v|
5557
opts.arch = v.downcase
5658
end
5759
parser.on("--upgrade-ide", "Upgrade whole IDE instead of plugins") { opts.upgrade_ide = true }
60+
parser.on("--ide-path PATH", "Specify custom IDE installation path") do |v|
61+
opts.ide_path = v
62+
end
5863
parser.on("--brew", "Patch Homebrew cask Ruby file instead of direct install") { opts.brew_patch = nil }
5964
parser.on("-h", "--help", "Show help") do
6065
puts parser

jb_updater/src/jb_updater/http_client.cr

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ module JBUpdater
3030
# ----------------------------------------------------------------------
3131
# Download a file with redirect support and a simple progress bar
3232
# ----------------------------------------------------------------------
33-
def self.download(uri : URI, dest_path : String) : Nil
33+
def self.download(uri : URI, dest_path : String, depth = 0) : Nil
34+
raise "Too many redirects: #{depth}" if depth > 5
3435
headers = HTTP::Headers{"User-Agent" => USER_AGENT}
3536
client = HTTP::Client.new(uri)
3637
client.before_request { |req| req.headers.merge!(headers) }
@@ -77,7 +78,7 @@ module JBUpdater
7778
path: loc
7879
)
7980
end
80-
return download(next_uri, dest_path)
81+
return download(next_uri, dest_path, depth + 1)
8182
else
8283
raise "redirect without Location header for #{uri}"
8384
end

jb_updater/src/jb_updater/updater.cr

Lines changed: 74 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -50,95 +50,102 @@ module JBUpdater
5050
end
5151
end
5252

53-
private def validate!
54-
raise "plugins_dir required" unless @opts.plugins_dir
55-
raise "Plugins dir not found: #{@opts.plugins_dir}" unless Dir.exists?(@opts.plugins_dir.not_nil!)
56-
end
57-
5853
private def detect_build_info : String?
54+
base_name = File.basename(File.dirname(@opts.plugins_dir.not_nil!)).gsub(/\d.*$/, "")
55+
info_json = ""
56+
build_txt = ""
57+
58+
# Prefer explicit --ide-path
59+
if custom = @opts.ide_path
60+
{% if flag?(:darwin) %}
61+
info_json, build_txt = ide_metadata_paths(:mac, custom)
62+
{% elsif flag?(:linux) %}
63+
info_json, build_txt = ide_metadata_paths(:linux, custom)
64+
{% elsif flag?(:win32) %}
65+
info_json, build_txt = ide_metadata_paths(:windows, custom)
66+
{% else %}
67+
raise "Unsupported platform"
68+
{% end %}
69+
if result = read_build_info(info_json, build_txt, File.basename(custom))
70+
return result
71+
end
72+
end
73+
74+
candidates = [] of String
5975
{% if flag?(:darwin) %}
60-
detect_build_info_macos
76+
candidates = ["/Applications/#{base_name}.app"]
6177
{% elsif flag?(:linux) %}
62-
detect_build_info_linux
78+
candidates = ["/opt/#{base_name}", "/usr/share/#{base_name}", "/snap/#{base_name}/current"]
6379
{% elsif flag?(:win32) %}
64-
detect_build_info_windows
80+
roots = [
81+
ENV["LOCALAPPDATA"]?,
82+
ENV["PROGRAMFILES"]?,
83+
ENV["PROGRAMFILES(X86)"]?,
84+
].compact
85+
candidates = roots.map { |r| File.join(r, "JetBrains", base_name) }
6586
{% else %}
6687
raise "Unsupported platform"
6788
{% end %}
68-
end
6989

70-
private def detect_build_info_macos : String?
71-
base = File.basename(File.dirname(@opts.plugins_dir.not_nil!)).gsub(/\d.*$/, "")
72-
app_path = "/Applications/#{base}.app"
73-
info_json = File.join(app_path, "Contents", "Resources", "product-info.json")
74-
build_txt = File.join(app_path, "Contents", "Resources", "build.txt")
75-
76-
if File.file?(info_json)
77-
data = JSON.parse(File.read(info_json))
78-
79-
code =
80-
data["productCode"]?.try(&.as_s?) ||
81-
data["product"]?.try(&.[]("code")).try(&.as_s?)
82-
83-
build =
84-
data["buildNumber"]?.try(&.as_s?) ||
85-
data["build"]?.try(&.as_s?) ||
86-
data["version"]?.try(&.as_s?)
87-
88-
return "#{code}-#{build}" if code && build
89-
elsif File.file?(build_txt)
90-
build = File.read(build_txt).strip
91-
return "RM-#{build}" unless build.empty?
90+
candidates.each do |root|
91+
{% if flag?(:darwin) %}
92+
info_json, build_txt = ide_metadata_paths(:mac, root)
93+
{% elsif flag?(:linux) %}
94+
info_json, build_txt = ide_metadata_paths(:linux, root)
95+
{% elsif flag?(:win32) %}
96+
info_json, build_txt = ide_metadata_paths(:windows, root)
97+
{% end %}
98+
if result = read_build_info(info_json, build_txt, base_name)
99+
return result
100+
end
92101
end
93102

103+
JBUpdater::Log.warn("Could not detect build for #{base_name}")
94104
nil
95105
end
96106

97-
private def detect_build_info_linux : String?
98-
base = File.basename(File.dirname(@opts.plugins_dir.not_nil!)).gsub(/\d.*$/, "")
99-
candidates = [
100-
"/opt/#{base}",
101-
"/usr/share/#{base}",
102-
"/snap/#{base}/current",
103-
]
104-
105-
candidates.each do |root|
106-
info = File.join(root, "bin", "product-info.json")
107-
if File.file?(info)
108-
data = JSON.parse(File.read(info))
109-
code = data["productCode"]?.try(&.as_s?) || data["product"]?.try(&.[]("code")).try(&.as_s?)
110-
build = data["buildNumber"]?.try(&.as_s?) || data["build"]?.try(&.as_s?) || data["version"]?.try(&.as_s?)
111-
return "#{code}-#{build}" if code && build
112-
end
107+
private def ide_metadata_paths(platform, root : String) : {String, String}
108+
case platform
109+
when :mac
110+
{
111+
File.join(root, "Contents", "Resources", "product-info.json"),
112+
File.join(root, "Contents", "Resources", "build.txt"),
113+
}
114+
else
115+
{
116+
File.join(root, "bin", "product-info.json"),
117+
File.join(root, "build.txt"),
118+
}
113119
end
114-
nil
115120
end
116121

117-
private def detect_build_info_windows : String?
118-
base = File.basename(File.dirname(@opts.plugins_dir.not_nil!)).sub(/\d.*$/, "")
119-
roots = [
120-
ENV["LOCALAPPDATA"]?,
121-
ENV["PROGRAMFILES"]?,
122-
ENV["PROGRAMFILES(X86)"]?,
123-
].compact
124-
125-
roots.each do |root|
126-
path = File.join(root, "JetBrains", base)
127-
info_json = File.join(path, "bin", "product-info.json")
128-
build_txt = File.join(path, "build.txt")
129-
if File.file?(info_json)
130-
j = JSON.parse(File.read(info_json))
131-
code = j["productCode"]?.try(&.as_s?)
132-
build = j["buildNumber"]?.try(&.as_s?)
122+
private def read_build_info(info_json : String, build_txt : String, base_name : String) : String?
123+
if File.file?(info_json)
124+
begin
125+
data = JSON.parse(File.read(info_json))
126+
code =
127+
data["productCode"]?.try(&.as_s?) ||
128+
data["product"]?.try(&.[]("code")).try(&.as_s?)
129+
build =
130+
data["buildNumber"]?.try(&.as_s?) ||
131+
data["build"]?.try(&.as_s?) ||
132+
data["version"]?.try(&.as_s?)
133133
return "#{code}-#{build}" if code && build
134-
elsif File.file?(build_txt)
135-
build = File.read(build_txt).strip
136-
return "#{base}-#{build}" unless build.empty?
134+
rescue ex
135+
JBUpdater::Log.warn("Parse error in #{info_json}: #{ex.message}")
137136
end
137+
elsif File.file?(build_txt)
138+
build = File.read(build_txt).strip
139+
return "#{base_name}-#{build}" unless build.empty?
138140
end
139141
nil
140142
end
141143

144+
private def validate!
145+
raise "plugins_dir required" unless @opts.plugins_dir
146+
raise "Plugins dir not found: #{@opts.plugins_dir}" unless Dir.exists?(@opts.plugins_dir.not_nil!)
147+
end
148+
142149
private def list_plugins : Nil
143150
plugins = installed_plugins
144151
if plugins.empty?
@@ -242,7 +249,6 @@ module JBUpdater
242249
HTTPClient.download(uri, tmp_zip)
243250
Utils.extract_zip(tmp_zip, target_dir)
244251

245-
# Re-parse plugin.xml in the freshly extracted directory
246252
if post = PluginMeta.parse_from_dir(target_dir)
247253
compat = Utils.build_in_range?(@build.not_nil!, post.since, post.until_build)
248254
suffix = compat ? "" : " (still incompatible)"
@@ -257,7 +263,6 @@ module JBUpdater
257263

258264
# --- URL-resolution helpers ----------------------------------------------
259265

260-
# Determine the correct download URL for a given plugin id
261266
private def final_uri(xml_id : String) : URI
262267
if @opts.direct_urls.has_key?(xml_id)
263268
URI.parse(@opts.direct_urls[xml_id])
@@ -268,11 +273,9 @@ module JBUpdater
268273
end
269274
end
270275

271-
# Resolve URL for a specific pinned version
272276
private def resolve_download_url_for_version(xml_id : String, version : String) : URI
273277
base = "https://plugins.jetbrains.com/plugin/download?pluginId=#{Utils.escape(xml_id)}&version=#{Utils.escape(version)}"
274278
res = HTTPClient.head_or_get(base)
275-
276279
case res.status_code
277280
when 301, 302
278281
loc = res.headers["Location"]?
@@ -286,11 +289,9 @@ module JBUpdater
286289
end
287290
end
288291

289-
# Resolve URL for the latest compatible version via pluginManager
290292
private def resolve_download_url_via_plugin_manager(xml_id : String, build : String) : URI
291293
base = "https://plugins.jetbrains.com/pluginManager?action=download&id=#{Utils.escape(xml_id)}&build=#{Utils.escape(build)}"
292294
res = HTTPClient.head_or_get(base)
293-
294295
case res.status_code
295296
when 301, 302
296297
loc = res.headers["Location"]?

jb_updater/src/main.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,5 @@ if opts.plugins_dir
3838
end
3939

4040
# If neither condition matched, print a brief help hint
41-
puts "Usage: jb_updater --plugins-dir <path> [options] or --product <IDE> [--brew]"
41+
puts "Usage: jb_updater --plugins-dir <path> [options] or --product <IDE>"
4242
exit 1

0 commit comments

Comments
 (0)