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