1 /**
2  * ae.sys.install.common
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.sys.install.common;
15 
16 import std.algorithm;
17 import std.array;
18 import std.exception;
19 import std.file;
20 import std.path;
21 import std.process : environment;
22 import std.string;
23 
24 import ae.net.ietf.url;
25 import ae.sys.archive;
26 import ae.sys.file;
27 import ae.sys.net;
28 import ae.sys.persistence;
29 import ae.utils.meta;
30 import ae.utils.path;
31 
32 class Installer
33 {
34 	/// Where all software will be unpacked
35 	/// (current directory, by default).
36 	static string installationDirectory = null;
37 
38 	/// Log sink
39 	static void delegate(string) logger;
40 
41 	static protected void log(string s)
42 	{
43 		if (logger) logger(s);
44 	}
45 
46 	/// Component name. Used for logging.
47 	@property string name() { return this.classinfo.name.split(".")[$-1].chomp("Installer"); }
48 
49 	/// The subdirectory where this component will be installed.
50 	@property string subdirectory() { return name.toLower(); }
51 
52 	/// Subdirectories (under the installation subdirectory)
53 	/// containing executables which need to be added to PATH.
54 	@property string[] binPaths() { return [""]; }
55 
56 	/// As above, but expanded to full absolute directory paths.
57 	@property final string[] binDirs() { return binPaths.map!(binPath => buildPath(directory, binPath)).array; }
58 
59 	deprecated("Please use ae.utils.path.pathDirs")
60 	@property static string[] pathDirs() { return ae.utils.path.pathDirs; }
61 
62 	/// The full installation directory.
63 	@property final string directory()
64 	{
65 		return buildPath(installationDirectory, subdirectory);
66 	}
67 
68 	/// The list of executable names required to be present.
69 	/// Null if this component is never considered already
70 	/// available on the system.
71 	@property string[] requiredExecutables() { return null; }
72 
73 	deprecated("Please use ae.utils.path.haveExecutable")
74 	/*protected*/ static bool haveExecutable(string name) { return ae.utils.path.haveExecutable(name); }
75 
76 	deprecated("Please use ae.utils.path.findExecutable")
77 	static string findExecutable(string name, string[] dirs) { return ae.utils.path.findExecutable(name, dirs); }
78 
79 	/// Get the full path to an executable.
80 	string exePath(string name)
81 	{
82 		return .findExecutable(name, installedLocally ? binDirs : .pathDirs);
83 	}
84 
85 	/// Whether the component is installed locally.
86 	@property bool installedLocally()
87 	{
88 		// The directory should only be created atomically upon
89 		// the end of a successful installation, so an exists
90 		// check is sufficient.
91 		return directory.exists;
92 	}
93 
94 	/// Whether the component is already present on the system.
95 	@property bool availableOnSystem()
96 	{
97 		if (requiredExecutables is null)
98 			return false;
99 
100 		return requiredExecutables.all!(.haveExecutable)();
101 	}
102 
103 	/// Whether the component is installed, locally or
104 	/// already present on the system.
105 	@property final bool available()
106 	{
107 		return installedLocally || availableOnSystem;
108 	}
109 
110 	/// Install this component if necessary.
111 	final void require()
112 	{
113 		if (!available)
114 			install();
115 
116 		assert(available);
117 
118 		if (installedLocally)
119 			addToPath();
120 	}
121 
122 	/// Install this component locally, if it isn't already installed.
123 	final void requireLocal(bool shouldAddToPath = true)
124 	{
125 		if (!installedLocally)
126 			install();
127 
128 		if (shouldAddToPath)
129 			addToPath();
130 	}
131 
132 	bool addedToPath;
133 
134 	void addToPath()
135 	{
136 		if (addedToPath)
137 			return;
138 		foreach (binPath; binPaths)
139 		{
140 			auto path = buildPath(directory, binPath).absolutePath();
141 			log("Adding " ~ path ~ " to PATH.");
142 			// Override any system installations
143 			environment["PATH"] = path ~ pathSeparator ~ environment["PATH"];
144 		}
145 		addedToPath = true;
146 	}
147 
148 	private void install()
149 	{
150 		mkdirRecurse(installationDirectory);
151 
152 		log("Installing " ~ name ~ " to " ~ directory ~ "...");
153 		atomicInstallImpl();
154 		log("Done installing " ~ name ~ ".");
155 	}
156 
157 	/// Install to `directory` atomically.
158 	protected void atomicInstallImpl()
159 	{
160 		void installProxy(string target) { installImpl(target); }
161 		atomic!installProxy(directory);
162 	}
163 
164 	protected void installImpl(string target)
165 	{
166 		uninstallable();
167 	}
168 
169 	// ----------------------------------------------------
170 
171 final:
172 //protected: // https://issues.dlang.org/show_bug.cgi?id=14528
173 	static string saveLocation(string url)
174 	{
175 		return buildPath(installationDirectory, url.fileNameFromURL());
176 	}
177 
178 	template cachedAction(alias fun, string fmt)
179 	{
180 		static void cachedAction(Args...)(Args args, string target)
181 		{
182 			if (target.exists)
183 				return;
184 			log(fmt.format(args, target));
185 			atomic!fun(args, target);
186 		}
187 	}
188 
189 	static string[string] urlDigests;
190 
191 	static void saveFile(string url, string target)
192 	{
193 		downloadFile(url, target);
194 		auto pDigest = url in urlDigests;
195 		if (pDigest)
196 		{
197 			auto digest = *pDigest;
198 			if (!digest)
199 				return;
200 			log("Verifying " ~ target.baseName() ~ "...");
201 
202 			import std.digest.sha, std.digest, std.stdio;
203 			SHA1 sha;
204 			sha.start();
205 			foreach (chunk; File(target, "rb").byChunk(0x10000))
206 				sha.put(chunk[]);
207 			auto hash = sha.finish();
208 			enforce(hash.toHexString!(LetterCase.lower) == digest, "Could not verify integrity of " ~ target);
209 		}
210 		else
211 			log("WARNING: Not verifying integrity of " ~ url ~ ".");
212 	}
213 
214 	alias saveTo = cachedAction!(saveFile, "Downloading %s to %s...");
215 	alias save = withTarget!(saveLocation, saveTo);
216 
217 	static auto saveAs(string url, string fn)
218 	{
219 		auto target = buildPath(installationDirectory, fn);
220 		ensurePathExists(target);
221 		url.I!saveTo(target);
222 		return target;
223 	}
224 
225 	static string stripArchiveExtension(string fn)
226 	{
227 		fn = fn.stripExtension();
228 		if (fn.extension == ".tar")
229 			fn = fn.stripExtension();
230 		return fn;
231 	}
232 
233 	alias unpackTo = cachedAction!(ae.sys.archive.unpack, "Unpacking %s to %s...");
234 	alias unpack = withTarget!(stripArchiveExtension, unpackTo);
235 
236 	static string resolveRedirectImpl(string url)
237 	{
238 		return net.resolveRedirect(url);
239 	}
240 	static string resolveRedirect(string url)
241 	{
242 		alias P = PersistentMemoized!(resolveRedirectImpl, FlushPolicy.atThreadExit);
243 		static P* p;
244 		if (!p)
245 			p = new P(buildPath(installationDirectory, "redirects.json"));
246 		return (*p)(url);
247 	}
248 
249 	void windowsOnly()
250 	{
251 		version(Windows)
252 			return;
253 		else
254 		{
255 			log(name ~ " is not installable on this platform.");
256 			uninstallable();
257 		}
258 	}
259 
260 	void uninstallable()
261 	{
262 		throw new Exception("Please install " ~ name ~ " and make sure it is on your PATH.");
263 	}
264 }
265 
266 /// Move a directory and its contents into another directory recursively,
267 /// overwriting any existing files.
268 package void moveInto(string source, string target)
269 {
270 	foreach (de; source.dirEntries(SpanMode.shallow))
271 	{
272 		auto targetPath = target.buildPath(de.baseName);
273 		if (de.isDir && targetPath.exists)
274 			de.moveInto(targetPath);
275 		else
276 			de.name.rename(targetPath);
277 	}
278 	source.rmdir();
279 }
280 
281 /// As above, but do not leave behind partially-merged
282 /// directories. In case of failure, both source and target
283 /// are deleted.
284 package void atomicMoveInto(string source, string target)
285 {
286 	auto tmpSource = source ~ ".tmp";
287 	auto tmpTarget = target ~ ".tmp";
288 	if (tmpSource.exists) tmpSource.rmdirRecurse();
289 	if (tmpTarget.exists) tmpTarget.rmdirRecurse();
290 	source.rename(tmpSource);
291 	target.rename(tmpTarget);
292 	{
293 		scope(failure) tmpSource.rmdirRecurse();
294 		scope(failure) tmpTarget.rmdirRecurse();
295 		tmpSource.moveInto(tmpTarget);
296 	}
297 	tmpTarget.rename(target);
298 }