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 }