2022-03-20 23:57:45 +01:00
#!/usr/bin/env python3
import urllib . request
import loaih . versions as versions
from lxml import etree
2022-03-30 00:07:43 +02:00
import tempfile , os , sys , glob , subprocess , shutil , re
2022-03-20 23:57:45 +01:00
class Build ( object ) :
LANGSTD = [ ' ar ' , ' de ' , ' en-GB ' , ' es ' , ' fr ' , ' it ' , ' ja ' , ' ko ' , ' pt ' , ' pt-BR ' , ' ru ' , ' zh-CN ' , ' zh-TW ' ]
LANGBASIC = [ ' en-GB ' ]
2022-03-31 01:19:51 +02:00
ARCHSTD = [ u ' x86 ' , u ' x86_64 ' ]
2022-03-20 23:57:45 +01:00
def __init__ ( self , query , arch ) :
""" Build all versions that can be found in the indicated repo. """
self . query = query
2022-03-26 01:32:15 +01:00
self . queried_name = False if ' . ' in self . query else True
2022-03-20 23:57:45 +01:00
self . arch = arch
2022-03-21 01:10:17 +01:00
self . url = { }
2022-03-20 23:57:45 +01:00
self . language = ' basic '
self . offline_help = False
self . portable = False
self . updatable = True
self . storage_path = ' /srv/http/appimage.sys42.eu '
self . download_path = ' /var/tmp/downloads '
2022-03-26 02:10:25 +01:00
self . appversion = ' '
2022-03-21 00:18:09 +01:00
self . appimagefilename = { }
2022-03-26 02:32:02 +01:00
self . genappversion = ' '
self . genappimagefilename = { }
2022-03-20 23:57:45 +01:00
# Getting versions and so on
v = versions . BuildVersion ( self . query )
2022-03-21 01:10:17 +01:00
# Creating a tempfile
self . builddir = tempfile . mkdtemp ( )
self . tarballs = { }
2022-03-22 01:24:55 +01:00
self . appname = ' LibreOffice ' if not self . query == ' daily ' and not self . query == ' prerelease ' else ' LibreOfficeDev '
2022-03-21 01:10:17 +01:00
self . version = v . version
self . url = v . basedirurl
2022-03-31 01:19:51 +02:00
self . built = { u ' x86 ' : False , u ' x86_64 ' : False }
2022-03-30 00:07:43 +02:00
# Preparing the default for the relative path on the storage for
# different versions.
# The path will evaluated as part of the check() function, as it is
# understood the storage_path can be changed before that phase.
self . relative_path = [ ]
self . full_path = ' '
2022-03-21 01:10:17 +01:00
2022-03-26 02:54:06 +01:00
def check ( self , storage_path ) :
""" Checking if the requested AppImage has been already built. """
2022-03-30 00:07:43 +02:00
# Mandate to the private function to calculate the full_path available
# for the storage and the checks.
self . __calculate_full_path__ ( )
2022-03-26 02:54:06 +01:00
# Incompatibilities - if portable and updatable are asked together,
# only portable will be built.
if self . portable and self . updatable :
print ( " Upgradable and portable options were required together. Building only portable. " )
self . updatable = False
if self . updatable and not self . queried_name :
# If the queried version was a numbered version, doesn't make sense
# to build an updatable version.
self . updatable = False
2022-03-20 23:57:45 +01:00
# Building expected AppImageName
2022-03-22 01:24:55 +01:00
self . languagepart = " . "
2022-03-20 23:57:45 +01:00
if ' , ' in self . language :
2022-03-22 01:24:55 +01:00
self . languagepart + = self . language . replace ( ' , ' , ' - ' )
2022-03-21 00:48:42 +01:00
else :
2022-03-22 01:24:55 +01:00
self . languagepart + = self . language
2022-03-20 23:57:45 +01:00
2022-03-26 01:32:15 +01:00
self . helppart = ' .help ' if self . offline_help else ' '
2022-03-26 02:04:02 +01:00
# If the build was called by queried name, build from latest release available but build with the most generic name
2022-03-26 02:32:02 +01:00
myver = str . join ( ' . ' , self . version . split ( ' . ' ) [ 0 : 2 ] )
self . genappversion = myver + self . languagepart + self . helppart
self . appversion = self . version + self . languagepart + self . helppart
2022-03-31 01:19:51 +02:00
for arch in ARCHSTD :
self . genappimagefilename [ arch ] = self . appname + ' - ' + self . genappversion + f ' - { arch } .AppImage '
self . appimagefilename [ arch ] = self . appname + ' - ' + self . appversion + f ' - { arch } .AppImage '
2022-03-20 23:57:45 +01:00
for arch in self . arch :
2022-03-26 02:32:02 +01:00
# For generalized builds, we need to check if there are .ver file
# and it contains the specific version found.
2022-03-26 02:45:55 +01:00
print ( " Debug: searching for {file} " . format ( file = self . genappimagefilename [ arch ] + ' .ver ' ) )
2022-03-26 02:42:53 +01:00
res = subprocess . run ( " find {path} -name {appimage} ' " . format (
2022-03-30 00:07:43 +02:00
path = self . full_path ,
2022-03-26 02:32:02 +01:00
appimage = self . genappimagefilename [ arch ] + ' .ver '
2022-03-30 00:07:43 +02:00
) , shell = True , capture_output = True , env = { " LC_ALL " : " C " } )
if " No such file or directory " in res . stderr . decode ( ' utf-8 ' ) :
# Folder is not existent: so the version was not built
# Build stays false, and we go to the next arch
continue
2022-03-26 02:42:53 +01:00
if res . stdout :
# All good, the command was executed fine.
for file in res . stdout . decode ( ' utf-8 ' ) . strip ( ' \n ' ) . split ( ' \n ' ) :
if self . version in open ( file , ' r ' ) . read ( ) :
2022-03-30 00:07:43 +02:00
self . built [ arch ] = True
2022-03-26 02:42:53 +01:00
2022-03-26 02:45:01 +01:00
print ( " Debug: searching for {file} " . format ( file = self . appimagefilename [ arch ] ) )
2022-03-26 02:42:53 +01:00
res = subprocess . run ( " find {path} -name ' {appimage} ' " . format (
2022-03-30 00:07:43 +02:00
path = self . full_path ,
2022-03-26 01:32:15 +01:00
appimage = self . appimagefilename [ arch ]
2022-03-26 02:42:53 +01:00
) , shell = True , capture_output = True )
if res . stdout :
if len ( res . stdout . decode ( ' utf-8 ' ) . strip ( ' \n ' ) ) > 1 :
2022-03-30 00:07:43 +02:00
self . built [ arch ] = True
if self . built [ arch ] :
print ( " The requested AppImage already exists on storage for {arch} . I ' ll skip downloading, building and moving the results. " . format ( arch = arch ) )
def __calculate_full_path__ ( self ) :
""" Calculate relative path of the build, based on internal other variables. """
if len ( self . relative_path ) == 0 :
if self . query == ' daily ' :
self . relative_path . append ( ' daily ' )
elif self . query == ' prerelease ' :
self . relative_path . append ( ' prerelease ' )
2022-03-20 23:57:45 +01:00
2022-03-30 00:07:43 +02:00
# Not the same check, an additional one
if self . portable :
self . relative_path . append ( ' portable ' )
fullpath_arr = self . storage_path . split ( ' / ' )
# Joining relative path only if it is not null
if len ( self . relative_path ) > 0 :
2022-03-30 00:12:15 +02:00
fullpath_arr . extend ( self . relative_path )
2022-03-30 00:07:43 +02:00
self . full_path = re . sub ( r " /+ " , ' / ' , str . join ( ' / ' , fullpath_arr ) )
2022-03-20 23:57:45 +01:00
def download ( self , download_path ) :
""" Downloads the contents of the URL as it was a folder. """
# Let's start with defining which files are to be downloaded.
# Let's explore the remote folder.
self . download_path = download_path
2022-03-21 01:10:17 +01:00
for arch in self . arch :
# Checking if a valid path has been provided
if self . url [ arch ] == ' - ' :
print ( " No build has been provided for the requested AppImage for {arch} . Continue with other options. " . format ( arch = arch ) )
2022-03-30 00:07:43 +02:00
# Faking already built it so to skip other checks.
self . built [ arch ] = True
continue
if self . built [ arch ] :
print ( " A build for {arch} was already found. Skipping specific packages. " . format ( arch = arch ) )
2022-03-21 01:10:17 +01:00
continue
contents = etree . HTML ( urllib . request . urlopen ( self . url [ arch ] ) . read ( ) ) . xpath ( " //td/a " )
2022-03-26 01:35:06 +01:00
self . tarballs [ arch ] = [ x . text for x in contents if x . text . endswith ( ' tar.gz ' ) and ' deb ' in x . text ]
2022-03-21 01:10:17 +01:00
tarballs = self . tarballs [ arch ]
maintarball = tarballs [ 0 ]
2022-03-21 01:11:17 +01:00
os . makedirs ( self . download_path , exist_ok = True )
2022-03-21 01:10:17 +01:00
os . chdir ( self . download_path )
for archive in tarballs :
2022-03-20 23:57:45 +01:00
# If the archive is already there, do not do anything.
2022-03-21 01:10:17 +01:00
if os . path . exists ( os . path . join ( self . download_path , archive ) ) :
2022-03-20 23:57:45 +01:00
print ( " Archive %s is already there! Sweet " % archive )
continue
# Download the archive
try :
2022-03-21 01:10:17 +01:00
urllib . request . urlretrieve ( self . url [ arch ] + archive , archive )
2022-03-20 23:57:45 +01:00
except :
print ( " Failed to download {archive} . " . format ( archive = archive ) )
print ( " Got %s . " % archive )
def build ( self ) :
""" Building all the versions. """
# We have 4 builds to do:
# * standard languages, no help
# * standard languages + offline help
# * all languages, no help
# * all languages + offline help
for arch in self . arch :
2022-03-30 00:07:43 +02:00
if self . built [ arch ] :
# Already built for arch or path not available. User has already been warned.
2022-03-21 01:10:17 +01:00
continue
2022-03-20 23:57:45 +01:00
# Preparation tasks
self . appnamedir = os . path . join ( self . builddir , self . appname )
self . appimagedir = os . path . join ( self . builddir , self . appname , self . appname + ' .AppDir ' )
os . makedirs ( self . appimagedir , exist_ok = True )
# And then cd to the appname folder.
os . chdir ( self . appnamedir )
# Download appimagetool from github
appimagetoolurl = " https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool- {arch} .AppImage " . format ( arch = arch )
urllib . request . urlretrieve ( appimagetoolurl , ' appimagetool ' )
os . chmod ( ' appimagetool ' , 0o755 )
# Build the requested version.
2022-03-30 02:06:24 +02:00
if self . queried_name and not self . portable :
2022-03-26 02:32:02 +01:00
self . __unpackbuild__ ( arch , True )
2022-03-21 01:10:17 +01:00
self . __unpackbuild__ ( arch )
2022-03-20 23:57:45 +01:00
2022-03-26 02:32:02 +01:00
def __unpackbuild__ ( self , arch , generalize = False ) :
2022-03-30 01:06:38 +02:00
if generalize and self . portable :
# Doesn't particularly make sense to build a generic portable
# version. Just skipping the specific generic build
return
2022-03-20 23:57:45 +01:00
# We start by filtering out tarballs from the list
2022-03-21 01:10:17 +01:00
buildtarballs = [ self . tarballs [ arch ] [ 0 ] ]
2022-03-20 23:57:45 +01:00
# Let's process standard languages and append results to the
# buildtarball
if self . language == ' basic ' :
if self . offline_help :
2022-03-30 00:25:42 +02:00
buildtarballs . extend ( [ x for x in self . tarballs [ arch ] if ' pack_en-GB ' in x ] )
else :
buildtarballs . extend ( [ x for x in self . tarballs [ arch ] if ' langpack_en-GB ' in x ] )
elif self . language == ' standard ' :
2022-03-20 23:57:45 +01:00
for lang in Build . LANGSTD :
2022-03-21 01:14:28 +01:00
if self . offline_help :
2022-03-30 01:10:36 +02:00
buildtarballs . extend ( [ x for x in self . tarballs [ arch ] if ( ' pack_ ' + lang ) in x ] )
2022-03-30 00:25:42 +02:00
else :
2022-03-30 01:10:36 +02:00
buildtarballs . extend ( [ x for x in self . tarballs [ arch ] if ( ' langpack_ ' + lang ) in x ] )
2022-03-30 00:42:31 +02:00
elif self . language == ' full ' :
if self . offline_help :
2022-03-20 23:57:45 +01:00
# We need also all help. Let's replace buildtarball with the
# whole bunch
2022-03-21 01:10:17 +01:00
buildtarballs = self . tarballs [ arch ]
2022-03-30 00:42:31 +02:00
else :
buildtarballs . extend ( [ x for x in self . tarballs [ arch ] if ' langpack ' in x ] )
else :
# Looping for each language in self.language
for lang in self . language . split ( " , " ) :
if self . offline_help :
buildtarballs . extend ( [ x for x in self . tarballs [ arch ] if ( ' pack ' + lang ) in x ] )
else :
buildtarballs . extend ( [ x for x in self . tarballs [ arch ] if ( ' langpack ' + lang ) in x ] )
2022-03-20 23:57:45 +01:00
# Unpacking the tarballs
for archive in buildtarballs :
subprocess . run ( " tar xzf {folder} / {archive} " . format ( folder = self . download_path , archive = archive ) , shell = True )
os . chdir ( self . appnamedir )
2022-03-26 01:32:15 +01:00
2022-03-20 23:57:45 +01:00
os . makedirs ( self . appimagedir , exist_ok = True )
# At this point, let's decompress the deb packages
subprocess . run ( " find .. -iname ' *.deb ' -exec dpkg -x {} . \ ; " , shell = True , cwd = self . appimagedir )
2022-03-26 01:32:15 +01:00
2022-03-21 01:29:27 +01:00
if self . portable :
2022-03-21 01:37:03 +01:00
shortversion = str . join ( ' . ' , self . version . split ( ' . ' ) [ : 3 ] )
2022-03-21 01:39:56 +01:00
subprocess . run ( " find . -type f -iname ' bootstraprc ' -exec sed -i ' s|^UserInstallation=.*|UserInstallation= \ $SYSUSERCONFIG/libreoffice/ %s |g ' {} \ + " % shortversion , shell = True , cwd = self . appimagedir )
2022-03-21 01:29:27 +01:00
2022-03-20 23:57:45 +01:00
# Changing desktop file
subprocess . run ( " find . -iname startcenter.desktop -exec cp {} . \ ; " , shell = True , cwd = self . appimagedir )
2022-03-21 01:18:37 +01:00
subprocess . run ( " sed -i -e ' s:^Name=.*$:Name= %s : ' startcenter.desktop " % self . appname , shell = True , cwd = self . appimagedir )
2022-03-20 23:57:45 +01:00
subprocess . run ( " find . -name ' *startcenter.png ' -path ' *hicolor*48x48* ' -exec cp {} . \ ; " , shell = True , cwd = self . appimagedir )
# Find the name of the binary called in the desktop file.
binaryname = subprocess . check_output ( " awk ' BEGIN { FS = \" = \" } /^Exec/ { print $2; exit } ' startcenter.desktop | awk ' { print $1 } ' " , shell = True , cwd = self . appimagedir ) . decode ( ' utf-8 ' ) . strip ( ' \n ' )
bindir = os . path . join ( self . appimagedir , ' usr ' , ' bin ' )
os . makedirs ( bindir , exist_ok = True )
subprocess . run ( " find ../../opt -iname soffice -path ' *program* ' -exec ln -sf {} ./ %s \ ; " % binaryname , shell = True , cwd = bindir )
# Download AppRun from github
apprunurl = " https://github.com/AppImage/AppImageKit/releases/download/continuous/AppRun- {arch} " . format ( arch = arch )
dest = os . path . join ( self . appimagedir , ' AppRun ' )
urllib . request . urlretrieve ( apprunurl , dest )
os . chmod ( dest , 0o755 )
# Building app
2022-03-26 02:32:02 +01:00
if self . updatable :
2022-03-26 01:32:15 +01:00
# Updatable make sense only for generic images for fresh, still,
# daily. If a request was for a specific version, I'd not build an
# updatable version.
# zsync name was generated already
2022-03-26 02:32:02 +01:00
# If asked to do a generalized build:
if generalize :
subprocess . run ( " VERSION= {version} ./appimagetool -u ' zsync| {zsync} ' -v ./ {appname} .AppDir/ " . format ( version = self . genappversion , zsync = self . genappimagefilename [ arch ] + ' .zsync ' , appname = self . appname ) , shell = True )
# Build version file management
2022-03-30 00:42:31 +02:00
with open ( self . genappimagefilename [ arch ] + ' .ver ' , ' w ' ) as v :
2022-03-26 02:32:02 +01:00
v . write ( self . version )
else :
subprocess . run ( " VERSION= {version} ./appimagetool -u ' zsync| {zsync} ' -v ./ {appname} .AppDir/ " . format ( version = self . appversion , zsync = self . appimagefilename [ arch ] + ' .zsync ' , appname = self . appname ) , shell = True )
2022-03-20 23:57:45 +01:00
else :
2022-03-26 02:32:02 +01:00
if generalize :
subprocess . run ( " VERSION= {version} ./appimagetool -v ./ {appname} .AppDir/ " . format ( version = self . genappversion , appname = self . appname ) , shell = True )
2022-03-30 00:42:31 +02:00
with open ( self . genappimagefilename [ arch ] + ' .ver ' , ' w ' ) as v :
2022-03-26 02:32:02 +01:00
v . write ( self . version )
else :
subprocess . run ( " VERSION= {version} ./appimagetool -v ./ {appname} .AppDir/ " . format ( version = self . appversion , appname = self . appname ) , shell = True )
2022-03-20 23:57:45 +01:00
2022-03-26 02:10:25 +01:00
print ( " Built AppImage version {version} " . format ( version = self . appversion ) )
2022-03-20 23:57:45 +01:00
# Cleanup phase, before new run.
for deb in glob . glob ( self . appnamedir + ' /*.deb ' ) :
os . remove ( deb )
2022-03-30 00:25:42 +02:00
subprocess . run ( " find . -mindepth 1 -maxdepth 1 -type d -exec rm -rf {} \ + " , shell = True )
2022-03-20 23:57:45 +01:00
def checksums ( self ) :
""" Create checksums of the built versions. """
2022-03-30 00:07:43 +02:00
if all ( self . built . values ( ) ) :
# All checksums are already created.
2022-03-20 23:57:45 +01:00
return
2022-03-30 00:07:43 +02:00
# On the contrary, checksums will be in any case overwritten if
# existent, but generated only for built packages anyways
2022-03-20 23:57:45 +01:00
os . chdir ( self . appnamedir )
for appimage in glob . glob ( ' *.AppImage* ' ) :
2022-03-30 01:06:38 +02:00
if appimage . endswith ( ' .ver ' ) :
# Skipping checksums for .ver files.
continue
2022-03-20 23:57:45 +01:00
# See if a checksum already exist
if not os . path . exists ( appimage + ' .md5 ' ) :
subprocess . run ( " md5sum {appimage} > {appimage} .md5 " . format ( appimage = appimage ) , shell = True )
2022-03-30 00:07:43 +02:00
def publish ( self ) :
2022-03-20 23:57:45 +01:00
""" Moves built versions to definitive storage. """
2022-03-30 00:07:43 +02:00
if all ( self . built . values ( ) ) :
# All files are already present in the full_path
2022-03-20 23:57:45 +01:00
return
os . chdir ( self . appnamedir )
2022-03-30 00:07:43 +02:00
# Forcing creation of subfolders, in case there is a new build
os . makedirs ( self . full_path , exist_ok = True )
subprocess . run ( " find . -iname ' *.AppImage* ' -exec cp -f {} %s \ ; " % self . full_path , shell = True )
2022-03-20 23:57:45 +01:00
def __del__ ( self ) :
""" Destructor """
# Cleaning up build directory
shutil . rmtree ( self . builddir )