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 ae.net.ietf.url;
17 import ae.sys.archive;
18 import ae.sys.file;
19 import ae.sys.net;
20 import ae.sys.persistence;
21 import ae.utils.meta;
22 
23 import std.algorithm;
24 import std.array;
25 import std.file;
26 import std.path;
27 import std.process : environment;
28 import std.string;
29 
30 class Installer
31 {
32 	/// Where all software will be unpacked
33 	/// (current directory, by default).
34 	static string installationDirectory = null;
35 
36 	/// Log sink
37 	static void delegate(string) logger;
38 
39 	static protected void log(string s)
40 	{
41 		if (logger) logger(s);
42 	}
43 
44 	/// Component name. Used for logging.
45 	@property string name() { return this.classinfo.name.split(".")[$-1]; }
46 
47 	/// The subdirectory where this component will be installed.
48 	@property string subdirectory() { return name.toLower(); }
49 
50 	/// Subdirectories (under the installation subdirectory)
51 	/// containing executables which need to be added to PATH.
52 	@property string[] binPaths() { return [""]; }
53 
54 	/// The full installation directory.
55 	@property string directory()
56 	{
57 		return buildPath(installationDirectory, subdirectory);
58 	}
59 
60 	/// The list of executable names required to be present.
61 	/// Null if this component is never considered already
62 	/// available on the system.
63 	@property string[] requiredExecutables() { return null; }
64 
65 	/*protected*/ static bool haveExecutable(string name)
66 	{
67 		version(Windows)
68 			enum executableSuffixes = [".exe", ".bat", ".cmd"];
69 		else
70 			enum executableSuffixes = [""];
71 
72 		foreach (entry; environment["PATH"].split(pathSeparator))
73 			foreach (suffix; executableSuffixes)
74 				if ((buildPath(entry, name) ~ suffix).exists)
75 					return true;
76 
77 		return false;
78 	}
79 
80 	/// Whether the component is installed locally.
81 	@property bool installedLocally()
82 	{
83 		// The directory should only be created atomically upon
84 		// the end of a successful installation, so an exists
85 		// check is sufficient.
86 		return directory.exists;
87 	}
88 
89 	/// Whether the component is already present on the system.
90 	@property bool availableOnSystem()
91 	{
92 		if (requiredExecutables is null)
93 			return false;
94 
95 		return requiredExecutables.all!haveExecutable();
96 	}
97 
98 	/// Whether the component is installed, locally or
99 	/// already present on the system.
100 	@property final bool available()
101 	{
102 		return installedLocally || availableOnSystem;
103 	}
104 
105 	/// Install this component if necessary.
106 	final void require()
107 	{
108 		if (!available)
109 			install();
110 
111 		assert(available);
112 
113 		if (installedLocally)
114 			addToPath();
115 	}
116 
117 	/// Install this component locally, if it isn't already installed.
118 	final void requireLocal(bool shouldAddToPath = true)
119 	{
120 		if (!installedLocally)
121 			install();
122 
123 		if (shouldAddToPath)
124 			addToPath();
125 	}
126 
127 	bool addedToPath;
128 
129 	void addToPath()
130 	{
131 		if (addedToPath)
132 			return;
133 		foreach (binPath; binPaths)
134 		{
135 			auto path = buildPath(directory, binPath).absolutePath();
136 			log("Adding " ~ path ~ " to PATH.");
137 			// Override any system installations
138 			environment["PATH"] = path ~ pathSeparator ~ environment["PATH"];
139 		}
140 		addedToPath = true;
141 	}
142 
143 	private void install()
144 	{
145 		mkdirRecurse(installationDirectory);
146 
147 		log("Installing " ~ name ~ " to " ~ directory ~ "...");
148 
149 		void installProxy(string target) { installImpl(target); }
150 		safeUpdate!installProxy(directory);
151 
152 		log("Done installing " ~ name ~ ".");
153 	}
154 
155 	protected void installImpl(string target)
156 	{
157 		uninstallable();
158 	}
159 
160 	// ----------------------------------------------------
161 
162 final:
163 protected:
164 	static string saveLocation(string url)
165 	{
166 		return buildPath(installationDirectory, url.fileNameFromURL());
167 	}
168 
169 	template cachedAction(alias fun, string fmt)
170 	{
171 		static void cachedAction(Args...)(Args args, string target)
172 		{
173 			if (target.exists)
174 				return;
175 			log(fmt.format(args, target));
176 			atomic!fun(args, target);
177 		}
178 	}
179 
180 	alias saveTo = cachedAction!(downloadFile, "Downloading %s to %s...");
181 	alias save = withTarget!(saveLocation, saveTo);
182 
183 	static auto saveAs(string url, string fn)
184 	{
185 		auto target = buildPath(installationDirectory, fn);
186 		ensurePathExists(target);
187 		url.I!saveTo(target);
188 		return target;
189 	}
190 
191 	alias unpackTo = cachedAction!(ae.sys.archive.unpack, "Unpacking %s to %s...");
192 	alias unpack = withTarget!(stripExtension, unpackTo);
193 
194 	static string resolveRedirectImpl(string url)
195 	{
196 		return net.resolveRedirect(url);
197 	}
198 	static string resolveRedirect(string url)
199 	{
200 		alias P = PersistentMemoized!(resolveRedirectImpl, FlushPolicy.atThreadExit);
201 		static P* p;
202 		if (!p)
203 			p = new P(buildPath(installationDirectory, "redirects.json"));
204 		return (*p)(url);
205 	}
206 
207 	void windowsOnly()
208 	{
209 		version(Windows)
210 			return;
211 		else
212 		{
213 			log(name ~ " is not installable on this platform.");
214 			uninstallable();
215 		}
216 	}
217 
218 	void uninstallable()
219 	{
220 		throw new Exception("Please install " ~ name ~ " and make sure it is on your PATH.");
221 	}
222 }