3 """Software Carpentry Windows Installer
5 Helps mimic a *nix environment on Windows with as little work as possible.
8 * Installs nano and makes it accessible from msysGit
9 * Installs SQLite and makes it accessible from msysGit
10 * Creates a ~/nano.rc with links to syntax highlighting configs
11 * Provides standard nosetests behavior for msysGit
12 * Adds R's bin directory to the path (if we can find it)
16 1. Install Python, IPython, and Nose. An easy way to do this is with
17 the Anaconda CE Python distribution
18 http://continuum.io/anacondace.html
20 https://github.com/msysgit/msysgit/releases
21 3. Run swc-windows-installer.py.
22 You should be able to simply double click the file in Windows
29 from io import BytesIO as _BytesIO
30 except ImportError: # Python 2
31 from StringIO import StringIO as _BytesIO
38 from urllib.request import urlopen as _urlopen
39 except ImportError: # Python 2
40 from urllib2 import urlopen as _urlopen
44 LOG = logging.getLogger('swc-windows-installer')
45 LOG.addHandler(logging.StreamHandler())
46 LOG.setLevel(logging.INFO)
49 if sys.version_info >= (3, 0): # Python 3
52 def open3(file, mode='r', newline=None):
55 raise NotImplementedError(newline)
56 f = open(file, mode + 'b')
62 def download(url, sha1):
63 """Download a file and verify it's hash"""
64 LOG.debug('download {}'.format(url))
66 byte_content = r.read()
67 download_sha1 = hashlib.sha1(byte_content).hexdigest()
68 if download_sha1 != sha1:
70 'downloaded {!r} has the wrong SHA-1 hash: {} != {}'.format(
71 url, download_sha1, sha1))
72 LOG.debug('SHA-1 for {} matches the expected {}'.format(url, sha1))
77 """Split a path into a list of components
79 >>> splitall('nano-2.2.6/doc/Makefile.am')
80 ['nano-2.2.6', 'doc', 'Makefile.am']
84 head, tail = os.path.split(path)
96 def transform(tarinfo, strip_components=0):
97 """Transform TarInfo objects for extraction"""
98 path_components = splitall(tarinfo.name)
100 tarinfo.name = os.path.join(*path_components[strip_components:])
102 if len(path_components) <= strip_components:
108 def tar_install(url, sha1, install_directory, compression='*',
110 """Download and install a tar bundle"""
111 if not os.path.isdir(install_directory):
112 tar_bytes = download(url=url, sha1=sha1)
113 tar_io = _BytesIO(tar_bytes)
114 filename = os.path.basename(url)
115 mode = 'r:{}'.format(compression)
116 tar_file = tarfile.open(filename, mode, tar_io)
117 LOG.info('installing {} into {}'.format(url, install_directory))
118 os.makedirs(install_directory)
120 transform(tarinfo=tarinfo, strip_components=strip_components)
121 for tarinfo in tar_file]
123 path=install_directory,
124 members=[m for m in members if m is not None])
126 LOG.info('existing installation at {}'.format(install_directory))
129 def zip_install(url, sha1, install_directory):
130 """Download and install a zipped bundle"""
131 if not os.path.isdir(install_directory):
132 zip_bytes = download(url=url, sha1=sha1)
133 zip_io = _BytesIO(zip_bytes)
134 zip_file = zipfile.ZipFile(zip_io)
135 LOG.info('installing {} into {}'.format(url, install_directory))
136 os.makedirs(install_directory)
137 zip_file.extractall(install_directory)
139 LOG.info('existing installation at {}'.format(install_directory))
142 def install_nano(install_directory):
143 """Download and install the nano text editor"""
145 url='http://www.nano-editor.org/dist/v2.2/NT/nano-2.2.6.zip',
146 sha1='f5348208158157060de0a4df339401f36250fe5b',
147 install_directory=install_directory)
150 def install_nanorc(install_directory):
151 """Download and install nano syntax highlighting"""
153 url='http://www.nano-editor.org/dist/v2.2/nano-2.2.6.tar.gz',
154 sha1='f2a628394f8dda1b9f28c7e7b89ccb9a6dbd302a',
155 install_directory=install_directory,
157 home = os.path.expanduser('~')
158 nanorc = os.path.join(home, 'nano.rc')
159 if not os.path.isfile(nanorc):
160 syntax_dir = os.path.join(install_directory, 'doc', 'syntax')
161 LOG.info('include nanorc from {} in {}'.format(syntax_dir, nanorc))
162 with open3(nanorc, 'w', newline='\n') as f:
163 for filename in os.listdir(syntax_dir):
164 if filename.endswith('.nanorc'):
165 path = os.path.join(syntax_dir, filename)
166 rel_path = os.path.relpath(path, home)
167 include_path = make_posix_path(os.path.join('~', rel_path))
168 f.write('include {}\n'.format(include_path))
171 def install_sqlite(install_directory):
172 """Download and install the SQLite shell"""
174 url='https://sqlite.org/2014/sqlite-shell-win32-x86-3080403.zip',
175 sha1='1a8ab0ca9f4c51afeffeb49bd301e1d7f64741bb',
176 install_directory=install_directory)
179 def create_nosetests_entry_point(python_scripts_directory):
180 """Creates a terminal-based nosetests entry point for msysGit"""
181 contents = '\n'.join([
182 '#!/usr/bin/env/ python',
185 "if __name__ == '__main__':",
186 ' sys.exit(nose.core.main())',
189 if not os.path.isdir(python_scripts_directory):
190 os.makedirs(python_scripts_directory)
191 path = os.path.join(python_scripts_directory, 'nosetests')
192 LOG.info('create nosetests entrypoint {}'.format(path))
193 with open(path, 'w') as f:
197 def get_r_bin_directory():
198 """Locate the R bin directory (if R is installed
200 pf = os.environ.get('ProgramFiles', r'c:\ProgramFiles')
201 bin_glob = os.path.join(pf, 'R', 'R-[0-9]*.[0-9]*.[0-9]*', 'bin')
202 version_re = re.compile('^R-(\d+)[.](\d+)[.](\d+)$')
204 for path in glob.glob(bin_glob):
205 version_dir = os.path.basename(os.path.dirname(path))
206 version_match = version_re.match(version_dir)
208 paths[version_match.groups()] = path
210 LOG.info('no R installation found under {}'.format(pf))
212 LOG.debug('detected R installs:\n* {}'.format('\n* '.join([
213 v for k,v in sorted(paths.items())])))
214 version = sorted(paths.keys())[-1]
215 LOG.info('using R v{} bin directory at {}'.format(
216 '.'.join(version), paths[version]))
217 return paths[version]
220 def update_bash_profile(extra_paths=()):
221 """Create or append to a .bash_profile for Software Carpentry
223 Adds nano to the path, sets the default editor to nano, and adds
224 additional paths for other executables.
228 '# Add paths for Software-Carpentry-installed scripts and executables',
229 'export PATH=\"$PATH:{}\"'.format(':'.join(
230 make_posix_path(path) for path in extra_paths),),
232 '# Make nano the default editor',
233 'export EDITOR=nano',
236 config_path = os.path.join(os.path.expanduser('~'), '.bash_profile')
237 LOG.info('update bash profile at {}'.format(config_path))
238 LOG.debug('extra paths:\n* {}'.format('\n* '.join(extra_paths)))
239 with open(config_path, 'a') as f:
240 f.write('\n'.join(lines))
243 def make_posix_path(windows_path):
244 """Convert a Windows path to a posix path"""
246 (re.compile(r'\\'), '/'),
247 (re.compile('^[Cc]:'), '/c'),
249 windows_path = regex.sub(sub, windows_path)
254 swc_dir = os.path.join(os.path.expanduser('~'), '.swc')
255 bin_dir = os.path.join(swc_dir, 'bin')
256 nano_dir = os.path.join(swc_dir, 'lib', 'nano')
257 nanorc_dir = os.path.join(swc_dir, 'share', 'nanorc')
258 sqlite_dir = os.path.join(swc_dir, 'lib', 'sqlite')
259 create_nosetests_entry_point(python_scripts_directory=bin_dir)
260 install_nano(install_directory=nano_dir)
261 install_nanorc(install_directory=nanorc_dir)
262 install_sqlite(install_directory=sqlite_dir)
263 paths = [nano_dir, sqlite_dir, bin_dir]
264 r_dir = get_r_bin_directory()
267 update_bash_profile(extra_paths=paths)
270 if __name__ == '__main__':
273 parser = argparse.ArgumentParser(
275 formatter_class=argparse.RawDescriptionHelpFormatter)
278 choices=['critical', 'error', 'warning', 'info', 'debug'],
279 help='Verbosity (defaults to {!r})'.format(
280 logging.getLevelName(LOG.level).lower()))
282 args = parser.parse_args()
285 level = getattr(logging, args.verbose.upper())
288 LOG.info('Preparing your Software Carpentry awesomeness!')
290 LOG.info('Installation complete.')