-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #161 from asdil12/rpmbuild
Allow installing non-rpm applications
- Loading branch information
Showing
7 changed files
with
328 additions
and
25 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import requests | ||
import opi | ||
|
||
def http_get_json(url): | ||
r = requests.get(url) | ||
r.raise_for_status() | ||
return r.json() | ||
|
||
def get_releases(org, repo, filter_prereleases=True, filters=[]): | ||
releases = http_get_json(f'https://api.github.com/repos/{org}/{repo}/releases') | ||
if filter_prereleases: | ||
releases = [release for release in releases if not release['prerelease']] | ||
for f in filters: | ||
releases = [r for r in releases if f(r)] | ||
return releases | ||
|
||
def get_latest_release(org, repo, filter_prereleases=True, filters=[]): | ||
releases = get_releases(org, repo, filter_prereleases, filters) | ||
return releases[0] if releases else None | ||
|
||
def get_release_assets(release): | ||
return [{'name': a['name'], 'url': a['browser_download_url']} for a in http_get_json(release['assets_url'])] | ||
|
||
def get_release_asset(release, filters=[]): | ||
assets = get_release_assets(release) | ||
for f in filters: | ||
assets = [r for r in assets if f(r)] | ||
return assets[0] if assets else None | ||
|
||
def install_rpm_release(org, repo, allow_unsigned=False): | ||
latest_release = get_latest_release(org, repo) | ||
if not latest_release: | ||
print(f'No release found for {org}/{repo}') | ||
return | ||
if not opi.ask_yes_or_no(f"Do you want to install {repo} release {latest_release['tag_name']} RPM from {org} github repo?"): | ||
return | ||
asset = get_release_asset(latest_release, filters=[lambda a: a['name'].endswith('.rpm')]) | ||
if not asset: | ||
print(f"No RPM asset found for {org}/{repo} release {latest_release['tag_name']}") | ||
return | ||
opi.install_packages([asset['url']], allow_unsigned=allow_unsigned) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import os | ||
import requests | ||
|
||
def download_file(url, local_filename): | ||
response = requests.get(url, stream=True) | ||
response.raise_for_status() | ||
|
||
total_size = int(response.headers.get('content-length', 0)) | ||
block_size = 1024*512 | ||
|
||
os.makedirs(os.path.dirname(local_filename), exist_ok=True) | ||
|
||
print(f"Downloading to {local_filename}:") | ||
with open(local_filename, 'wb') as local_file: | ||
total_bytes_received = 0 | ||
for data in response.iter_content(chunk_size=block_size): | ||
local_file.write(data) | ||
total_bytes_received += len(data) | ||
print_progress(total_bytes_received, total_size) | ||
print() | ||
|
||
def print_progress(bytes_received, total_size): | ||
progress = (bytes_received / total_size) * 100 | ||
progress = min(100, progress) | ||
print(f"Progress: [{int(progress)}%] [{'=' * int(progress / 2)}{' ' * (50 - int(progress / 2))}]", end='\r') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import os | ||
import opi | ||
from opi.plugins import BasePlugin | ||
from opi import github | ||
from opi import rpmbuild | ||
from opi import http | ||
|
||
class OrcaSlicer(BasePlugin): | ||
main_query = 'orcaslicer' | ||
description = 'Slicer and controller for Bambu and other 3D printers' | ||
queries = [main_query, 'orca-slicer', 'OrcaSlicer'] | ||
|
||
@classmethod | ||
def run(cls, query): | ||
org = 'SoftFever' | ||
repo = 'OrcaSlicer' | ||
latest_release = github.get_latest_release(org, repo) | ||
if not latest_release: | ||
print(f'No release found for {org}/{repo}') | ||
return | ||
version = latest_release['tag_name'].lstrip('v').replace('-', '_') | ||
if not opi.ask_yes_or_no(f"Do you want to install {repo} release {version} from {org} github repo?"): | ||
return | ||
asset = github.get_release_asset(latest_release, filters=[lambda a: a['name'].endswith('.AppImage')]) | ||
if not asset: | ||
print(f"No asset found for {org}/{repo} release {version}") | ||
return | ||
url = asset['url'] | ||
|
||
binary_path = 'usr/bin/OrcaSlicer' | ||
icon_path = 'usr/share/pixmaps/OrcaSlicer.svg' | ||
|
||
rpm = rpmbuild.RPMBuild('OrcaSlicer', version, cls.description, "x86_64", files=[ | ||
f"/{binary_path}", | ||
f"/{icon_path}" | ||
]) | ||
|
||
binary_abspath = os.path.join(rpm.buildroot, binary_path) | ||
http.download_file(url, binary_abspath) | ||
os.chmod(binary_abspath, 0o755) | ||
|
||
icon_abspath = os.path.join(rpm.buildroot, icon_path) | ||
icon_url = "https://raw.githubusercontent.com/SoftFever/OrcaSlicer/2d849f/resources/images/OrcaSlicer.svg" | ||
http.download_file(icon_url, icon_abspath) | ||
|
||
rpm.add_desktop_file(cmd="OrcaSlicer", icon=f"/{icon_path}") | ||
|
||
rpm.build() | ||
|
||
opi.install_packages([rpm.rpmfile_path], allow_unsigned=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import os | ||
import opi | ||
import shutil | ||
from opi.plugins import BasePlugin | ||
from opi import rpmbuild | ||
from opi import snap | ||
from opi import http | ||
|
||
class Spotify(BasePlugin): | ||
main_query = 'spotify' | ||
description = 'Listen to music for a monthly fee' | ||
queries = [main_query] | ||
|
||
@classmethod | ||
def run(cls, query): | ||
s = snap.get_snap('spotify') | ||
if not opi.ask_yes_or_no(f"Do you want to install spotify release {s['version']} converted to RPM from snapcraft repo?"): | ||
return | ||
|
||
binary_path = 'usr/bin/spotify' | ||
data_path = 'usr/share/spotify' | ||
|
||
rpm = rpmbuild.RPMBuild('spotify', s['version'], cls.description, "x86_64", | ||
conflicts = ["spotify-client"], | ||
requires = [ | ||
"libasound2", | ||
"libatk-bridge-2_0-0", | ||
"libatomic1", | ||
"libcurl4", | ||
"libgbm1", | ||
"libglib-2_0-0", | ||
"libgtk-3-0", | ||
"mozilla-nss", | ||
"libopenssl1_1", | ||
"libxshmfence1", | ||
"libXss1", | ||
"libXtst6", | ||
"xdg-utils", | ||
"libayatana-appindicator3-1", | ||
], | ||
autoreq = False, | ||
recommends = [ | ||
"libavcodec.so", | ||
"libavformat.so", | ||
], | ||
suggests = ["libnotify4"], | ||
files = [ | ||
f"/{binary_path}", | ||
f"/{data_path}" | ||
] | ||
) | ||
|
||
snap_path = os.path.join(rpm.tmpdir.name, 'spotify.snap') | ||
snap_path_extracted = os.path.join(rpm.tmpdir.name, 'spotify') | ||
http.download_file(s['url'], snap_path) | ||
snap.extract_snap(snap_path, snap_path_extracted) | ||
|
||
binary_abspath = os.path.join(rpm.buildroot, binary_path) | ||
rpmbuild.copy(os.path.join(snap_path_extracted, binary_path), binary_abspath) | ||
|
||
data_abspath = os.path.join(rpm.buildroot, data_path) | ||
rpmbuild.copy(os.path.join(snap_path_extracted, data_path), data_abspath) | ||
|
||
rpm.add_desktop_file(cmd="spotify %U", icon="/usr/share/spotify/icons/spotify_icon.ico", | ||
Categories="Audio;Music;Player;AudioVideo;", | ||
MimeType="x-scheme-handler/spotify" | ||
) | ||
|
||
rpm.build() | ||
opi.install_packages([rpm.rpmfile_path], allow_unsigned=True) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import os | ||
import tempfile | ||
import re | ||
import subprocess | ||
import glob | ||
import shutil | ||
|
||
def copy(src, dst): | ||
""" | ||
Copy src to dst using hardlinks. | ||
dst will be the full final path. | ||
Directories will be created as needed. | ||
""" | ||
if os.path.islink(src) or os.path.isfile(src): | ||
os.makedirs(os.path.dirname(dst), exist_ok=True) | ||
if os.path.islink(src): | ||
link_target = os.readlink(src) | ||
os.symlink(link_target, dst) | ||
elif os.path.isfile(src): | ||
shitil.copy2(src, dst) | ||
else: | ||
shutil.copytree(src, dst, copy_function=os.link, symlinks=True, ignore_dangling_symlinks=False) | ||
|
||
def dedent(s): | ||
""" Other than textwrap's implementation this one has no problems with some lines unindented. | ||
It will unconditionally strip any leading whitespaces for each line. | ||
""" | ||
return re.sub(r"^\s*", "", s, flags=re.M) | ||
|
||
class RPMBuild: | ||
def __init__(self, name, version, description, buildarch="noarch", | ||
requires=[], recommends=[], provides=[], suggests=[], conflicts=[], autoreq=True, | ||
files=[], dirs=[], config=[]): | ||
self.name = name | ||
self.version = version | ||
self.description = description | ||
self.buildarch = buildarch | ||
self.requires = requires | ||
self.recommends = recommends | ||
self.provides = provides | ||
self.suggests = suggests | ||
self.conflicts = conflicts | ||
self.autoreq = autoreq | ||
self.files = files | ||
self.dirs = dirs | ||
self.config = config | ||
|
||
self.tmpdir = tempfile.TemporaryDirectory() | ||
|
||
self.buildroot = os.path.join(self.tmpdir.name, "buildroot") # buildroot where plugins copy files to | ||
self.spec_path = os.path.join(self.tmpdir.name, "specfile.spec") | ||
self.rpmbuild_buildroot = os.path.join(self.tmpdir.name, "rpmbuildroot") # buildroot internally used by rpmbuild | ||
self.rpm_out_dir = os.path.join(self.tmpdir.name, "rpms") | ||
|
||
os.mkdir(self.buildroot) | ||
os.mkdir(self.rpm_out_dir) | ||
|
||
def mkspec(self): | ||
nl = "\n" | ||
spec = dedent(f""" | ||
Name: {self.name} | ||
Version: {self.version} | ||
Release: 0 | ||
Summary: {self.description} | ||
License: n/a | ||
BuildArch: {self.buildarch} | ||
{nl.join(f"Requires: {r}" for r in self.requires)} | ||
{nl.join(f"Recommends: {r}" for r in self.recommends)} | ||
{nl.join(f"Provides: {r}" for r in self.provides)} | ||
{nl.join(f"Suggests: {r}" for r in self.suggests)} | ||
{nl.join(f"Conflicts: {r}" for r in self.conflicts)} | ||
{"AutoReq: no" if not self.autoreq else ''} | ||
%description | ||
{self.description} | ||
Built locally using OPI. | ||
%install | ||
cp -lav ./buildroot/* %{{buildroot}}/ | ||
%files | ||
{nl.join(self.files)} | ||
{nl.join(f"%dir {d}" for d in self.dirs)} | ||
{nl.join(f"%config {c}" for c in self.config)} | ||
%changelog | ||
""") | ||
return spec | ||
|
||
def add_desktop_file(self, cmd, icon, **kwargs): | ||
os.makedirs(os.path.join(self.buildroot, 'usr/share/applications')) | ||
desktop_path = f'usr/share/applications/{self.name}.desktop' | ||
desktop_abspath = os.path.join(self.buildroot, desktop_path) | ||
self.files.append(f"/{desktop_path}") | ||
with open(desktop_abspath, 'w') as f: | ||
nl = "\n" | ||
f.write(dedent(f""" | ||
[Desktop Entry] | ||
Name={self.name} | ||
Comment={self.description} | ||
Exec={cmd} | ||
Icon={icon} | ||
Type=Application | ||
{nl.join(["%s=%s" % (k, v) for k, v in kwargs.items()])} | ||
""")) | ||
|
||
def build(self): | ||
print(f"Creating RPM for {self.name}") | ||
with open(self.spec_path, 'w') as f: | ||
spec = self.mkspec() | ||
f.write(spec) | ||
subprocess.check_call([ | ||
"rpmbuild", "-bb", "--build-in-place", | ||
"--buildroot", self.rpmbuild_buildroot, | ||
"--define", f"_rpmdir {self.rpm_out_dir}", | ||
"specfile.spec" | ||
], cwd=self.tmpdir.name) | ||
rpmfile = glob.glob(f"{self.rpm_out_dir}/*/*.rpm")[0] | ||
self.rpmfile_path = rpmfile | ||
return rpmfile |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import subprocess | ||
import requests | ||
import opi | ||
|
||
def http_get_json(url): | ||
r = requests.get(url, headers={'Snap-Device-Series': '16'}) | ||
r.raise_for_status() | ||
return r.json() | ||
|
||
def get_snap(snap, channel='stable', arch=None): | ||
channels = http_get_json(f'https://api.snapcraft.io/v2/snaps/info/{snap}')['channel-map'] | ||
if arch: | ||
arch.replace('x86_64', 'amd64') | ||
channels = [c for c in channels if c['channel']['architecture'] == arch] | ||
channels = [c for c in channels if c['channel']['name'] == channel] | ||
c = channels[0] | ||
return {"version": c['version'], "url": c['download']['url']} | ||
|
||
def extract_snap(snap, target_dir): | ||
subprocess.check_call(['unsquashfs', '-d', target_dir, snap]) |