Revised and tested build cli subcommand.

This commit is contained in:
emiliano.vavassori 2023-12-05 00:12:10 +01:00
parent 024535afa9
commit 1a24f54d89
3 changed files with 361 additions and 336 deletions

View File

@ -4,6 +4,7 @@
import datetime import datetime
import json import json
from re import I
import requests import requests
from lxml import html from lxml import html
from packaging.version import parse as parse_version from packaging.version import parse as parse_version
@ -49,6 +50,7 @@ class Version():
def __init__(self): def __init__(self):
self.query = '' self.query = ''
self.branch = ''
self.version = '' self.version = ''
self.urls = { self.urls = {
'x86': '-', 'x86': '-',
@ -82,6 +84,7 @@ class Solver():
def __init__(self, text: str, default_to_current = False): def __init__(self, text: str, default_to_current = False):
self.text = text self.text = text
self.branch = text
self.version = None self.version = None
self.default_to_current = default_to_current self.default_to_current = default_to_current
self.baseurl = ARCHIVE self.baseurl = ARCHIVE
@ -110,9 +113,20 @@ class Solver():
def to_version(self): def to_version(self):
retval = Version() retval = Version()
retval.query = self.text retval.query = self.text
retval.branch = self.branch
retval.version = self.version retval.version = self.version
retval.urls['x86_64'] = self.baseurl if retval.branch != 'daily':
retval.urls['x86_64'] = self.baseurl + 'x86_64/'
try:
x86ver = match_xpath(self.baseurl + 'x86/', '//td/a/text()')
except Exception:
return retval
if len(x86ver) > 1:
retval.urls['x86'] = self.baseurl + 'x86/'
else:
retval.urls['x86_64'] = self.baseurl
return retval return retval
@staticmethod @staticmethod
@ -127,6 +141,7 @@ class DailySolver(Solver):
def __init__(self, text: str, default_to_current = False): def __init__(self, text: str, default_to_current = False):
super().__init__(text, default_to_current) super().__init__(text, default_to_current)
self.branch = 'daily'
self.baseurl = DAILY self.baseurl = DAILY
def solve(self): def solve(self):
@ -146,10 +161,8 @@ class DailySolver(Solver):
matching = datetime.datetime.strptime(self.text, "%Y%m%d").strftime('%Y-%m-%d') matching = datetime.datetime.strptime(self.text, "%Y%m%d").strftime('%Y-%m-%d')
except ValueError: except ValueError:
# All textual version # All textual version
if self.text == 'current': if self.text in { 'current', 'daily' }:
matching = 'current' matching = 'current'
elif self.text == 'daily':
matching = today.strftime('%Y-%m-%d')
elif self.text == 'yesterday': elif self.text == 'yesterday':
matching = (today + datetime.timedelta(days=-1)).strftime("%Y-%m-%d") matching = (today + datetime.timedelta(days=-1)).strftime("%Y-%m-%d")
@ -163,6 +176,8 @@ class DailySolver(Solver):
else: else:
self.baseurl = self.baseurl + results[-1] self.baseurl = self.baseurl + results[-1]
# baseurl for x86 is not available for sure on daily builds.
xpath_string = "//td/a[contains(text(), '_deb.tar.gz')]/text()" xpath_string = "//td/a[contains(text(), '_deb.tar.gz')]/text()"
links = match_xpath(self.baseurl, xpath_string) links = match_xpath(self.baseurl, xpath_string)
if len(links) > 0: if len(links) > 0:
@ -177,6 +192,7 @@ class NamedSolver(Solver):
def __init__(self, text: str): def __init__(self, text: str):
super().__init__(text) super().__init__(text)
self.branch = text
self.baseurl = SELECTORS[self.text]['URL'] self.baseurl = SELECTORS[self.text]['URL']
self.generalver = '' self.generalver = ''
@ -197,7 +213,7 @@ class NamedSolver(Solver):
# Return just the last versions # Return just the last versions
fullversion: str = str(archived_versions[-1]) fullversion: str = str(archived_versions[-1])
self.baseurl = ARCHIVE + fullversion + 'deb/x86_64/' self.baseurl = ARCHIVE + fullversion + 'deb/'
self.version = fullversion.rstrip('/') self.version = fullversion.rstrip('/')
return self.version return self.version
@ -208,6 +224,7 @@ class NumberedSolver(Solver):
def __init__(self, text: str): def __init__(self, text: str):
super().__init__(text) super().__init__(text)
self.branch = '.'.join(text.split('.')[0-2])
def solve(self): def solve(self):
xpath_string = f"//td/a[starts-with(text(),'{self.text}')]/text()" xpath_string = f"//td/a[starts-with(text(),'{self.text}')]/text()"
@ -217,7 +234,7 @@ class NumberedSolver(Solver):
return self.version return self.version
version = str(versions[-1]) version = str(versions[-1])
self.baseurl = self.baseurl + version + 'deb/x86_64/' self.baseurl = self.baseurl + version + 'deb/'
self.version = version.rstrip('/') self.version = version.rstrip('/')
return self.version return self.version

View File

@ -9,12 +9,11 @@ import shutil
import re import re
import shlex import shlex
import tempfile import tempfile
import urllib.error
import urllib.request
import hashlib import hashlib
from lxml import etree import requests
import loaih import loaih
class Collection(list): class Collection(list):
"""Aggregates metadata on a collection of builds.""" """Aggregates metadata on a collection of builds."""
@ -28,6 +27,7 @@ class Collection(list):
arch = [ x for x in arch if version.urls[x] != '-' ] arch = [ x for x in arch if version.urls[x] != '-' ]
self.extend([ Build(version, ar) for ar in arch ]) self.extend([ Build(version, ar) for ar in arch ])
class Build(): class Build():
"""Builds a single version.""" """Builds a single version."""
@ -42,12 +42,7 @@ class Build():
self.verbose = True self.verbose = True
self.arch = arch self.arch = arch
self.short_version = str.join('.', self.version.version.split('.')[0:2]) self.short_version = str.join('.', self.version.version.split('.')[0:2])
self.branch_version = None self.branch_version = self.version.branch
if not '.' in self.version.query:
self.branch_version = self.version.query
numeric = re.match(r'^[0-9]{8}$', self.version.query)
if numeric or self.version.query in { 'yesterday', 'current' }:
self.branch_version = 'daily'
self.url = self.version.urls[arch] self.url = self.version.urls[arch]
# Other default values - for structured builds # Other default values - for structured builds
@ -68,8 +63,8 @@ class Build():
self.appname = 'LibreOffice' self.appname = 'LibreOffice'
self.appversion = '' self.appversion = ''
self.appimagedir = '' self.appimagedir = ''
self.appimagefilename = {} self.appimagefilename = ''
self.zsyncfilename = {} self.zsyncfilename = ''
# Other variables by build # Other variables by build
self.languagepart = '.' + self.language self.languagepart = '.' + self.language
@ -78,7 +73,8 @@ class Build():
# Creating a tempfile # Creating a tempfile
self.builddir = tempfile.mkdtemp() self.builddir = tempfile.mkdtemp()
self.tarballs = {} self.tarballs = {}
self.built = { 'x86': False, 'x86_64': False } self.found = False
self.built = False
# Preparing the default for the relative path on the storage for # Preparing the default for the relative path on the storage for
# different versions. # different versions.
@ -88,6 +84,7 @@ class Build():
self.full_path = '' self.full_path = ''
self.baseurl = '' self.baseurl = ''
def calculate(self): def calculate(self):
"""Calculate exclusions and other variables.""" """Calculate exclusions and other variables."""
@ -121,19 +118,258 @@ class Build():
self.helppart = '.help' self.helppart = '.help'
# Building the required names # Building the required names
for arch in Build.ARCHSTD: self.appimagefilename = self.__gen_appimagefilename__()
self.appimagefilename[arch] = self.__gen_appimagefilename__(self.version, arch) self.zsyncfilename = self.appimagefilename + '.zsync'
self.zsyncfilename[arch] = self.appimagefilename[arch] + '.zsync'
# Mandate to the private function to calculate the full_path available # Mandate to the private function to calculate the full_path available
# for the storage and the checks. # for the storage and the checks.
self.__calculate_full_path__() self.__calculate_full_path__()
def __gen_appimagefilename__(self, version, arch): def check(self):
"""Checking if the requested AppImage has been already built."""
if self.branch_version == 'daily':
# Daily versions have to be downloaded and built each time; no
# matter if another one is already present.
return
if self.verbose:
print("--- Check Phase ---")
if len(self.appimagefilename) == 0:
self.calculate()
if self.verbose:
print(f"Searching for {self.appimagefilename}")
# First, check if by metadata the repo is remote or not.
if self.repo_type == 'remote':
# Remote storage. I have to query a remote site to know if it
# was already built.
name = self.appimagefilename
url = self.storage_path.rstrip('/') + self.full_path + '/'
matching = []
try:
if len(loaih.match_xpath(url, f"//a[contains(@href,'{name}')]/@href")) > 0:
# Already built.
self.found = True
except Exception:
# The URL specified do not exist. So it is to build.
self.found = False
else:
# Repo is local
command = f"find {self.full_path} -name {self.appimagefilename}"
res = subprocess.run(shlex.split(command),
capture_output=True,
env={ "LC_ALL": "C" },
text=True, encoding='utf-8', check=True)
if res.stdout and len(res.stdout.strip("\n")) > 0:
# All good, the command was executed fine.
self.found = True
if self.found:
if self.verbose:
print(f"Found requested AppImage: {self.appimagefilename}.")
def download(self):
"""Downloads the contents of the URL as it was a folder."""
if self.verbose:
print("--- Download Phase ---")
if self.found:
return
if self.verbose:
print(f"Started downloads for {self.version.version}. Please wait.")
# Checking if a valid path has been provided
if self.url == '-':
if self.verbose:
print(f"Cannot build for arch {self.arch}. Continuing with other arches.")
# Faking already built it so to skip other checks.
self.found = True
# Identifying downloads
self.tarballs = [ x for x in loaih.match_xpath(self.url, "//td/a/text()")
if x.endswith('tar.gz') and 'deb' in x ]
# Create and change directory to the download location
os.makedirs(self.download_path, exist_ok = True)
os.chdir(self.download_path)
for archive in self.tarballs:
# If the archive is already there, do not do anything.
# If it is a daily build or a pre-release, due to filename
# clashes, redownload the whole build.
if os.path.exists(archive) and self.version.query not in { 'daily', 'prerelease' }:
continue
# Download the archive
try:
self.__download_archive__(archive)
except Exception as error:
print(f"Failed to download {archive}: {error}.")
if self.verbose:
print(f"Finished downloads for {self.version.version}.")
def build(self):
"""Building all the versions."""
if self.verbose:
print("--- Building Phase ---")
if self.found:
return
# Preparation tasks
self.appnamedir = os.path.join(self.builddir, self.appname)
os.makedirs(self.appnamedir, exist_ok=True)
# And then cd to the appname folder.
os.chdir(self.appnamedir)
# Download appimagetool from github
appimagetoolurl = r"https://github.com/AppImage/AppImageKit/releases/"
appimagetoolurl += f"download/continuous/appimagetool-{self.arch}.AppImage"
self.__download__(appimagetoolurl, 'appimagetool')
os.chmod('appimagetool', 0o755)
# Build the requested version.
self.__unpackbuild__()
def checksums(self):
"""Create checksums of the built versions."""
# Skip checksum if initally the build was already found in the storage directory
if self.verbose:
print("--- Checksum Phase ---")
if self.found:
return
os.chdir(self.appnamedir)
if self.built:
for item in [ self.appimagefilename, self.zsyncfilename ]:
itempath = os.path.join(self.appnamedir, item)
if os.path.exists(itempath):
self.__create_checksum__(item)
def publish(self):
"""Moves built versions to definitive storage."""
if self.verbose:
print("--- Publish Phase ---")
if self.found:
# All files are already present in the full_path
return
os.chdir(self.appnamedir)
# Two cases here: local and remote storage_path.
if self.repo_type == 'remote':
# Remote first.
# Build destination directory
remotepath = self.remote_path.rstrip('/') + self.full_path
try:
if self.verbose:
subprocess.run(
r"rsync -rlIvz --munge-links *.AppImage* " +
f"{self.remote_host}:{remotepath}",
cwd=self.appnamedir, shell=True, check=True
)
else:
subprocess.run(
r"rsync -rlIvz --munge-links *.AppImage* " +
f"{self.remote_host}:{remotepath}",
cwd=self.appnamedir, shell=True, check=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
finally:
pass
else:
# Local
# Forcing creation of subfolders, in case there is a new build
os.makedirs(self.full_path, exist_ok = True)
for file in glob.glob("*.AppImage*"):
subprocess.run(shlex.split(
f"cp -f {file} {self.full_path}"
), check=True)
def generalize_and_link(self, chdir = 'default'):
"""Creates the needed generalized files if needed."""
if self.verbose:
print("--- Generalize and Link Phase ---")
# If called with a pointed version, no generalize and link necessary.
if not self.branch_version:
return
# If a prerelease or a daily version, either.
if self.version.query in { 'daily', 'prerelease' }:
return
if chdir == 'default':
chdir = self.full_path
appimagefilename = r''
zsyncfilename = r''
# Creating versions for short version and query text
versions = [ self.short_version, self.branch_version ]
os.chdir(chdir)
# if the appimage for the reported arch is not found, skip to next
# arch
if not os.path.exists(self.appimagefilename):
return
# Doing it both for short_name and for branchname
for version in versions:
appimagefilename = f"{self.appname}-{version}"
appimagefilename += f"{self.languagepart}{self.helppart}"
appimagefilename += f'-{self.arch}.AppImage'
zsyncfilename = appimagefilename + '.zsync'
# Create the symlink
if self.verbose:
print(f"Creating {appimagefilename} and checksums.")
if os.path.exists(appimagefilename):
os.unlink(appimagefilename)
os.symlink(self.appimagefilename, appimagefilename)
# Create the checksum for the AppImage
self.__create_checksum__(appimagefilename)
# Do not continue if no zsync file is provided.
if not self.updatable:
continue
if self.verbose:
print(f"Creating zsync file for version {version}.")
if os.path.exists(zsyncfilename):
os.unlink(zsyncfilename)
shutil.copyfile(self.zsyncfilename, zsyncfilename)
# Editing the zsyncfile
subprocess.run(shlex.split(
r"sed --in-place 's/^Filename:.*$/Filename: " +
f"{appimagefilename}/' {zsyncfilename}"
), check=True)
self.__create_checksum__(zsyncfilename)
### Private methods ###
def __gen_appimagefilename__(self):
"""Generalize the construction of the name of the app.""" """Generalize the construction of the name of the app."""
self.appversion = version + self.languagepart + self.helppart self.appversion = self.version.version + self.languagepart + self.helppart
return self.appname + f'-{self.appversion}-{arch}.AppImage' return self.appname + f'-{self.appversion}-{self.arch}.AppImage'
def __calculate_full_path__(self): def __calculate_full_path__(self):
@ -142,7 +378,7 @@ class Build():
if self.tidy_folder: if self.tidy_folder:
if self.branch_version == 'daily': if self.branch_version == 'daily':
self.relative_path.append('daily') self.relative_path.append('daily')
elif self.query == 'prerelease': elif self.branch_version == 'prerelease':
self.relative_path.append('prerelease') self.relative_path.append('prerelease')
# Not the same check, an additional one # Not the same check, an additional one
@ -166,173 +402,93 @@ class Build():
fullpath_arr.extend(self.relative_path) fullpath_arr.extend(self.relative_path)
self.full_path = re.sub(r"/+", '/', str.join('/', fullpath_arr)) self.full_path = re.sub(r"/+", '/', str.join('/', fullpath_arr))
def __create_checksum__(self, file):
"""Internal function to create checksum file."""
def check(self): retval = hashlib.md5()
"""Checking if the requested AppImage has been already built.""" with open(file, 'rb') as rawfile:
while True:
buf = rawfile.read(2**20)
if not buf:
break
if self.verbose: retval.update(buf)
print("--- Check Phase ---")
if len(self.appimagefilename) != 2: checksum = subprocess.run(f"md5sum {file}", shell=True,
self.calculate() capture_output=True, text=True, encoding='utf-8', check=True,
cwd=self.appnamedir)
for arch in self.arch: if checksum.stdout:
if self.verbose: with open(f"{file}.md5", 'w', encoding='utf-8') as checkfile:
print(f"Searching for {self.appimagefilename[arch]}") checkfile.write(f"{retval.hexdigest()} {os.path.basename(file)}")
# First, check if by metadata the repo is remote or not.
if self.repo_type == 'remote':
# Remote storage. I have to query a remote site to know if it
# was already built.
name = self.appimagefilename[arch]
url = self.storage_path.rstrip('/') + self.full_path + '/'
matching = []
try:
with urllib.request.urlopen(url) as response:
matching = etree.HTML(response.read()).xpath(
f"//a[contains(@href, '{name}')]/@href"
)
if len(matching) > 0: def __download_archive__(self, archive):
# Already built. return self.__download__(self.url, archive)
self.built[arch] = True
except urllib.error.HTTPError: def __download__(self, url: str, filename: str):
# The URL specified do not exist. So it is to build. basename = filename
pass if '/' in filename:
basename = filename.split('/')[-1]
else: full_url = url
# Repo is local if url.endswith('/'):
command = f"find {self.full_path} -name {self.appimagefilename[arch]}" # URL has to be completed with basename of filename
res = subprocess.run(shlex.split(command), full_url = url + basename
capture_output=True,
env={ "LC_ALL": "C" },
text=True, encoding='utf-8', check=True)
if "No such file or directory" in res.stderr: with requests.get(full_url, stream=True, timeout=10) as resource:
# Folder is not existent: so the version was not built resource.raise_for_status()
# Build stays false, and we go to the next arch with open(filename, 'wb') as file:
continue for chunk in resource.iter_content(chunk_size=8192):
file.write(chunk)
return filename
if res.stdout and len(res.stdout.strip("\n")) > 0: def __unpackbuild__(self):
# All good, the command was executed fine.
self.built[arch] = True
if self.built[arch]:
if self.verbose:
print(f"Found requested AppImage: {self.appimagefilename[arch]}.")
def download(self):
"""Downloads the contents of the URL as it was a folder."""
if self.verbose:
print("--- Download Phase ---")
print(f"Started downloads for {self.version}. Please wait.")
for arch in self.arch:
# Checking if a valid path has been provided
if self.url[arch] == '-':
if self.verbose:
print(f"Cannot build for arch {arch}. Continuing with other arches.")
# Faking already built it so to skip other checks.
self.built[arch] = True
continue
if self.built[arch]:
if self.verbose:
print(f"A build for {arch} was already found. Skipping specific packages.")
continue
# Identifying downloads
contents = loaih.match_xpath(self.url[arch], "//td/a/text()")
self.tarballs[arch] = [ x for x in contents if x.endswith('tar.gz') and 'deb' in x ]
tarballs = self.tarballs[arch]
# Create and change directory to the download location
os.makedirs(self.download_path, exist_ok = True)
os.chdir(self.download_path)
for archive in tarballs:
# If the archive is already there, do not do anything.
# If it is a daily build or a pre-release, due to filename
# clashes, redownload the whole build.
if os.path.exists(archive) and self.version.query not in { 'daily', 'prerelease' }:
continue
# Download the archive
try:
urllib.request.urlretrieve(self.url[arch] + archive, archive)
except Exception as error:
print(f"Failed to download {archive}: {error}.")
if self.verbose:
print(f"Finished downloads for {self.version}.")
def build(self):
"""Building all the versions."""
if self.verbose:
print("--- Building Phase ---")
for arch in self.arch:
if self.built[arch]:
# Already built for arch or path not available. User has already been warned.
continue
# Preparation tasks
self.appnamedir = os.path.join(self.builddir, self.appname)
os.makedirs(self.appnamedir, exist_ok=True)
# And then cd to the appname folder.
os.chdir(self.appnamedir)
# Download appimagetool from github
appimagetoolurl = f"https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-{arch}.AppImage"
urllib.request.urlretrieve(appimagetoolurl, 'appimagetool')
os.chmod('appimagetool', 0o755)
# Build the requested version.
self.__unpackbuild__(arch)
def __unpackbuild__(self, arch):
# We start by filtering out tarballs from the list # We start by filtering out tarballs from the list
buildtarballs = [ self.tarballs[arch][0] ] buildtarballs = [ self.tarballs[0] ]
# Let's process standard languages and append results to the # Let's process standard languages and append results to the
# buildtarball # buildtarball
if self.language == 'basic': if self.language == 'basic':
if self.offline_help: if self.offline_help:
buildtarballs.extend([ x for x in self.tarballs[arch] if 'pack_en-GB' in x ]) buildtarballs.extend([ x for x in self.tarballs if 'pack_en-GB' in x ])
else: else:
buildtarballs.extend([ x for x in self.tarballs[arch] if 'langpack_en-GB' in x]) buildtarballs.extend([ x for x in self.tarballs if 'langpack_en-GB' in x])
elif self.language == 'standard': elif self.language == 'standard':
for lang in Build.LANGSTD: for lang in Build.LANGSTD:
if self.offline_help: if self.offline_help:
buildtarballs.extend([ x for x in self.tarballs[arch] if ('pack_' + lang) in x ]) buildtarballs.extend([ x for x in self.tarballs if 'pack_' + lang in x ])
else: else:
buildtarballs.extend([ x for x in self.tarballs[arch] if ('langpack_' + lang) in x ]) buildtarballs.extend([ x for x in self.tarballs if 'langpack_' + lang in x ])
elif self.language == 'full': elif self.language == 'full':
if self.offline_help: if self.offline_help:
# We need also all help. Let's replace buildtarball with the # We need also all help. Let's replace buildtarball with the
# whole bunch # whole bunch
buildtarballs = self.tarballs[arch] buildtarballs = self.tarballs
else: else:
buildtarballs.extend([ x for x in self.tarballs[arch] if 'langpack' in x ]) buildtarballs.extend([ x for x in self.tarballs if 'langpack' in x ])
else: else:
# Looping for each language in self.language # Looping for each language in self.language
for lang in self.language.split(","): for lang in self.language.split(","):
if self.offline_help: if self.offline_help:
buildtarballs.extend([ x for x in self.tarballs[arch] buildtarballs.extend([ x for x in self.tarballs
if 'pack' + lang in x ]) if 'pack' + lang in x ])
else: else:
buildtarballs.extend([ x for x in self.tarballs[arch] buildtarballs.extend([ x for x in self.tarballs
if 'langpack' + lang in x ]) if 'langpack' + lang in x ])
os.chdir(self.appnamedir) os.chdir(self.appnamedir)
# Unpacking the tarballs # Unpacking the tarballs
if self.verbose:
print("---- Unpacking ----")
for archive in buildtarballs: for archive in buildtarballs:
subprocess.run(shlex.split( subprocess.run(shlex.split(
f"tar xzf {self.download_path}/{archive}"), check=True) f"tar xzf {self.download_path}/{archive}"), check=True)
# create appimagedir # create appimagedir
if self.verbose:
print("---- Preparing the build ----")
self.appimagedir = os.path.join(self.builddir, self.appname, self.appname + '.AppDir') self.appimagedir = os.path.join(self.builddir, self.appname, self.appname + '.AppDir')
os.makedirs(self.appimagedir, exist_ok = True) os.makedirs(self.appimagedir, exist_ok = True)
@ -387,9 +543,9 @@ class Build():
# Download AppRun from github # Download AppRun from github
apprunurl = r"https://github.com/AppImage/AppImageKit/releases/" apprunurl = r"https://github.com/AppImage/AppImageKit/releases/"
apprunurl += f"download/continuous/AppRun-{arch}" apprunurl += f"download/continuous/AppRun-{self.arch}"
dest = os.path.join(self.appimagedir, 'AppRun') dest = os.path.join(self.appimagedir, 'AppRun')
urllib.request.urlretrieve(apprunurl, dest) self.__download__(apprunurl, dest)
os.chmod(dest, 0o755) os.chmod(dest, 0o755)
# Dealing with extra options # Dealing with extra options
@ -399,21 +555,24 @@ class Build():
# adding zsync build if updatable # adding zsync build if updatable
if self.updatable: if self.updatable:
buildopts.append(f"-u 'zsync|{self.zsyncfilename[arch]}'") buildopts.append(f"-u 'zsync|{self.zsyncfilename}'")
buildopts_str = str.join(' ', buildopts) buildopts_str = str.join(' ', buildopts)
# Build the number-specific build # Build the number-specific build
if self.verbose: if self.verbose:
subprocess.run(shlex.split( print("---- Start building ----")
f"{self.appnamedir}/appimagetool {buildopts_str} -v " +
f"./{self.appname}.AppDir/"
), env={ "VERSION": self.appversion }, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
else:
subprocess.run(shlex.split( subprocess.run(shlex.split(
f"{self.appnamedir}/appimagetool {buildopts_str} -v " + f"{self.appnamedir}/appimagetool {buildopts_str} -v " +
f"./{self.appname}.AppDir/" f"./{self.appname}.AppDir/"
), env={ "VERSION": self.appversion }, check=True) ), env={ "VERSION": self.appversion }, check=True)
print("---- End building ----")
else:
subprocess.run(shlex.split(
f"{self.appnamedir}/appimagetool {buildopts_str} -v " +
f"./{self.appname}.AppDir/"
), env={ "VERSION": self.appversion }, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, check=True)
if self.verbose: if self.verbose:
print(f"Built AppImage version {self.appversion}") print(f"Built AppImage version {self.appversion}")
@ -425,157 +584,7 @@ class Build():
r"find . -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} \+" r"find . -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} \+"
), check=True) ), check=True)
self.built = True
def checksums(self):
"""Create checksums of the built versions."""
# Skip checksum if initally the build was already found in the storage directory
if self.verbose:
print("--- Checksum Phase ---")
if all(self.built[arch] for arch in self.arch):
return
os.chdir(self.appnamedir)
for arch in self.arch:
if not self.built[arch]:
# Here's the contrary. A newly built package has not yet been
# marked as built.
for item in [ self.appimagefilename[arch], self.zsyncfilename[arch] ]:
itempath = os.path.join(self.appnamedir, item)
if os.path.exists(itempath):
# For any built arch, find out if a file exist.
self.__create_checksum__(item)
def __create_checksum__(self, file):
"""Internal function to create checksum file."""
retval = hashlib.md5()
with open(file, 'rb') as rawfile:
while True:
buf = rawfile.read(2**20)
if not buf:
break
retval.update(buf)
checksum = subprocess.run(f"md5sum {file}", shell=True,
capture_output=True, text=True, encoding='utf-8', check=True,
cwd=self.appnamedir)
if checksum.stdout:
with open(f"{file}.md5", 'w', encoding='utf-8') as checkfile:
checkfile.write(f"{retval.hexdigest()} {os.path.basename(file)}")
def publish(self):
"""Moves built versions to definitive storage."""
if self.verbose:
print("--- Publish Phase ---")
if all(self.built[arch] for arch in self.arch):
# All files are already present in the full_path
return
os.chdir(self.appnamedir)
# Two cases here: local and remote storage_path.
if self.repo_type == 'remote':
# Remote first.
# Build destination directory
remotepath = self.remote_path.rstrip('/') + self.full_path
try:
if self.verbose:
subprocess.run(
r"rsync -rlIvz --munge-links *.AppImage* " +
f"{self.remote_host}:{remotepath}",
cwd=self.appnamedir, shell=True, check=True
)
else:
subprocess.run(
r"rsync -rlIvz --munge-links *.AppImage* " +
f"{self.remote_host}:{remotepath}",
cwd=self.appnamedir, shell=True, check=True,
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
)
finally:
pass
else:
# Local
# Forcing creation of subfolders, in case there is a new build
os.makedirs(self.full_path, exist_ok = True)
for file in glob.glob("*.AppImage*"):
subprocess.run(shlex.split(
f"cp -f {file} {self.full_path}"
), check=True)
def generalize_and_link(self, chdir = 'default'):
"""Creates the needed generalized files if needed."""
if self.verbose:
print("--- Generalize and Link Phase ---")
# If called with a pointed version, no generalize and link necessary.
if not self.branch_version:
return
# If a prerelease or a daily version, either.
if self.version.query in { 'daily', 'prerelease' }:
return
if chdir == 'default':
chdir = self.full_path
appimagefilename = {}
zsyncfilename = {}
# Creating versions for short version and query text
versions = [ self.short_version, self.branch_version ]
for arch in Build.ARCHSTD:
# If already built, do not do anything.
if self.built[arch]:
continue
os.chdir(chdir)
# if the appimage for the reported arch is not found, skip to next
# arch
if not os.path.exists(self.appimagefilename[arch]):
continue
# Doing it both for short_name and for branchname
for version in versions:
appimagefilename[arch] = self.appname + '-' + version
appimagefilename[arch] += self.languagepart + self.helppart
appimagefilename[arch] += f'-{arch}.AppImage'
zsyncfilename[arch] = appimagefilename[arch] + '.zsync'
# Create the symlink
if self.verbose:
print(f"Creating {appimagefilename[arch]} and checksums.")
if os.path.exists(appimagefilename[arch]):
os.unlink(appimagefilename[arch])
os.symlink(self.appimagefilename[arch], appimagefilename[arch])
# Create the checksum for the AppImage
self.__create_checksum__(appimagefilename[arch])
# Do not continue if no zsync file is provided.
if not self.updatable:
continue
if self.verbose:
print(f"Creating zsync file for version {version}.")
if os.path.exists(zsyncfilename[arch]):
os.unlink(zsyncfilename[arch])
shutil.copyfile(self.zsyncfilename[arch], zsyncfilename[arch])
# Editing the zsyncfile
subprocess.run(shlex.split(
r"sed --in-place 's/^Filename:.*$/Filename: " +
f"{appimagefilename[arch]}/' {zsyncfilename[arch]}"
), check=True)
self.__create_checksum__(zsyncfilename[arch])
def __del__(self): def __del__(self):
"""Destructor""" """Destructor"""

View File

@ -3,6 +3,7 @@
"""Helps with command line commands.""" """Helps with command line commands."""
import os import os
import shutil
import sys import sys
import json import json
import click import click
@ -106,19 +107,19 @@ def batch(yamlfile, verbose):
@cli.command() @cli.command()
@click.option('-a', '--arch', 'arch', default='x86_64', @click.option('-a', '--arch', 'arch', default='x86_64',
type=click.Choice(['x86', 'x86_64', 'all'], case_sensitive=False), help="Build the AppImage for a specific architecture. If there is no specific options, the process will build for both architectures (if available). Default: x86_64") type=click.Choice(['x86', 'x86_64', 'all'], case_sensitive=False), help="Build the AppImage for a specific architecture. Default: x86_64")
@click.option('--check', '-c', is_flag=True, default=False, @click.option('--check', '-c', is_flag=True, default=False, help="Checks in the repository path if the queried version is existent. Default: do not check")
help="Checks in the repository path if the queried version is existent. Default: do not check") @click.option('--checksums', '-e', is_flag=True, default=False, help="Create checksums for each created file (AppImage). Default: do not create checksums.")
@click.option('-d', '--download-path', 'download_path', default = '/var/tmp/downloads', type=str, help="Path to the download folder. Default: /var/tmp/downloads") @click.option('--keep-downloads', '-k', 'keep', is_flag=True, default=False, help="Keep the downloads folder after building the AppImage. Default: do not keep.")
@click.option('-l', '--language', 'language', default = 'basic', type=str, help="Languages to be included. Options: basic, standard, full, a language string (e.g. 'it') or a list of languages comma separated (e.g.: 'en-US,en-GB,it'). Default: basic") @click.option('--languages', '-l', 'language', default = 'basic', type=str, help="Languages to be included. Options: basic, standard, full, a language string (e.g. 'it') or a list of languages comma separated (e.g.: 'en-US,en-GB,it'). Default: basic")
@click.option('-o/-O', '--offline-help/--no-offline-help', 'offline', default = False, help="Include or not the offline help for the chosen languages. Default: no offline help") @click.option('--offline-help', '-o', 'offline', is_flag=True, default = False, help="Include the offline help pages for the chosen languages. Default: no offline help")
@click.option('-p/-P', '--portable/--no-portable', 'portable', default = False, @click.option('--portable', '-p', 'portable', is_flag=True, default = False, help="Create a portable version of the AppImage or not. Default: no portable")
help="Create a portable version of the AppImage or not. Default: no portable") @click.option('--sign', '-s', is_flag=True, default=False, help="Sign the build with your default GPG key. Default: do not sign")
@click.option('-r', '--repo-path', 'repo_path', default = '.', type=str, help="Path to the final storage of the AppImage. Default: current directory") @click.option('--updatable', '-u', is_flag=True, default = False, help="Create an updatable AppImage (compatible with zsync2). Default: not updatable")
@click.option('-s/-S', '--sign/--no-sign', 'sign', default=True, help="Wether to sign the build. Default: sign") @click.option('--download-path', '-d', default = './downloads', type=str, help="Path to the download folder. Default: ./downloads")
@click.option('-u/-U', '--updatable/--no-updatable', 'updatable', default = True, help="Create an updatable version of the AppImage or not. Default: updatable") @click.option('--repo-path', '-r', default = '.', type=str, help="Path to the final storage of the AppImage. Default: current directory")
@click.argument('query') @click.argument('query')
def build(arch, language, offline, portable, updatable, download_path, repo_path, check, sign, query): def build(arch, language, offline, portable, updatable, download_path, repo_path, check, checksums, sign, keep, query):
"""Builds an Appimage with the provided options.""" """Builds an Appimage with the provided options."""
# Multiple query support # Multiple query support
@ -136,22 +137,17 @@ def build(arch, language, offline, portable, updatable, download_path, repo_path
else: else:
arches = [ arch.lower() ] arches = [ arch.lower() ]
for q in queries: for myquery in queries:
collection = loaih.build.Collection(q, arches) for appbuild in loaih.build.Collection(myquery, arches):
for appbuild in collection:
# Configuration phase # Configuration phase
appbuild.tidy_folder = False appbuild.tidy_folder = False
appbuild.language = language appbuild.language = language
appbuild.offline_help = offline appbuild.offline_help = offline
appbuild.portable = portable appbuild.portable = portable
appbuild.updatable = updatable appbuild.updatable = updatable
if repo_path == '.': appbuild.storage_path = os.path.abspath(repo_path)
repo_path = os.getcwd() appbuild.download_path = os.path.abspath(download_path)
appbuild.storage_path = repo_path appbuild.sign = sign
appbuild.download_path = download_path
if sign:
appbuild.sign = True
# Running phase # Running phase
appbuild.calculate() appbuild.calculate()
@ -160,8 +156,11 @@ def build(arch, language, offline, portable, updatable, download_path, repo_path
appbuild.check() appbuild.check()
appbuild.download() appbuild.download()
appbuild.appbuild() appbuild.build()
appbuild.checksums() if checksums:
appbuild.checksums()
appbuild.publish() appbuild.publish()
appbuild.generalize_and_link() if not keep:
shutil.rmtree(appbuild.download_path)
del appbuild del appbuild