Skip to content

Commit afdeea5

Browse files
committed
Add Python build and 'build reducer' scripts
0 parents  commit afdeea5

File tree

3 files changed

+303
-0
lines changed

3 files changed

+303
-0
lines changed

.travis.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Travis can building for Linux and macOS
2+
matrix:
3+
include:
4+
# To maximise compatibility pick earliest image, OS X 10.10 Yosemite
5+
- os: osx
6+
osx_image: xcode6.4
7+
sudo: required
8+
language: generic
9+
10+
before_install:
11+
# OS and default Python info
12+
- uname -a
13+
- if [ "$TRAVIS_OS_NAME" = "osx" ]; then sw_vers; fi
14+
15+
install:
16+
- HOMEBREW_NO_AUTO_UPDATE=1 brew install openssl
17+
- PROJECT_DIR="$PWD"
18+
- echo $PROJECT_DIR
19+
- mkdir upload
20+
21+
script:
22+
# Download and build Python
23+
- cd "$PROJECT_DIR"
24+
- sh build_python.sh
25+
26+
# Compress and upload it before touching it
27+
- cd "$PROJECT_DIR"
28+
- tar czf upload/python3-full.tar.gz python-portable/
29+
- curl --upload-file ./upload/python3-full.tar.gz https://transfer.sh/python3-full.tar.gz | tee -a output_urls.txt && echo "" >> output_urls.txt
30+
31+
# Check built Python
32+
- cd "$PROJECT_DIR"
33+
- du -sk python-portable/
34+
- cd python-portable
35+
- otool -L bin/python3.6
36+
- ./bin/python3.6 -c 'import ssl; print(ssl.OPENSSL_VERSION)'
37+
- ./bin/python3.6 -m pip --version
38+
39+
# Reduce stand-alone Python and upload it
40+
- cd "$PROJECT_DIR"
41+
- python process_python_build.py "$PROJECT_DIR/python-portable"
42+
- tar czf upload/python3-reduced.tar.gz python-portable/
43+
- curl --upload-file ./upload/python3-reduced.tar.gz https://transfer.sh/python3-reduced.tar.gz | tee -a output_urls.txt && echo "" >> output_urls.txt
44+
45+
# Print all uploaded files URLs
46+
- cd "$PROJECT_DIR"
47+
- cat output_urls.txt

build_python.sh

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/bin/sh
2+
3+
alias large_echo='{ set +x; } 2> /dev/null; f(){ echo "#\n#\n# $1\n#\n#"; set -x; }; f'
4+
5+
large_echo "Check OpenSSL installation path and CWD"
6+
if brew ls --versions openssl > /dev/null; then
7+
OPENSSL_ROOT="$(brew --prefix openssl)"
8+
echo $OPENSSL_ROOT
9+
else
10+
echo "Please install OpenSSL with brew: 'brew install openssl'"
11+
exit 1
12+
fi
13+
CURRENT_DIR="$PWD"
14+
echo $CURRENT_DIR
15+
16+
large_echo "Download and uncompress Python source"
17+
wget https://www.python.org/ftp/python/3.6.5/Python-3.6.5.tgz
18+
tar -zxvf Python-3.6.5.tgz &> /dev/null
19+
20+
cd Python-3.6.5
21+
large_echo "Configure Python"
22+
./configure MACOSX_DEPLOYMENT_TARGET=10.9 CPPFLAGS="-I$OPENSSL_ROOT/include" LDFLAGS="-L$OPENSSL_ROOT/lib" --prefix="$CURRENT_DIR/python-portable"
23+
large_echo "Build Python"
24+
make altinstall

process_python_build.py

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
"""
4+
Prepares a portable Python directory.
5+
"""
6+
import os
7+
import sys
8+
import shutil
9+
import subprocess
10+
import py_compile
11+
12+
13+
VERBOSE = False
14+
15+
# Python version is used for folder names and exec files in built output
16+
VERSION_STR = '3.6'
17+
PYTHON_VER = 'python{}'.format(VERSION_STR)
18+
19+
# Set the files and directories we can remove from a Python build folder
20+
PYTHON_REMOVE_DIRS = [
21+
os.path.join('share', 'man'),
22+
os.path.join('lib', PYTHON_VER, 'ensurepip'),
23+
os.path.join('lib', PYTHON_VER, 'idlelib'),
24+
os.path.join('lib', PYTHON_VER, 'test'),
25+
]
26+
PYTHON_REMOVE_FILES = [
27+
#os.path.join('lib', PYTHON_VER, 'ensurepip'),
28+
#os.path.join('bin', '{}m'.format(PYTHON_VER)),
29+
]
30+
31+
# Files and folders to keep inside the bin folder
32+
PYTHON_KEEP_BIN_ITEMS = [
33+
PYTHON_VER,
34+
'pip{}'.format(VERSION_STR),
35+
]
36+
37+
38+
def remove_file(file_to_remove):
39+
"""
40+
Removes the given file if it exists, print info out if not.
41+
:param file_to_remove: Path to file to remove.
42+
"""
43+
if os.path.isfile(file_to_remove):
44+
if VERBOSE:
45+
print('\tRemoving file {}'.format(file_to_remove))
46+
os.remove(file_to_remove)
47+
else:
48+
print('\tFile {} was not found.'.format(file_to_remove))
49+
50+
51+
def remove_directory(dir_to_remove):
52+
"""
53+
Removes the given directory if it exists, print info out if not.
54+
:param dir_to_remove: Path to folder to remove.
55+
"""
56+
if os.path.isdir(dir_to_remove):
57+
if VERBOSE:
58+
print('\tRemoving directory {}'.format(dir_to_remove))
59+
shutil.rmtree(dir_to_remove)
60+
else:
61+
print('\tDirectory {} was not found.'.format(dir_to_remove))
62+
63+
64+
def remove_file_type_from(file_extension, scan_path):
65+
"""
66+
Goes through a directory recursively and removes all files with an specific
67+
extension.
68+
:param file_extension: File extension of the files to remove.
69+
:param scan_path: Directory to scan for file type removal.
70+
"""
71+
for root, dirs, files in os.walk(scan_path, topdown=False):
72+
for file_ in files:
73+
if file_.endswith('.' + file_extension):
74+
file_path = os.path.join(root, file_)
75+
remove_file(file_path)
76+
77+
78+
def remove_all_folder_items_except(items_to_exclude, scan_path):
79+
"""
80+
Goes through a directory immediate child files and folders and remove all
81+
except those indicated in the items_to_exclude argument.
82+
The items_to_exclude list MUST NOT contain full paths, just file/folder
83+
names.
84+
"""
85+
scan_path = os.path.abspath(scan_path)
86+
for entry_name in os.listdir(scan_path):
87+
full_path = os.path.join(scan_path, entry_name)
88+
if os.path.exists(full_path) and entry_name not in items_to_exclude:
89+
remove_file(full_path)
90+
91+
92+
def compress_folder(folder_path, zip_path, zip_as_folder=True):
93+
"""
94+
Compresses the folder indicated by folder_path, without the a pa
95+
"""
96+
folder_path = os.path.abspath(folder_path)
97+
zip_path = os.path.abspath(zip_path)
98+
if os.path.isfile(zip_path):
99+
raise Exception('Destination file {} already exists.'.format(zip_path))
100+
101+
old_cwd = os.getcwd()
102+
parent_path = os.path.dirname(folder_path)
103+
if zip_as_folder:
104+
os.chdir(parent_path)
105+
path_to_zip = os.path.relpath(folder_path, parent_path)
106+
else:
107+
os.chdir(folder_path)
108+
path_to_zip = '.'
109+
110+
zip_process = subprocess.Popen(
111+
["zip", "--symlinks", "-r", zip_path, path_to_zip],
112+
stdout=subprocess.PIPE,
113+
stderr=subprocess.PIPE)
114+
std_out, std_err = zip_process.communicate()
115+
if VERBOSE:
116+
print(std_out)
117+
if std_err:
118+
raise Exception('Error zipping standard library:\n{}'.format(std_err))
119+
120+
os.chdir(old_cwd)
121+
122+
123+
def remove_pycache_dirs(scan_path):
124+
"""
125+
Recursively removes all folders named "__pycache__" from the given path.
126+
:param scan_path: Directory to scan for __pycache__ removal.
127+
:return:
128+
"""
129+
for root, dirs, files in os.walk(scan_path, topdown=False):
130+
for name in dirs:
131+
if name == '__pycache__':
132+
remove_directory(os.path.join(root, name))
133+
134+
135+
def compile_pyc(py_file, pyc_file):
136+
"""
137+
Uses current running interpreter, compiles a Python file into a pyc file.
138+
"""
139+
py_file = os.path.abspath(py_file)
140+
pyc_file = os.path.abspath(pyc_file)
141+
142+
if not os.path.isfile(py_file) or not py_file.lower().endswith('.py'):
143+
raise Exception('Not a Python source file: {}'.format(py_file))
144+
if os.path.isfile(pyc_file):
145+
raise Exception('Destination file {} already exists.'.format(pyc_file))
146+
147+
print('\tCompiling file {} to {}'.format(py_file, pyc_file))
148+
py_compile.compile(py_file, cfile=pyc_file, doraise=True)
149+
150+
151+
def compile_pyc_dir(python_exec_path, src_path):
152+
"""
153+
Use the command line to execute compileall python utility on a directory.
154+
"""
155+
py_process = subprocess.Popen(
156+
[python_exec_path, '-m', 'compileall', '-b', '-f', src_path],
157+
stdout=subprocess.PIPE,
158+
stderr=subprocess.PIPE)
159+
std_out, std_err = py_process.communicate()
160+
if VERBOSE:
161+
print(std_out)
162+
if std_err:
163+
raise Exception('Error using Python compileall:\n{}'.format(std_err))
164+
165+
166+
def get_python_path(args):
167+
"""
168+
Gets the first item of the args list and verifies is a valid path to a
169+
directory. Assumes the input argument comes from the command line.
170+
"""
171+
# Check if a command line argument has been given
172+
if args:
173+
msg = 'Command line argument "{}" found'.format(args[0])
174+
# Take the first argument and use it as a tag appendage
175+
if os.path.isdir(args[0]):
176+
abs_path = os.path.abspath(args[0])
177+
print('{} as Python path:\n\t{}'.format(msg, abs_path))
178+
return abs_path
179+
else:
180+
raise Exception(msg + ', but it is not a valid path')
181+
else:
182+
raise Exception('No command line argument found')
183+
184+
185+
def main(args):
186+
python_path = get_python_path(args)
187+
std_lib_path = os.path.join(python_path, 'lib', PYTHON_VER)
188+
bin_path = os.path.join(python_path, 'bin')
189+
python_exec_path = os.path.join(python_path, 'bin', PYTHON_VER)
190+
global VERBOSE
191+
192+
print('Remove unnecessary directories:')
193+
VERBOSE = True
194+
for dir_ in PYTHON_REMOVE_DIRS:
195+
full_path = os.path.join(python_path, dir_)
196+
remove_directory(full_path)
197+
VERBOSE = False
198+
199+
print('Remove unnecessary files:')
200+
VERBOSE = True
201+
for file_ in PYTHON_REMOVE_FILES:
202+
full_path = os.path.join(python_path, file_)
203+
print('\tRemoving "{}"'.format(full_path))
204+
remove_file(full_path)
205+
remove_all_folder_items_except(PYTHON_KEEP_BIN_ITEMS, bin_path)
206+
VERBOSE = False
207+
208+
print('Remove __pycache__ directories from "{}"'.format(std_lib_path))
209+
remove_pycache_dirs(std_lib_path)
210+
211+
print('Compile Python files from "{}"'.format(std_lib_path))
212+
compile_pyc_dir(python_exec_path=python_exec_path, src_path=std_lib_path)
213+
214+
print('Remove Python source files from "{}"'.format(std_lib_path))
215+
remove_file_type_from('py', std_lib_path)
216+
217+
print('Remove __pycache__ directories from "{}"'.format(std_lib_path))
218+
remove_pycache_dirs(std_lib_path)
219+
220+
# TODO: Figure out compressing issue and if it's worth doing
221+
# print('\nCompressing the stand library "{}"'.format(std_lib_path))
222+
# compress_folder(std_lib_path,
223+
# os.path.join(python_path, 'lib', PYTHON_VER + '.zip'),
224+
# zip_as_folder=False)
225+
# print('\nRemove uncompressed stand library dir"{}"'.format(std_lib_path))
226+
# remove_directory(std_lib_path)
227+
228+
print('\nAll done! :)')
229+
230+
231+
if __name__ == "__main__":
232+
main(sys.argv[1:])

0 commit comments

Comments
 (0)