Skip to content

Commit 429abb6

Browse files
authored
Added installer.py (#318)
1 parent 8026175 commit 429abb6

File tree

1 file changed

+243
-0
lines changed

1 file changed

+243
-0
lines changed

install.py

+243
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
from datetime import datetime
2+
from os import name, getenv
3+
from json import loads
4+
from re import compile, IGNORECASE, sub
5+
from pathlib import Path
6+
from configparser import ConfigParser
7+
from argparse import ArgumentParser
8+
from shutil import copytree, ignore_patterns
9+
from urllib.request import urlopen
10+
from subprocess import check_output
11+
from io import BytesIO
12+
from zipfile import ZipFile
13+
14+
"""
15+
install(-betterfox).py
16+
17+
Usage:
18+
python install.py
19+
20+
21+
When called without arguments, it will:
22+
- Backup your current firefox profile
23+
- Automatically download user.js from the latest Betterfox release compatible with your Firefox version into the profile
24+
- Apply user-overrides in the same directory
25+
26+
However, you can check out install.py/betterfox-install.exe --help to customise most behaviours!
27+
28+
Limitations:
29+
- When using a different repositoy as a source, that repository needs to use the same releases workflow
30+
- Over time, the get_releases might not list older releases due to limited page size. This can be expanded down the road, though
31+
32+
Building into an exe (on Windows):
33+
- pipx install pyinstaller (note: you can try without pipx, but this didn't work for me)
34+
- Run:
35+
- CMD: `pyinstaller --onefile --name install-betterfox install.py && move %cd%\dist\install-betterfox.exe %cd% && del install-betterfox.spec && rmdir /S /Q build && rmdir dist`
36+
- BASH: `pyinstaller --onefile --name install-betterfox install.py && && mv dist/install-betterfox.exe . && rm install-betterfox.spec && rm -rf ./build/ && rmdir dist`
37+
(Sorry, didn't want to add a .gitignore solely for the install script)
38+
- Done!
39+
40+
If there's any problems with the script, feel free to mention @Denperidge on GitHub!
41+
"""
42+
43+
re_find_version = compile(r"mozilla.org/.*?/firefox/(?P<version>[\d.]*?)/", IGNORECASE)
44+
re_find_overrides = r"(overrides|prefs).*\n(?P<space>\n)"
45+
46+
FIREFOX_ROOT = Path.home().joinpath(".mozilla/firefox").absolute() if name != "nt" else Path(getenv("APPDATA") + "/Mozilla/Firefox/").resolve()
47+
DEFAULT_FIREFOX_INSTALL = Path("C:/Program Files/Mozilla Firefox/" if name == "nt" else "")
48+
49+
selected_if_backup = None
50+
selected_config = ""
51+
userjs_path = None
52+
53+
54+
def _get_firefox_version(bin="firefox"):
55+
try:
56+
ver_string = check_output([bin, "--version"], encoding="UTF-8")
57+
return ver_string[ver_string.rindex(" ")+1:].strip()
58+
except FileNotFoundError:
59+
return _get_firefox_version(str(DEFAULT_FIREFOX_INSTALL.joinpath("firefox")))
60+
61+
def _get_default_profile_folder():
62+
config_path = FIREFOX_ROOT.joinpath("profiles.ini")
63+
64+
print(f"Reading {config_path}...")
65+
66+
config_parser = ConfigParser(strict=False)
67+
config_parser.read(config_path)
68+
69+
for section in config_parser.sections():
70+
if "Default" in config_parser[section]:
71+
if config_parser[section]["Default"] == "1":
72+
print("Default detected: " + section)
73+
return FIREFOX_ROOT.joinpath(config_parser[section]["Path"])
74+
75+
76+
def _get_releases(repository_owner, repository_name):
77+
releases = []
78+
raw_releases = loads(urlopen(f"https://api.github.com/repos/{repository_owner}/{repository_name}/releases").read())
79+
for raw_release in raw_releases:
80+
name = raw_release["name"] or raw_release["tag_name"] # or fixes 126.0 not being lodaded
81+
body = raw_release["body"]
82+
83+
84+
# Find which firefox releases are supported. Manual overrides for ones that don't have it written in their thing!
85+
if name == "user.js v.122.1":
86+
supported = ["107.0", "107.1", "108.0", "108.0.1", "108.0.2", "109.0", "109.0", "110.1", "110.0.1", "111.0", "111.0.1", "112.0", "112.0.1", "112.0.2", "113.0", "113.0.1", "113.0.2", "114.0", "114.0.1", "114.0.2", "115.0", "115.0.1", "115.0.2", "115.0.3", "115.1.0", "115.10.0", "115.11.0", "115.12.0", "115.13.0", "115.14.0", "115.15.0", "115.16.0", "115.16.1", "115.17.0", "115.2.0", "115.2.1", "115.3.0", "115.3.1", "115.4.0", "115.5.0", "115.6.0", "115.7.0", "115.8.0", "115.9.0", "115.9.1", "116.0", "116.0.1", "116.0.2", "116.0.3", "117.0", "117.0.1", "118.0", "118.0.1", "118.0.2", "119.0", "119.0.1", "120.0", "120.0.1", "121.0", "121.0.1", "122.0", "122.0.1"]
87+
elif name == "user.js 116.1":
88+
supported = ["116.0", "116.0.1", "116.0.2", "116.0.3"]
89+
elif name == "Betterfox v.107":
90+
supported = ["107.0"]
91+
elif "firefox release" in body.lower():
92+
trim_body = body.lower()[body.lower().index("firefox release"):]
93+
supported = re_find_version.findall(trim_body)
94+
if len(supported) == 0:
95+
print(f"Could not parse release in '{name}'. Please post this error message on https://github.com/{repository_owner}/{repository_name}/issues")
96+
continue
97+
else:
98+
print(f"Could not find firefox release header '{name}'. Please post this error message on https://github.com/{repository_owner}/{repository_name}/issues")
99+
continue
100+
101+
releases.append({
102+
"name": name,
103+
"url": raw_release["zipball_url"],
104+
"supported": supported,
105+
})
106+
return releases
107+
108+
def _get_latest_compatible_release(releases):
109+
for release in releases:
110+
if firefox_version in release["supported"]:
111+
return release
112+
return None
113+
114+
115+
def backup_profile(src):
116+
dest = f"{src}-backup-{datetime.today().strftime('%Y-%m-%d-%H-%M-%S')}"
117+
118+
copytree(src, dest, ignore=ignore_patterns("*lock"))
119+
print("Backed up profile to " + dest)
120+
121+
122+
def download_betterfox(url):
123+
data = BytesIO()
124+
data.write(urlopen(url).read())
125+
return data
126+
127+
def extract_betterfox(data, profile_folder):
128+
zipfile = ZipFile(data)
129+
userjs_zipinfo = None
130+
for file in zipfile.filelist:
131+
if file.filename.endswith("user.js"):
132+
userjs_zipinfo = file
133+
userjs_zipinfo.filename = Path(userjs_zipinfo.filename).name
134+
135+
if not userjs_zipinfo:
136+
raise BaseException("Could not find user.js!")
137+
138+
return zipfile.extract(userjs_zipinfo, profile_folder)
139+
140+
141+
def list_releases(releases, only_supported=False, add_index=False):
142+
print()
143+
print(f"Listing {'compatible' if only_supported else 'all'} Betterfox releases:")
144+
if only_supported:
145+
print("Use --list-all to view all available releases")
146+
else:
147+
print(f"Releases marked with '> ' are documented to be compatible with your Firefox version ({firefox_version})")
148+
print()
149+
150+
i = 0
151+
for release in releases:
152+
supported = firefox_version in release["supported"]
153+
if not only_supported or (only_supported and supported):
154+
print(f"{f'[{i}]' if add_index else ''}{'> ' if supported else ' '}{release['name'].ljust(20)}\t\t\tSupported: {','.join(release['supported'])}")
155+
i+=1
156+
157+
158+
if __name__ == "__main__":
159+
firefox_version = _get_firefox_version()
160+
selected_release = None
161+
162+
default_profile_folder = _get_default_profile_folder()
163+
argparser = ArgumentParser(
164+
165+
)
166+
argparser.add_argument("--overrides", "-o", default=default_profile_folder.joinpath("user-overrides.js"), help="if the provided file exists, add overrides to user.js. Defaults to " + str(default_profile_folder.joinpath("user-overrides.js"))),
167+
168+
169+
advanced = argparser.add_argument_group("Advanced")
170+
advanced.add_argument("--betterfox-version", "-bv", default=None, help=f"Which version of Betterfox to install. Defaults to the latest compatible release for your installed Firefox version")
171+
advanced.add_argument("--profile-dir", "-p", "-pd", default=default_profile_folder, help=f"Which profile dir to install user.js in. Defaults to {default_profile_folder}")
172+
advanced.add_argument("--repository-owner", "-ro", default="yokoffing", help="owner of the Betterfox repository. Defaults to yokoffing")
173+
advanced.add_argument("--repository-name", "-rn", default="Betterfox", help="name of the Betterfox repository. Defaults to Betterfox")
174+
175+
disable = argparser.add_argument_group("Disable functionality")
176+
disable.add_argument("--no-backup", "-nb", action="store_true", default=False, help="disable backup of current profile (not recommended)"),
177+
disable.add_argument("--no-install", "-ni", action="store_true", default=False, help="don't install Betterfox"),
178+
179+
modes = argparser.add_mutually_exclusive_group()
180+
modes.add_argument("--list", action="store_true", default=False, help=f"List all Betterfox releases compatible with your version of Firefox ({firefox_version})")
181+
modes.add_argument("--list-all", action="store_true", default=False, help=f"List all Betterfox releases")
182+
modes.add_argument("--interactive", "-i", action="store_true", default=False, help=f"Interactively select Betterfox version")
183+
184+
args = argparser.parse_args()
185+
186+
releases = _get_releases(args.repository_owner, args.repository_name)
187+
188+
189+
if args.list or args.list_all:
190+
list_releases(releases, args.list)
191+
input("Press ENTER to exit...")
192+
exit()
193+
194+
if not args.no_backup:
195+
backup_profile(args.profile_dir)
196+
197+
if args.betterfox_version:
198+
# If not None AND not string, default value has been used
199+
if not isinstance(args.betterfox_version, str):
200+
selected_release = args.betterfox_version
201+
print(f"Using latest compatible Betterfox version ({selected_release['name']})...")
202+
# If string has been passed
203+
else:
204+
selected_release = next(rel for rel in releases if rel['name'] == args.betterfox_version)
205+
print(f"Using manually selected Betterfox version ({selected_release['name']})")
206+
207+
if not args.betterfox_version:
208+
selected_release = _get_latest_compatible_release(releases)
209+
210+
if args.interactive or not selected_release:
211+
if not selected_release:
212+
print("Could not find a compatible Betterfox version for your Firefox installation.")
213+
214+
list_releases(releases, False, True)
215+
selection = int(input(f"Select Betterfox version, or press enter without typing a number to cancel [0-{len(releases) - 1}]: "))
216+
217+
selected_release = releases[selection]
218+
219+
220+
221+
if not args.no_install:
222+
userjs_path = extract_betterfox(
223+
download_betterfox(selected_release["url"]),
224+
args.profile_dir
225+
)
226+
print(f"Installed user.js to {userjs_path} !")
227+
228+
229+
if Path(args.overrides).exists():
230+
print("Found overrides at " + str(args.overrides))
231+
232+
with open(str(args.overrides), "r", encoding="utf-8") as overrides_file:
233+
overrides = overrides_file.read()
234+
with open(userjs_path, "r", encoding="utf-8") as userjs_file:
235+
old_content = userjs_file.read()
236+
new_content = sub(re_find_overrides, "\n" + overrides + "\n", old_content, count=1, flags=IGNORECASE)
237+
with open(userjs_path, "w", encoding="utf-8") as userjs_file:
238+
userjs_file.write(new_content)
239+
else:
240+
print(f"Found no overrides in {args.overrides}")
241+
242+
input("Press ENTER to exit...")
243+

0 commit comments

Comments
 (0)