Electrify/command_line.py

1345 lines
52 KiB
Python

'''Command line module for electrify.'''
import ssl
try:
ssl._create_default_https_context = ssl._create_unverified_context
except AttributeError:
pass
from utils import zip_files, join_files, log, get_temp_dir
from pycns import save_icns
from pepy.pe import PEFile
import argparse
import urllib.request as request
import platform
import re
import time
import sys
import os
import glob
import json
import shutil
import stat
import tarfile
import zipfile
import traceback
import subprocess
import logging
import logging.handlers as lh
import plistlib
import codecs
import requests
from pprint import pprint
from utils import get_data_path, get_data_file_path
import utils
from semantic_version import Version
from zipfile import ZipFile
from tarfile import TarFile
from io import StringIO
from configobj import ConfigObj
COMMAND_LINE = True
inside_packed_exe = getattr(sys, 'frozen', '')
if inside_packed_exe:
# we are running in a |PyInstaller| bundle
CWD = os.path.dirname(sys.executable)
else:
# we are running in a normal Python environment
CWD = os.getcwd()
def get_file(path):
parts = path.split('/')
independent_path = utils.path_join(CWD, *parts)
return independent_path
def is_installed():
uninst = get_file('uninst.exe')
return utils.is_windows() and os.path.exists(uninst)
__version__ = "v0.0.0"
with open(get_file('files/version.txt')) as f:
__version__ = f.read().strip()
TEMP_DIR = get_temp_dir()
DEFAULT_DOWNLOAD_PATH = get_data_path('files/downloads')
logger = logging.getLogger('W2E logger')
LOG_FILENAME = get_data_file_path('files/error.log')
if __name__ != '__main__':
logging.basicConfig(
filename=LOG_FILENAME,
format=("%(levelname) -10s %(asctime)s %(module)s.py: "
"%(lineno)s %(funcName)s - %(message)s"),
level=logging.DEBUG
)
logger = logging.getLogger('W2E logger')
handler = lh.RotatingFileHandler(LOG_FILENAME, maxBytes=100000, backupCount=2)
logger.addHandler(handler)
def my_excepthook(type_, value, tback):
output_err = u''.join([x for x in traceback.format_exception(type_, value, tback)])
logger.error(u'{}'.format(output_err))
sys.__excepthook__(type_, value, tback)
sys.excepthook = my_excepthook
try:
os.makedirs(DEFAULT_DOWNLOAD_PATH)
except:
pass
class Setting(object):
def __init__(self, name='', display_name=None, value=None,
required=False, type=None, file_types=None, *args, **kwargs):
self.name = name
self.display_name = (display_name
if display_name
else name.replace('_', ' ').capitalize())
self.value = value
self.last_value = None
self.required = required
self.type = type
self.copy = kwargs.pop('copy', True)
self.file_types = file_types
self.scope = kwargs.pop('scope', 'local')
self.default_value = kwargs.pop('default_value', None)
self.label_suffix = kwargs.pop('label_suffix', '')
self.button = kwargs.pop('button', None)
self.button_callback = kwargs.pop('button_callback', None)
self.description = kwargs.pop('description', u'')
self.values = kwargs.pop('values', [])
self.exists = kwargs.pop('exists', True)
self.filter = kwargs.pop('filter', '.*')
self.factor = kwargs.pop('factor', 1)
self.filter_action = kwargs.pop('filter_action', 'None')
self.check_action = kwargs.pop('check_action', 'None')
self.action = kwargs.pop('action', None)
convert = kwargs.pop('convert', lambda x: x)
if not callable(convert):
convert = eval(convert)
def conv(x):
if x is None or x == '':
return x
try:
x = convert(x)
except ValueError:
x = convert(float(x))
if isinstance(x, (int, float)) and not isinstance(x, bool):
x = x/self.factor
return convert(x)
self.convert = conv
self.set_extra_attributes_from_keyword_args(**kwargs)
if self.value is None:
self.value = self.default_value
if self.value:
self.value = self.convert(self.value)
self.save_path = kwargs.pop('save_path', u'')
self.get_file_information_from_url()
def filter_name(self, text):
if hasattr(self.filter_action, text):
action = getattr(self.filter_action, text)
return action(text)
return text
def get_file_information_from_url(self):
if hasattr(self, 'url'):
self.file_name = self.url.split(u'/')[-1]
self.full_file_path = utils.path_join(self.save_path, self.file_name)
self.file_ext = os.path.splitext(self.file_name)[1]
if self.file_ext == '.zip':
self.extract_class = ZipFile
self.extract_args = ()
elif self.file_ext == '.gz':
self.extract_class = TarFile.open
self.extract_args = ('r:gz',)
def save_file_path(self, version, location=None):
if location:
self.save_path = location
else:
self.save_path = self.save_path or DEFAULT_DOWNLOAD_PATH
self.get_file_information_from_url()
if self.full_file_path:
path = self.full_file_path.format(version)
versions = re.findall('(\d+)\.(\d+)\.(\d+)', version)[0]
minor = int(versions[1])
return path
return ''
def set_extra_attributes_from_keyword_args(self, **kwargs):
for undefined_key, undefined_value in kwargs.items():
setattr(self, undefined_key, undefined_value)
def extract(self, ex_path, version):
if os.path.exists(ex_path):
utils.rmtree(ex_path, ignore_errors=True)
path = self.save_file_path(version)
file = self.extract_class(path,
*self.extract_args)
# currently, python's extracting mechanism for zipfile doesn't
# copy file permissions, resulting in a binary that
# that doesn't work. Copied from a patch here:
# http://bugs.python.org/file34873/issue15795_cleaned.patch
if path.endswith('.zip'):
members = file.namelist()
for zipinfo in members:
minfo = file.getinfo(zipinfo)
target = file.extract(zipinfo, ex_path)
mode = minfo.external_attr >> 16 & 0x1FF
os.chmod(target, mode)
else:
file.extractall(ex_path)
if path.endswith('.tar.gz'):
dir_name = utils.path_join(ex_path, os.path.basename(path).replace('.tar.gz',''))
else:
dir_name = utils.path_join(ex_path, os.path.basename(path).replace('.zip',''))
if os.path.exists(dir_name):
for p in os.listdir(dir_name):
abs_file = utils.path_join(dir_name, p)
utils.move(abs_file, ex_path)
utils.rmtree(dir_name, ignore_errors=True)
def __repr__(self):
url = ''
if hasattr(self, 'url'):
url = self.url
return (u'Setting: (name={}, '
u'display_name={}, '
u'value={}, required={}, '
u'type={}, url={})').format(self.name,
self.display_name,
self.value,
self.required,
self.type,
url)
class CommandBase(object):
def __init__(self):
self.quiet = False
self.logger = None
self.output_package_json = True
self.settings = self.get_settings()
self.js_cmd_args = ''
self.js_window_init = ''
self._project_dir = ''
self._output_dir = ''
self._progress_text = ''
self._output_err = ''
self._extract_error = ''
self._project_name = None
self.original_packagejson = {}
def init(self):
self.logger = logging.getLogger('CMD logger')
self.update_electron_versions(None)
self.setup_electron_versions()
def update_electron_versions(self, button):
self.progress_text = 'Updating electron versions...'
self.get_versions()
self.progress_text = '\nDone.\n'
def setup_electron_versions(self):
electron_version = self.get_setting('electron_version')
electron_version.values = []
try:
f = codecs.open(get_data_file_path('files/electron-versions.txt'), encoding='utf-8')
for line in f:
electron_version.values.append(line.strip())
f.close()
except IOError:
electron_version.values.append(electron_version.default_value)
def get_electron_versions(self):
electron_version = self.get_setting('electron_version')
return electron_version.values[:]
def get_settings(self):
config_file = get_file('files/settings.cfg')
contents = codecs.open(config_file, encoding='utf-8').read()
contents = contents.replace(u'{DEFAULT_DOWNLOAD_PATH}',
DEFAULT_DOWNLOAD_PATH)
config_io = StringIO(contents)
config = ConfigObj(config_io, unrepr=True).dict()
settings = {'setting_groups': []}
setting_items = (list(config['setting_groups'].items()) +
[('export_settings', config['export_settings'])] +
[('compression', config['compression'])])
for setting_group, setting_group_dict in setting_items:
settings[setting_group] = {}
for setting_name, setting_dict in setting_group_dict.items():
for key, val in setting_dict.items():
if '_callback' in key:
setting_dict[key] = getattr(self, setting_dict[key])
setting_obj = Setting(name=setting_name, **setting_dict)
settings[setting_group][setting_name] = setting_obj
sgroup_items = config['setting_groups'].items()
for setting_group, setting_group_dict in sgroup_items:
settings['setting_groups'].append(settings[setting_group])
self._setting_items = (list(config['setting_groups'].items()) +
[('export_settings', config['export_settings'])] +
[('compression', config['compression'])])
config.pop('setting_groups')
config.pop('export_settings')
config.pop('compression')
self._setting_items += config.items()
for key, val in config.items():
settings[key] = val
return settings
def project_dir(self):
return self._project_dir
def output_dir(self):
return self._output_dir
def project_name(self):
return (self._project_name or
os.path.basename(os.path.abspath(self.project_dir())))
def get_setting(self, name):
for setting_group in (self.settings['setting_groups'] +
[self.settings['export_settings']] +
[self.settings['compression']]):
if name in setting_group:
setting = setting_group[name]
return setting
def get_settings_type(self, type):
settings = []
for setting_group in (self.settings['setting_groups'] +
[self.settings['export_settings']] +
[self.settings['compression']]):
for name, setting in setting_group.items():
if setting.type == type:
settings.append(setting)
return settings
def show_error(self, error):
if self.logger is not None:
self.logger.error(error)
def enable_ui_after_error(self):
pass
def get_versions(self):
if self.logger is not None:
self.logger.info('Getting versions...')
union_versions = set()
url = self.settings['version_info']['electron_url']
req = requests.get(url)
data = json.loads(req.text)
versions = [d['name'].split(' ')[-1][1:] for d in data]
electron_version = self.get_setting('electron_version')
old_versions = set(electron_version.values)
old_versions = old_versions.union(union_versions)
new_versions = set(versions)
union_versions = old_versions.union(new_versions)
versions = sorted(union_versions,
key=Version, reverse=True)
electron_version.values = versions
f = None
try:
f = codecs.open(get_data_file_path('files/electron-versions.txt'), 'w', encoding='utf-8')
for v in electron_version.values:
f.write(v+os.linesep)
f.close()
except IOError:
error = u''.join([x for x in traceback.format_exception(sys.exc_info()[0],
sys.exc_info()[1],
sys.exc_info()[2])])
self.show_error(error)
self.enable_ui_after_error()
finally:
if f:
f.close()
def download_file_with_error_handling(self):
setting = self.files_to_download.pop()
location = self.get_setting('download_dir').value
version = self.selected_version()
path = setting.url.format(version, version)
versions = re.findall('(\d+)\.(\d+)\.(\d+)', version)[0]
try:
return self.download_file(setting.url.format(version, version),
setting)
except (Exception, KeyboardInterrupt):
if os.path.exists(setting.save_file_path(version, location)):
os.remove(setting.save_file_path(version, location))
error = u''.join([x for x in traceback.format_exception(sys.exc_info()[0],
sys.exc_info()[1],
sys.exc_info()[2])])
self.show_error(error)
self.enable_ui_after_error()
def load_package_json(self, json_path=None):
self.logger.info('Loading package.json')
if json_path is not None:
p_json = [json_path]
else:
p_json = glob.glob(utils.path_join(self.project_dir(),
'package.json'))
setting_list = []
if p_json:
json_str = ''
try:
with codecs.open(p_json[0], 'r', encoding='utf-8') as f:
json_str = f.read()
except IOError:
return setting_list
try:
setting_list = self.load_from_json(json_str)
except ValueError as e: # Json file is invalid
self.logger.warning('Warning: Json file invalid.')
self.progress_text = u'{}\n'.format(e)
return setting_list
def generate_json(self, global_json=False):
self.logger.info('Generating package.json...')
dic = {'webexe_settings': {},
'web_preferences': {}}
if not global_json:
dic.update({'webkit': {}, 'window': {}, 'web': {}})
dic.update(self.original_packagejson)
for setting_name, setting in self.settings['app_settings'].items():
if setting.value is not None:
dic[setting_name] = setting.value
if setting_name == 'keywords':
dic[setting_name] = re.findall('\w+', setting.value)
for setting_name, setting in self.settings['window_settings'].items():
if setting.value is not None:
dic['window'][setting_name] = setting.value
for setting_name, setting in self.settings['webkit_settings'].items():
if setting.value is not None:
dic['webkit'][setting_name] = setting.value
for setting_name, setting in self.settings['web_preferences'].items():
if setting.value is not None:
dic['web'][setting_name] = setting.value
if not global_json:
dl_export_items = (list(self.settings['download_settings'].items()) +
list(self.settings['export_settings'].items()) +
list(self.settings['compression'].items()) +
list(self.settings['electrify_settings'].items()))
else:
dl_export_items = (list(self.settings['download_settings'].items()) +
list(self.settings['export_settings'].items()) +
list(self.settings['compression'].items()))
for setting_name, setting in dl_export_items:
if setting.value is not None:
dic['webexe_settings'][setting_name] = setting.value
dic['main'] = 'main.js'
js_settings = {'webPreferences': {}}
for setting_name, setting in self.settings['window_settings'].items():
js_settings[utils.to_camel_case(setting_name)] = setting.convert(setting.value)
for setting_name, setting in self.settings['web_preferences'].items():
js_settings['webPreferences'][utils.to_camel_case(setting_name)] = setting.convert(setting.value)
ignored_app_settings = set(['main_html', 'app_name', 'description', 'version', 'user_agent'])
self.js_cmd_args = ''
for setting_name, setting in self.settings['app_settings'].items():
if setting_name not in ignored_app_settings:
if setting.value:
js_name = setting_name.replace('_', '-')
if setting.type == 'check':
self.js_cmd_args += 'app.commandLine.appendSwitch("'+js_name+'");\n'
else:
value = setting.value
self.js_cmd_args += 'app.commandLine.appendSwitch("'+js_name+'", "'+value+'");\n'
self.js_window_init = 'var mainWindow = new BrowserWindow('+json.dumps(js_settings, indent=4)+');'
s = json.dumps(dic, indent=4)
return s
@property
def extract_error(self):
return self._extract_error
@extract_error.setter
def extract_error(self, value):
if value is not None and not self.quiet and COMMAND_LINE:
self._extract_error = value
sys.stderr.write(u'\r{}'.format(self._extract_error))
sys.stderr.flush()
@property
def output_err(self):
return self._output_err
@output_err.setter
def output_err(self, value):
if value is not None and not self.quiet and COMMAND_LINE:
self._output_err = value
sys.stderr.write(u'\r{}'.format(self._output_err))
sys.stderr.flush()
@property
def progress_text(self):
return self._progress_text
@progress_text.setter
def progress_text(self, value):
if value is not None and not self.quiet and COMMAND_LINE:
self._progress_text = value
sys.stdout.write(u'\r{}'.format(self._progress_text))
sys.stdout.flush()
def load_from_json(self, json_str):
dic = json.loads(json_str)
self.original_packagejson.update(dic)
setting_list = []
stack = [('root', dic)]
while stack:
parent, new_dic = stack.pop()
for item in new_dic:
setting = self.get_setting(item)
if setting:
setting_list.append(setting)
if (setting.type == 'file' or
setting.type == 'string' or
setting.type == 'folder'):
val_str = self.convert_val_to_str(new_dic[item])
setting.value = setting.convert(val_str)
if setting.type == 'strings':
strs = self.convert_val_to_str(new_dic[item]).split(',')
setting.value = strs
if setting.type == 'check':
setting.value = new_dic[item]
if setting.type == 'list':
val_str = self.convert_val_to_str(new_dic[item])
setting.value = val_str
if setting.type == 'range':
setting.value = new_dic[item]
if setting.type == 'color':
setting.value = new_dic[item]
if isinstance(new_dic[item], dict):
stack.append((item, new_dic[item]))
return setting_list
def selected_version(self):
return self.get_setting('electron_version').value
def extract_files(self):
self.extract_error = None
location = self.get_setting('download_dir').value
version = self.selected_version()
for setting_name, setting in self.settings['export_settings'].items():
save_file_path = setting.save_file_path(version,
location)
try:
if setting.value:
extract_path = get_data_path('files/'+setting.name)
setting.extract(extract_path, version)
self.progress_text += '.'
except (tarfile.ReadError, zipfile.BadZipfile) as e:
if os.path.exists(save_file_path):
os.remove(save_file_path)
self.extract_error = e
self.logger.error(self.extract_error)
# cannot use GUI in thread to notify user. Save it for later
self.progress_text = '\nDone.\n'
return True
def create_icns_for_app(self, icns_path):
icon_setting = self.get_setting('icon')
mac_app_icon_setting = self.get_setting('mac_icon')
icon_path = (mac_app_icon_setting.value
if mac_app_icon_setting.value
else icon_setting.value)
if icon_path:
icon_path = utils.path_join(self.project_dir(), icon_path)
if not icon_path.endswith('.icns'):
save_icns(icon_path, icns_path)
else:
utils.copy(icon_path, icns_path)
def replace_icon_in_exe(self, exe_path):
icon_setting = self.get_setting('icon')
exe_icon_setting = self.get_setting('exe_icon')
icon_path = (exe_icon_setting.value
if exe_icon_setting.value
else icon_setting.value)
if icon_path:
p = PEFile(exe_path)
p.replace_icon(utils.path_join(self.project_dir(), icon_path))
p.write(exe_path)
p = None
def make_output_dirs(self):
self.output_err = ''
try:
self.progress_text = 'Removing old output directory...\n'
output_dir = utils.path_join(self.output_dir(), self.project_name())
if os.path.exists(output_dir):
utils.rmtree(output_dir, onerror=self.remove_readonly)
temp_dir = utils.path_join(TEMP_DIR, 'webexectemp')
if os.path.exists(temp_dir):
utils.rmtree(temp_dir, onerror=self.remove_readonly)
self.progress_text = 'Making new directories...\n'
if not os.path.exists(output_dir):
os.makedirs(output_dir)
if not os.path.exists(temp_dir):
os.makedirs(temp_dir)
self.copy_files_to_project_folder()
json_file = utils.path_join(self.project_dir(), 'package.json')
global_json = utils.get_data_file_path('files/global.json')
js_template = get_file('files/mainjstemplate.js')
main_js_file = utils.path_join(self.project_dir(), 'main.js')
if self.output_package_json:
with codecs.open(json_file, 'w+', encoding='utf-8') as f:
f.write(self.generate_json())
with codecs.open(global_json, 'w+', encoding='utf-8') as f:
f.write(self.generate_json(global_json=True))
js_string = codecs.open(js_template, encoding='utf-8').read()
inject_js_start = ''
inject_js_end = ''
inject_start_file = self.get_setting('inject_js_start').value
inject_start_file = utils.path_join(self.project_dir(),
inject_start_file)
if os.path.isfile(inject_start_file):
inject_js_start = codecs.open(inject_start_file, encoding='utf-8').read()
inject_end_file = self.get_setting('inject_js_end').value
inject_end_file = utils.path_join(self.project_dir(),
inject_end_file)
if os.path.isfile(inject_end_file):
inject_js_end = codecs.open(inject_end_file, encoding='utf-8').read()
main_html = self.get_setting('main_html')
user_agent = self.get_setting('user_agent')
js_string = js_string.replace('{{command_line_switches}}', self.js_cmd_args)
js_string = js_string.replace('{{user_agent}}', '"'+user_agent.value+'"')
js_string = js_string.replace('{{main_html}}', main_html.value)
js_string = js_string.replace('{{main_window_line}}', self.js_window_init)
js_string = js_string.replace('{{app_name}}', self.project_name())
js_string = js_string.replace('{{inject_js_start}}', inject_js_start)
js_string = js_string.replace('{{inject_js_end}}', inject_js_end)
with codecs.open(main_js_file, 'w+', encoding='utf-8') as f:
f.write(js_string)
zip_file = utils.path_join(temp_dir, self.project_name()+'.nw')
app_electron_folder = utils.path_join(temp_dir, self.project_name()+'.nwf')
if self.project_dir() in self.output_dir():
utils.copytree(self.project_dir(), app_electron_folder,
ignore=shutil.ignore_patterns(os.path.basename(self.output_dir())))
else:
utils.copytree(self.project_dir(), app_electron_folder)
for ex_setting in self.settings['export_settings'].values():
if ex_setting.value:
self.progress_text = '\n'
name = ex_setting.display_name
self.progress_text = u'Making files for {}...'.format(name)
export_dest = utils.path_join(output_dir, ex_setting.name)
versions = re.findall('(\d+)\.(\d+)\.(\d+)', self.selected_version())[0]
minor = int(versions[1])
if os.path.exists(export_dest):
utils.rmtree(export_dest, ignore_errors=True)
# shutil will make the directory for us
utils.copytree(get_data_path('files/'+ex_setting.name),
export_dest,
ignore=shutil.ignore_patterns('place_holder.txt'))
utils.rmtree(get_data_path('files/'+ex_setting.name), ignore_errors=True)
self.progress_text += '.'
if 'mac' in ex_setting.name:
uncomp_setting = self.get_setting('uncompressed_folder')
uncompressed = uncomp_setting.value
app_path = utils.path_join(export_dest,
self.project_name()+'.app')
utils.move(utils.path_join(export_dest,
'Electron.app'),
app_path)
plist_path = utils.path_join(app_path, 'Contents', 'Info.plist')
plist_dict = plistlib.readPlist(plist_path)
plist_dict['CFBundleDisplayName'] = self.project_name()
plist_dict['CFBundleName'] = self.project_name()
version_setting = self.get_setting('version')
plist_dict['CFBundleShortVersionString'] = version_setting.value
plist_dict['CFBundleVersion'] = version_setting.value
plistlib.writePlist(plist_dict, plist_path)
self.progress_text += '.'
app_electron_res = utils.path_join(app_path,
'Contents',
'Resources',
'default_app')
if os.path.exists(app_electron_res):
utils.rmtree(app_electron_res)
utils.copytree(app_electron_folder, app_electron_res)
self.create_icns_for_app(utils.path_join(app_path,
'Contents',
'Resources',
'atom.icns'))
self.progress_text += '.'
else:
ext = ''
windows = False
if 'windows' in ex_setting.name:
ext = '.exe'
windows = True
electron_path = utils.path_join(export_dest,
ex_setting.binary_location)
if windows:
self.replace_icon_in_exe(electron_path)
self.compress_nw(electron_path)
dest_binary_path = utils.path_join(export_dest,
self.project_name() +
ext)
if 'linux' in ex_setting.name:
self.make_desktop_file(dest_binary_path, export_dest)
utils.move(electron_path, dest_binary_path)
app_electron_res = utils.path_join(export_dest,
'resources',
'default_app')
if os.path.exists(app_electron_res):
utils.rmtree(app_electron_res)
utils.copytree(app_electron_folder, app_electron_res)
sevenfivefive = (stat.S_IRWXU |
stat.S_IRGRP |
stat.S_IXGRP |
stat.S_IROTH |
stat.S_IXOTH)
os.chmod(dest_binary_path, sevenfivefive)
self.progress_text += '.'
if os.path.exists(electron_path):
os.remove(electron_path)
except Exception:
error = u''.join([x for x in traceback.format_exception(sys.exc_info()[0],
sys.exc_info()[1],
sys.exc_info()[2])])
self.logger.error(error)
self.output_err += error
finally:
utils.rmtree(temp_dir, onerror=self.remove_readonly)
def make_desktop_file(self, electron_path, export_dest):
icon_set = self.get_setting('icon')
icon_path = utils.path_join(self.project_dir(), icon_set.value)
if os.path.exists(icon_path) and icon_set.value:
utils.copy(icon_path, export_dest)
icon_path = utils.path_join(export_dest, os.path.basename(icon_path))
else:
icon_path = ''
name = self.project_name()
pdir = self.project_dir()
version = self.get_setting('version')
desc = self.get_setting('description')
dfile_path = utils.path_join(export_dest, u'{}.desktop'.format(name))
file_str = (
u'[Desktop Entry]\n'
u'Version={}\n'
u'Name={}\n'
u'Comment={}\n'
u'Exec={}\n'
u'Icon={}\n'
u'Terminal=false\n'
u'Type=Application\n'
u'Categories=Utility;Application;\n'
)
file_str = file_str.format(version.value,
name,
desc.value,
electron_path,
icon_path)
with codecs.open(dfile_path, 'w+', encoding='utf-8') as f:
f.write(file_str)
os.chmod(dfile_path, 0o755)
def compress_nw(self, electron_path):
compression = self.get_setting('electron_compression_level')
if compression.value == 0:
return
comp_dict = {'Darwin64bit': get_file('files/compressors/upx-mac'),
'Darwin32bit': get_file('files/compressors/upx-mac'),
'Linux64bit': get_file('files/compressors/upx-linux-x64'),
'Linux32bit': get_file('files/compressors/upx-linux-x32'),
'Windows64bit': get_file('files/compressors/upx-win.exe'),
'Windows32bit': get_file('files/compressors/upx-win.exe')
}
if is_installed():
comp_dict['Windows64bit'] = get_data_file_path('files/compressors/upx-win.exe')
comp_dict['Windows32bit'] = get_data_file_path('files/compressors/upx-win.exe')
plat = platform.system()+platform.architecture()[0]
upx_version = comp_dict.get(plat, None)
if upx_version is not None:
upx_bin = upx_version
os.chmod(upx_bin, 0o755)
cmd = [upx_bin, '--lzma', u'-{}'.format(compression.value), electron_path]
if platform.system() == 'Windows':
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE,
startupinfo=startupinfo)
else:
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
stdin=subprocess.PIPE)
self.progress_text = '\n\n'
self.progress_text = 'Compressing files'
while proc.poll() is None:
self.progress_text += '.'
time.sleep(2)
output, err = proc.communicate()
def remove_readonly(self, action, name, exc):
try:
os.chmod(name, stat.S_IWRITE)
os.remove(name)
except Exception as e:
error = u'Failed to remove file: {}.'.format(name)
error += '\nError recieved: {}'.format(e)
self.logger.error(error)
self.output_err += error
def copy_files_to_project_folder(self):
old_dir = CWD
os.chdir(self.project_dir())
self.logger.info(u'Copying files to {}'.format(self.project_dir()))
for sgroup in self.settings['setting_groups']:
for setting in sgroup.values():
if setting.copy and setting.type == 'file' and setting.value:
f_path = setting.value.replace(self.project_dir(), '')
if os.path.isabs(f_path):
try:
utils.copy(setting.value, self.project_dir())
self.logger.info(u'Copying file {} to {}'.format(setting.value, self.project_dir()))
except shutil.Error as e: # same file warning
self.logger.warning(u'Warning: {}'.format(e))
finally:
setting.value = os.path.basename(setting.value)
os.chdir(old_dir)
def convert_val_to_str(self, val):
if isinstance(val, (list, tuple)):
return ', '.join(val)
return str(val).replace(self.project_dir()+os.path.sep, '')
def run_script(self, script):
if not script:
return
if os.path.exists(script):
self.progress_text = 'Executing script {}...'.format(script)
contents = ''
with codecs.open(script, 'r', encoding='utf-8') as f:
contents = f.read()
_, ext = os.path.splitext(script)
export_opts = self.get_export_options()
export_dir = '{}{}{}'.format(self.output_dir(),
os.path.sep,
self.project_name())
export_dirs = []
for opt in export_opts:
export_dirs.append('{}{}{}'.format(export_dir, os.path.sep, opt))
command = None
bat_file = None
export_dict = {'mac-x64_dir': '',
'mac-x32_dir': '',
'windows-x64_dir': '',
'windows-x32_dir': '',
'linux-x64_dir': '',
'linux-x32_dir': ''}
if ext == '.py':
env_file = get_file('files/env_vars.py')
env_contents = codecs.open(env_file, 'r', encoding='utf-8').read()
for i, ex_dir in enumerate(export_dirs):
opt = export_opts[i]
export_dict[opt+'_dir'] = ex_dir
env_vars = env_contents.format(proj_dir=self.project_dir(),
proj_name=self.project_name(),
export_dir=export_dir,
export_dirs=str(export_dirs),
num_dirs=len(export_dirs),
**export_dict)
pycontents = '{}\n{}'.format(env_vars, contents)
command = ['python', '-c', pycontents]
elif ext == '.bash':
env_file = get_file('files/env_vars.bash')
env_contents = codecs.open(env_file, 'r', encoding='utf-8').read()
ex_dir_vars = ''
for i, ex_dir in enumerate(export_dirs):
opt = export_opts[i]
export_dict[opt+'_dir'] = ex_dir
for ex_dir in export_dirs:
ex_dir_vars += "'{}' ".format(ex_dir)
env_vars = env_contents.format(proj_dir=self.project_dir(),
proj_name=self.project_name(),
export_dir=export_dir,
num_dirs=len(export_dirs),
export_dirs=ex_dir_vars,
**export_dict)
shcontents = '{}\n{}'.format(env_vars, contents)
command = ['bash', '-c', shcontents]
elif ext == '.bat':
env_file = get_file('files/env_vars.bat')
env_contents = codecs.open(env_file, 'r', encoding='utf-8').read()
ex_dir_vars = ''
for i, ex_dir in enumerate(export_dirs):
opt = export_opts[i]
export_dict[opt+'_dir'] = ex_dir
ex_dir_vars += 'set "EXPORT_DIRS[{}]={}"\n'.format(i, ex_dir)
env_vars = env_contents.format(proj_dir=self.project_dir(),
proj_name=self.project_name(),
export_dir=export_dir,
num_dirs=len(export_dirs),
export_dirs=ex_dir_vars,
**export_dict)
batcontents = '{}\n{}'.format(env_vars, contents)
bat_file = utils.path_join(TEMP_DIR, '{}.bat'.format(self.project_name()))
self.logger.debug(batcontents)
with open(bat_file, 'w+') as f:
f.write(batcontents)
command = [bat_file]
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
output, error = proc.communicate()
output = output.strip()
error = error.strip()
if bat_file:
os.remove(bat_file)
with open(get_file('script-output.txt'), 'w+') as f:
f.write('Output:\n{}'.format(output))
if error:
f.write('\n\nErrors:\n{}\n'.format(error))
self.progress_text = 'Done executing script.'
else:
self.progress_text = '\nThe script {} does not exist. Not running.'.format(script)
def export(self):
self.get_files_to_download()
res = self.try_to_download_files()
if res:
self.make_output_dirs()
script = self.get_setting('custom_script').value
self.run_script(script)
self.progress_text = '\nDone!\n'
self.progress_text = u'Output directory is {}{}{}.\n'.format(self.output_dir(),
os.path.sep,
self.project_name())
self.delete_files()
else:
print('Export failed. Check the log at {} or run again with --verbose.'.format(LOG_FILENAME))
def get_export_options(self):
options = []
for setting_name, setting in self.settings['export_settings'].items():
if setting.value is True:
options.append(setting_name)
return options
def get_files_to_download(self):
self.files_to_download = []
for setting_name, setting in self.settings['export_settings'].items():
if setting.value is True:
self.files_to_download.append(setting)
return True
def try_to_download_files(self):
if self.files_to_download:
return self.download_file_with_error_handling()
def continue_downloading_or_extract(self):
if self.files_to_download:
return self.download_file_with_error_handling()
else:
self.progress_text = 'Extracting files.'
return self.extract_files()
def get_redirected_url(self, url):
opener = request.build_opener(request.HTTPRedirectHandler)
req = opener.open(url)
return req.url
def download_file(self, path, setting):
self.logger.info(u'Downloading file {}.'.format(path))
location = self.get_setting('download_dir').value
versions = re.findall('v(\d+)\.(\d+)\.(\d+)', path)[0]
path = self.get_redirected_url(path)
url = path
file_name = setting.save_file_path(self.selected_version(), location)
tmp_file = list(os.path.split(file_name))
tmp_file[-1] = '.tmp.' + tmp_file[-1]
tmp_file = os.sep.join(tmp_file)
tmp_size = 0
archive_exists = os.path.exists(file_name)
tmp_exists = os.path.exists(tmp_file)
dest_files_exist = False
forced = self.get_setting('force_download').value
if (archive_exists or dest_files_exist) and not forced:
print('File exists.')
self.logger.info(u'File {} already downloaded. Continuing...'.format(path))
return self.continue_downloading_or_extract()
elif tmp_exists and (os.stat(tmp_file).st_size > 0):
tmp_size = os.stat(tmp_file).st_size
headers = {'Range': 'bytes={}-'.format(tmp_size)}
url = request.Request(url, headers=headers)
web_file = request.urlopen(url)
f = open(tmp_file, 'ab')
meta = web_file.info()
file_size = tmp_size + int(meta["Content-Length"])
version = self.selected_version()
version_file = self.settings['base_url'].format(version)
short_name = path.replace(version_file, '')
MB = file_size/1000000.0
downloaded = ''
if tmp_size:
self.progress_text = 'Resuming previous download...\n'
self.progress_text = u'Already downloaded {:.2f} MB\n'.format(tmp_size/1000000.0)
self.progress_text = (u'Downloading: {}, '
u'Size: {:.2f} MB {}\n'.format(short_name,
MB,
downloaded))
file_size_dl = (tmp_size or 0)
block_sz = 8192
while True:
buff = web_file.read(block_sz)
if not buff:
break
file_size_dl += len(buff)
DL_MB = file_size_dl/1000000.0
percent = file_size_dl*100.0/file_size
f.write(buff)
args = (DL_MB, MB, percent)
status = "{:10.2f}/{:.2f} MB [{:3.2f}%]".format(*args)
self.progress_text = status
self.progress_text = '\nDone downloading.\n'
f.close()
try:
os.rename(tmp_file, file_name)
except OSError:
if sys.platform.startswith('win32') and not(os.path.isdir(file_name)):
os.remove(file_name)
os.rename(tmp_file, file_name)
else:
os.remove(tmp_file)
raise OSError
return self.continue_downloading_or_extract()
def delete_files(self):
for ex_setting in self.settings['export_settings'].values():
f_path = get_data_file_path('files/{}/'.format(ex_setting.name))
if os.path.exists(f_path):
utils.rmtree(f_path)
class ArgParser(argparse.ArgumentParser):
def error(self, message):
sys.stderr.write('error: {}\n'.format(message))
self.print_help()
sys.exit(2)
def unicode_arg(bytestring):
return bytestring
def main():
parser = ArgParser(description=('Command line interface '
'to electrify. {}'.format(__version__)),
prog='electrifycmd')
command_base = CommandBase()
command_base.init()
parser.add_argument('project_dir', metavar='project_dir',
help='The project directory.', type=unicode_arg)
parser.add_argument('--output-dir', dest='output_dir',
help='The output directory for exports.',
type=unicode_arg)
parser.add_argument('--quiet', dest='quiet', action='store_true',
default=False,
help='Silences output messages')
parser.add_argument('--verbose', dest='verbose', action='store_true',
default=False,
help=('Prints debug errors and messages instead '
'of logging to files/errors.log'))
parser.add_argument('--package-json',
dest='load_json',
nargs='?',
default='',
const=True,
help=('Loads the package.json '
'file in the project directory. '
'Ignores other command line arguments.'))
parser.add_argument('--cmd-version', action='version', version='%(prog)s {}'.format(__version__))
for setting_group_dict in command_base.settings['setting_groups']+[command_base.settings['compression']]:
for setting_name, setting in setting_group_dict.items():
kwargs = {}
if setting_name == 'name':
kwargs.update({'default': command_base.project_name})
else:
kwargs.update({'required': setting.required,
'default': setting.default_value})
action = 'store'
option_name = setting_name.replace('_', '-')
if setting.type in ['file', 'string', 'strings']:
kwargs.update({'type': unicode_arg})
if isinstance(setting.default_value, bool):
action = ('store_true' if setting.default_value is False
else 'store_false')
kwargs.update({'action': action})
if setting.default_value is True:
option_name = u'disable-{}'.format(option_name)
else:
if setting.values:
kwargs.update({'choices': setting.values})
setting.description += u' Possible values: {{{}}}'.format(', '.join([str(x) for x in setting.values]))
kwargs.update({'metavar': ''})
else:
kwargs.update({'metavar': '<{}>'.format(setting.display_name)})
parser.add_argument(u'--{}'.format(option_name),
dest=setting_name,
help=setting.description,
**kwargs
)
export_args = [arg for arg in command_base.settings['export_settings']]
parser.add_argument('--export-to', dest='export_options',
nargs='+', required=True,
choices=export_args,
help=('Choose at least one system '
'to export to.'))
args = parser.parse_args()
if args.verbose:
logging.basicConfig(
stream=sys.stdout,
format=("%(levelname) -10s %(module)s.py: "
"%(lineno)s %(funcName)s - %(message)s"),
level=logging.DEBUG
)
else:
logging.basicConfig(
filename=LOG_FILENAME,
format=("%(levelname) -10s %(asctime)s %(module)s.py: "
"%(lineno)s %(funcName)s - %(message)s"),
level=logging.DEBUG
)
global logger
global handler
logger = logging.getLogger('CMD Logger')
handler = lh.RotatingFileHandler(LOG_FILENAME, maxBytes=100000, backupCount=2)
logger.addHandler(handler)
def my_excepthook(type_, value, tback):
output_err = u''.join([x for x in traceback.format_exception(type_, value, tback)])
logger.error(u'{}'.format(output_err))
sys.__excepthook__(type_, value, tback)
sys.excepthook = my_excepthook
command_base.logger = logger
if args.quiet:
command_base.quiet = True
command_base._project_dir = args.project_dir
command_base._output_dir = (args.output_dir or
utils.path_join(command_base._project_dir, 'output'))
if args.app_name is None:
args.app_name = command_base.project_name()
command_base._project_name = args.app_name if not callable(args.app_name) else args.app_name()
if not args.title:
args.title = command_base.project_name()
for name, val in args._get_kwargs():
if callable(val):
val = val()
if name == 'export_options':
for opt in val:
setting = command_base.get_setting(opt)
if setting is not None:
setting.value = True
else:
setting = command_base.get_setting(name)
if setting is not None:
setting.value = val
if args.load_json is True:
command_base.load_package_json()
elif args.load_json:
command_base.load_package_json(args.load_json)
command_base.export()
if __name__ == '__main__':
main()