Skip to content

Commit 06040d0

Browse files
committed
Initial version from our internal codebase
0 parents  commit 06040d0

4 files changed

Lines changed: 349 additions & 0 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*.pyc
2+
*.pyo
3+
*.egg-info
4+
.DS_Store

LICENSE

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
Copyright (c) 2012 by Fireteam Ltd., see AUTHORS for more details.
2+
3+
Redistribution and use in source and binary forms, with or without
4+
modification, are permitted provided that the following conditions are
5+
met:
6+
7+
* Redistributions of source code must retain the above copyright
8+
notice, this list of conditions and the following disclaimer.
9+
10+
* Redistributions in binary form must reproduce the above
11+
copyright notice, this list of conditions and the following
12+
disclaimer in the documentation and/or other materials provided
13+
with the distribution.
14+
15+
* The names of the contributors may not be used to endorse or
16+
promote products derived from this software without specific
17+
prior written permission.
18+
19+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
2+
-- virtualenv-tools
3+
4+
This repository contains scripts we're using at Fireteam for our
5+
deployment of Python code. We're using them in combination with
6+
salt to build code on one server on a self contained virtualenv
7+
and then move that over to the destination servers to run.
8+
9+
Why not virtualenv --relocatable?
10+
11+
For starters: because it does not work. relocatable is very
12+
limited in what it does and it works at runtime instead of
13+
making the whole thing actually move to the new location. We
14+
ran into a ton of issues with it and it is currently in the
15+
process of being phased out.
16+
17+
Why would I want to use it?
18+
19+
The main reason you want to use this is for build caching. You
20+
have one folder where one virtualenv exists, you install the
21+
latest version of your codebase and all extensions in there, then
22+
you can make the virtualenv relocate to a target location, put it
23+
into a tarball, distribute it to all servers and done!
24+
25+
Example flow:
26+
27+
First time: create the build cache
28+
29+
$ mkdir /tmp/build-cache
30+
$ virtualenv --distribute /tmp/build-cache
31+
32+
Now every time you build:
33+
34+
$ . /tmp/build-cache/bin/activate
35+
$ pip install YourApplication
36+
37+
Build done, package up and copy to whatever location you want to have
38+
it.
39+
40+
Once unpacked on the target server, use the virtualenv tools to
41+
update the paths and make the virtualenv magically work in the new
42+
location. For instance we deploy things to a path with the
43+
hash of the commit in:
44+
45+
$ virtualenv-tools --update-path /srv/your-application/<hash>
46+
47+
48+
Compile once, deploy whereever. Virtualenvs are completely self
49+
contained. In order to switch the current version all you need to
50+
do is to relink the builds.
51+
52+

virtualenv-tools.py

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
#!/usr/bin/env python
2+
"""
3+
move-virtualenv
4+
~~~~~~~~~~~~~~~
5+
6+
A helper script that moves virtualenvs to a new location.
7+
8+
It only supports POSIX based virtualenvs and Python 2 at the moment.
9+
10+
:copyright: (c) 2012 by Fireteam Ltd.
11+
:license: BSD, see LICENSE for more details.
12+
"""
13+
import os
14+
import re
15+
import sys
16+
import marshal
17+
import optparse
18+
import subprocess
19+
from types import CodeType
20+
21+
22+
ACTIVATION_SCRIPTS = [
23+
'activate',
24+
'activate.csh',
25+
'activate.fish'
26+
]
27+
_pybin_match = re.compile(r'^python\d+\.\d+$')
28+
_activation_path_re = re.compile(r'^(?:set -gx |setenv |)VIRTUAL_ENV[ =]"(.*?)"\s*$')
29+
30+
31+
def update_activation_script(script_filename, new_path):
32+
"""Updates the paths for the activate shell scripts."""
33+
with open(script_filename) as f:
34+
lines = list(f)
35+
36+
def _handle_sub(match):
37+
text = match.group()
38+
start, end = match.span()
39+
g_start, g_end = match.span(1)
40+
return text[:(g_start - start)] + new_path + text[(g_end - end):]
41+
42+
changed = False
43+
for idx, line in enumerate(lines):
44+
new_line = _activation_path_re.sub(_handle_sub, line)
45+
if line != new_line:
46+
lines[idx] = new_line
47+
changed = True
48+
49+
if changed:
50+
print 'A %s' % script_filename
51+
with open(script_filename, 'w') as f:
52+
f.writelines(lines)
53+
54+
55+
def update_script(script_filename, new_path):
56+
"""Updates shebang lines for actual scripts."""
57+
with open(script_filename) as f:
58+
lines = list(f)
59+
if not lines:
60+
return
61+
62+
if not lines[0].startswith('#!'):
63+
return
64+
args = lines[0][2:].strip().split()
65+
if not args:
66+
return
67+
68+
if not args[0].endswith('/bin/python') or \
69+
'/usr/bin/env python' in args[0]:
70+
return
71+
72+
new_bin = os.path.join(new_path, 'bin', 'python')
73+
if new_bin == args[0]:
74+
return
75+
76+
args[0] = new_bin
77+
lines[0] = '#!%s\n' % ' '.join(args)
78+
print 'S %s' % script_filename
79+
with open(script_filename, 'w') as f:
80+
f.writelines(lines)
81+
82+
83+
def update_scripts(bin_dir, new_path):
84+
"""Updates all scripts in the bin folder."""
85+
for fn in os.listdir(bin_dir):
86+
if fn in ACTIVATION_SCRIPTS:
87+
update_activation_script(os.path.join(bin_dir, fn), new_path)
88+
else:
89+
update_script(os.path.join(bin_dir, fn), new_path)
90+
91+
92+
def update_pyc(filename, new_path):
93+
"""Updates the filenames stored in pyc files."""
94+
with open(filename, 'rb') as f:
95+
magic = f.read(8)
96+
code = marshal.load(f)
97+
98+
def _make_code(code, filename, consts):
99+
return CodeType(code.co_argcount, code.co_nlocals, code.co_stacksize,
100+
code.co_flags, code.co_code, tuple(consts),
101+
code.co_names, code.co_varnames, filename,
102+
code.co_name, code.co_firstlineno, code.co_lnotab,
103+
code.co_freevars, code.co_cellvars)
104+
105+
def _process(code):
106+
new_filename = new_path
107+
consts = []
108+
for const in code.co_consts:
109+
if type(const) is CodeType:
110+
const = _process(const)
111+
consts.append(const)
112+
if new_path != code.co_filename or consts != list(code.co_consts):
113+
code = _make_code(code, new_path, consts)
114+
return code
115+
116+
new_code = _process(code)
117+
118+
if new_code is not code:
119+
print 'B %s' % filename
120+
with open(filename, 'wb') as f:
121+
f.write(magic)
122+
marshal.dump(new_code, f)
123+
124+
125+
def update_pycs(lib_dir, new_path, lib_name):
126+
"""Walks over all pyc files and updates their paths."""
127+
files = []
128+
129+
def get_new_path(filename):
130+
filename = os.path.normpath(filename)
131+
if filename.startswith(lib_dir.rstrip('/') + '/'):
132+
return os.path.join(new_path, filename[len(lib_dir) + 1:])
133+
134+
for dirname, dirnames, filenames in os.walk(lib_dir):
135+
for filename in filenames:
136+
if filename.endswith(('.pyc', '.pyo')):
137+
filename = os.path.join(dirname, filename)
138+
local_path = get_new_path(filename)
139+
if local_path is not None:
140+
update_pyc(filename, local_path)
141+
142+
143+
def update_local(base, new_path):
144+
"""On some systems virtualenv seems to have something like a local
145+
directory with symlinks. It appears to happen on debian systems and
146+
it causes havok if not updated. So do that.
147+
"""
148+
local_dir = os.path.join(base, 'local')
149+
if not os.path.isdir(local_dir):
150+
return
151+
152+
for folder in 'bin', 'lib', 'include':
153+
filename = os.path.join(local_dir, folder)
154+
target = '../%s' % folder
155+
if os.path.islink(filename) and os.readlink(filename) != target:
156+
os.remove(filename)
157+
os.symlink('../%s' % folder, filename)
158+
print 'L %s' % filename
159+
160+
161+
def update_paths(base, new_path):
162+
"""Updates all paths in a virtualenv to a new one."""
163+
if new_path == 'auto':
164+
new_path = os.path.abspath(base)
165+
if not os.path.isabs(new_path):
166+
print 'error: %s is not an absolute path' % new_path
167+
return False
168+
169+
bin_dir = os.path.join(base, 'bin')
170+
base_lib_dir = os.path.join(base, 'lib')
171+
lib_dir = None
172+
lib_name = None
173+
174+
if os.path.isdir(base_lib_dir):
175+
for folder in os.listdir(base_lib_dir):
176+
if _pybin_match.match(folder):
177+
lib_name = folder
178+
lib_dir = os.path.join(base_lib_dir, folder)
179+
break
180+
181+
if lib_dir is None or not os.path.isdir(bin_dir) \
182+
or not os.path.isfile(os.path.join(bin_dir, 'python')):
183+
print 'error: %s does not refer to a python installation' % base
184+
return False
185+
186+
update_scripts(bin_dir, new_path)
187+
update_pycs(lib_dir, new_path, lib_name)
188+
update_local(base, new_path)
189+
190+
return True
191+
192+
193+
def reinitialize_virtualenv(path):
194+
"""Re-initializes a virtualenv."""
195+
lib_dir = os.path.join(path, 'lib')
196+
if not os.path.isdir(lib_dir):
197+
print 'error: %s is not a virtualenv bin folder' % path
198+
return False
199+
200+
py_ver = None
201+
for filename in os.listdir(lib_dir):
202+
if _pybin_match.match(filename):
203+
py_ver = filename
204+
break
205+
206+
if py_ver is None:
207+
print 'error: could not detect python version of virtualenv %s' % path
208+
return False
209+
210+
sys_py_executable = subprocess.Popen(['which', py_ver],
211+
stdout=subprocess.PIPE).communicate()[0].strip()
212+
213+
if not sys_py_executable:
214+
print 'error: could not find system version for expected python ' \
215+
'version %s' % py_ver
216+
return False
217+
218+
lib_dir = os.path.join(path, 'lib', py_ver)
219+
220+
args = ['virtualenv', '-p', sys_py_executable]
221+
if not os.path.isfile(os.path.join(lib_dir,
222+
'no-global-site-packages.txt')):
223+
args.append('--system-site-packages')
224+
225+
for filename in os.listdir(lib_dir):
226+
if filename.startswith('distribute-') and \
227+
filename.endswith('.egg'):
228+
args.append('--distribute')
229+
230+
new_env = {}
231+
for key, value in os.environ.items():
232+
if not key.startswith('VIRTUALENV_'):
233+
new_env[key] = value
234+
args.append(path)
235+
subprocess.Popen(args, env=new_env).wait()
236+
237+
238+
def main():
239+
parser = optparse.OptionParser()
240+
parser.add_option('--reinitialize', action='store_true',
241+
help='Updates the python installation '
242+
'and reinitializes the virtualenv.')
243+
parser.add_option('--update-path', help='Update the path for all '
244+
'required executables and helper files that are '
245+
'supported to the new python prefix. You can also set '
246+
'this to "auto" for autodetection.')
247+
options, paths = parser.parse_args()
248+
if not paths:
249+
paths = ['.']
250+
251+
rv = 0
252+
253+
if options.reinitialize:
254+
for path in paths:
255+
reinitialize_virtualenv(path)
256+
if options.update_path:
257+
for path in paths:
258+
if not update_paths(path, options.update_path):
259+
rv = 1
260+
sys.exit(rv)
261+
262+
263+
if __name__ == '__main__':
264+
main()

0 commit comments

Comments
 (0)