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