9af86e797d1dc00ce73ea40b7d95899ae8a01a40
[swc-setup-windows-installer.git] / swc-windows-installer.py
1 #!/usr/bin/env python
2
3 """Software Carpentry Windows Installer
4
5 Helps mimic a *nix environment on Windows with as little work as possible.
6
7 The script:
8 * Installs nano and makes it accessible from msysgit
9 * Installs sqlite3 and makes it accessible from msysGit
10 * Creates ~/nano.rc with links to syntax highlighting configs
11 * Provides standard nosetests behavior for msysgit
12
13 To use:
14
15 1. Install Python, IPython, and Nose.  An easy way to do this is with
16    the Anaconda CE Python distribution
17    http://continuum.io/anacondace.html
18 2. Install msysgit
19    http://code.google.com/p/msysgit/downloads/list
20 3. Run swc_windows_installer.py
21    You should be able to simply double click the file in Windows
22
23 """
24
25 import hashlib
26 try:  # Python 3
27     from io import BytesIO as _BytesIO
28 except ImportError:  # Python 2
29     from StringIO import StringIO as _BytesIO
30 import os
31 import re
32 import sys
33 import tarfile
34 try:  # Python 3
35     from urllib.request import urlopen as _urlopen
36 except ImportError:  # Python 2
37     from urllib2 import urlopen as _urlopen
38 import zipfile
39
40
41 if sys.version_info >= (3, 0):  # Python 3
42     open3 = open
43 else:
44     def open3(file, mode='r', newline=None):
45         if newline:
46             if newline != '\n':
47                 raise NotImplementedError(newline)
48             f = open(file, mode + 'b')
49         else:
50             f = open(file, mode)
51         return f
52
53
54 def download(url, sha1):
55     """Download a file and verify it's hash"""
56     r = _urlopen(url)
57     byte_content = r.read()
58     download_sha1 = hashlib.sha1(byte_content).hexdigest()
59     if download_sha1 != sha1:
60         raise ValueError(
61             'downloaded {!r} has the wrong SHA1 hash: {} != {}'.format(
62                 url, download_sha1, sha1))
63     return byte_content
64
65
66 def splitall(path):
67     """Split a path into a list of components
68
69     >>> splitall('nano-2.2.6/doc/Makefile.am')
70     ['nano-2.2.6', 'doc', 'Makefile.am']
71     """
72     parts = []
73     while True:
74         head, tail = os.path.split(path)
75         if tail:
76             parts.insert(0, tail)
77         elif head:
78             parts.insert(0, head)
79             break
80         else:
81             break
82         path = head
83     return parts
84
85
86 def transform(tarinfo, strip_components=0):
87     """Transform TarInfo objects for extraction"""
88     path_components = splitall(tarinfo.name)
89     try:
90         tarinfo.name = os.path.join(*path_components[strip_components:])
91     except TypeError:
92         if len(path_components) <= strip_components:
93             return None
94         raise
95     return tarinfo
96
97
98 def tar_install(url, sha1, install_directory, compression='*',
99                 strip_components=0):
100     """Download and install a tar bundle"""
101     if not os.path.isdir(install_directory):
102         tar_bytes = download(url=url, sha1=sha1)
103         tar_io = _BytesIO(tar_bytes)
104         filename = os.path.basename(url)
105         mode = 'r:{}'.format(compression)
106         tar_file = tarfile.open(filename, mode, tar_io)
107         os.makedirs(install_directory)
108         members = [
109             transform(tarinfo=tarinfo, strip_components=strip_components)
110             for tarinfo in tar_file]
111         tar_file.extractall(
112             path=install_directory,
113             members=[m for m in members if m is not None])
114
115
116 def zip_install(url, sha1, install_directory):
117     """Download and install a zipped bundle"""
118     if not os.path.isdir(install_directory):
119         zip_bytes = download(url=url, sha1=sha1)
120         zip_io = _BytesIO(zip_bytes)
121         zip_file = zipfile.ZipFile(zip_io)
122         os.makedirs(install_directory)
123         zip_file.extractall(install_directory)
124
125
126 def install_nano(install_directory):
127     """Download and install the nano text editor"""
128     zip_install(
129         url='http://www.nano-editor.org/dist/v2.2/NT/nano-2.2.6.zip',
130         sha1='f5348208158157060de0a4df339401f36250fe5b',
131         install_directory=install_directory)
132
133
134 def install_nanorc(install_directory):
135     """Download and install nano syntax highlighting"""
136     tar_install(
137         url='http://www.nano-editor.org/dist/v2.2/nano-2.2.6.tar.gz',
138         sha1='f2a628394f8dda1b9f28c7e7b89ccb9a6dbd302a',
139         install_directory=install_directory,
140         strip_components=1)
141     home = os.path.expanduser('~')
142     nanorc = os.path.join(home, 'nano.rc')
143     if not os.path.isfile(nanorc):
144         syntax_dir = os.path.join(install_directory, 'doc', 'syntax')
145         with open3(nanorc, 'w', newline='\n') as f:
146             for filename in os.listdir(syntax_dir):
147                 if filename.endswith('.nanorc'):
148                     path = os.path.join(syntax_dir, filename)
149                     rel_path = os.path.relpath(path, home)
150                     include_path = make_posix_path(os.path.join('~', rel_path))
151                     f.write('include {}\n'.format(include_path))
152
153
154 def install_sqlite(install_directory):
155     """Download and install the sqlite3 shell"""
156     zip_install(
157         url='https://sqlite.org/2014/sqlite-shell-win32-x86-3080403.zip',
158         sha1='1a8ab0ca9f4c51afeffeb49bd301e1d7f64741bb',
159         install_directory=install_directory)
160
161
162 def create_nosetests_entry_point(python_scripts_directory):
163     """Creates a terminal-based nosetests entry point for msysgit"""
164     contents = '\n'.join([
165             '#!/usr/bin/env/ python',
166             'import sys',
167             'import nose',
168             "if __name__ == '__main__':",
169             '    sys.exit(nose.core.main())',
170             '',
171             ])
172     if not os.path.isdir(python_scripts_directory):
173         os.makedirs(python_scripts_directory)
174     with open(os.path.join(python_scripts_directory, 'nosetests'), 'w') as f:
175         f.write(contents)
176
177
178 def update_bash_profile(extra_paths=()):
179     """Create or append to a .bash_profile for Software Carpentry
180
181     Adds nano to the path, sets the default editor to nano, and adds
182     additional paths for other executables.
183     """
184     lines = [
185         '',
186         '# Add paths for Software-Carpentry-installed scripts and executables',
187         'export PATH=\"$PATH:{}\"'.format(':'.join(
188             make_posix_path(path) for path in extra_paths),),
189         '',
190         '# Make nano the default editor',
191         'export EDITOR=nano',
192         '',
193         ]
194     config_path = os.path.join(os.path.expanduser('~'), '.bash_profile')
195     with open(config_path, 'a') as f:
196         f.write('\n'.join(lines))
197
198
199 def make_posix_path(windows_path):
200     """Convert a Windows path to a posix path"""
201     for regex, sub in [
202             (re.compile(r'\\'), '/'),
203             (re.compile('^[Cc]:'), '/c'),
204             ]:
205         windows_path = regex.sub(sub, windows_path)
206     return windows_path
207
208
209 def main():
210     swc_dir = os.path.join(os.path.expanduser('~'), '.swc')
211     bin_dir = os.path.join(swc_dir, 'bin')
212     nano_dir = os.path.join(swc_dir, 'lib', 'nano')
213     nanorc_dir = os.path.join(swc_dir, 'share', 'nanorc')
214     sqlite_dir = os.path.join(swc_dir, 'lib', 'sqlite')
215     create_nosetests_entry_point(python_scripts_directory=bin_dir)
216     install_nano(install_directory=nano_dir)
217     install_nanorc(install_directory=nanorc_dir)
218     install_sqlite(install_directory=sqlite_dir)
219     update_bash_profile(extra_paths=(nano_dir, sqlite_dir, bin_dir))
220
221
222 if __name__ == '__main__':
223     print("Preparing your Software Carpentry awesomeness!")
224     main()