Skip to content

Commit

Permalink
Merge pull request #161 from asdil12/rpmbuild
Browse files Browse the repository at this point in the history
Allow installing non-rpm applications
  • Loading branch information
asdil12 authored Nov 16, 2023
2 parents 7f7eeac + 6a86965 commit 0957d1c
Show file tree
Hide file tree
Showing 7 changed files with 328 additions and 25 deletions.
41 changes: 41 additions & 0 deletions opi/github.py
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)
25 changes: 25 additions & 0 deletions opi/http.py
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')
27 changes: 2 additions & 25 deletions opi/plugins/maptool.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,6 @@
import opi
from opi.plugins import BasePlugin

import requests

def http_get_json(url):
r = requests.get(url)
r.raise_for_status()
return r.json()

def install_github_release(org, repo):
releases = http_get_json(f'https://api.github.com/repos/{org}/{repo}/releases')
releases = [release for release in releases if not release['prerelease']]
if not releases:
print(f'No release found for {org}/{repo}')
return
latest_release = releases[0]
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
assets = http_get_json(latest_release['assets_url'])
rpm_assets = [asset for asset in assets if asset['name'].endswith('.rpm')]
if not rpm_assets:
print(f"No RPM asset found for {org}/{repo} release {latest_release['tag_name']}")
return
rpm_url = rpm_assets[0]['browser_download_url']
opi.install_packages([rpm_url], allow_unsigned=True) # no key available
from opi import github

class MapTool(BasePlugin):
main_query = 'maptool'
Expand All @@ -32,4 +9,4 @@ class MapTool(BasePlugin):

@classmethod
def run(cls, query):
install_github_release('RPTools', 'maptool')
github.install_rpm_release('RPTools', 'maptool', allow_unsigned=True) # no key available
50 changes: 50 additions & 0 deletions opi/plugins/orca_slicer.py
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)
70 changes: 70 additions & 0 deletions opi/plugins/spotify.py
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)
120 changes: 120 additions & 0 deletions opi/rpmbuild.py
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
20 changes: 20 additions & 0 deletions opi/snap.py
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])

0 comments on commit 0957d1c

Please sign in to comment.