1 /* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
2 file Copyright.txt or https://cmake.org/licensing for details. */
3
4 #include "cmFileInstaller.h"
5
6 #include <map>
7 #include <sstream>
8 #include <utility>
9
10 #include <cm/string_view>
11 #include <cmext/string_view>
12
13 #include "cm_sys_stat.h"
14
15 #include "cmExecutionStatus.h"
16 #include "cmFSPermissions.h"
17 #include "cmMakefile.h"
18 #include "cmStringAlgorithms.h"
19 #include "cmSystemTools.h"
20 #include "cmValue.h"
21
22 using namespace cmFSPermissions;
23
cmFileInstaller(cmExecutionStatus & status)24 cmFileInstaller::cmFileInstaller(cmExecutionStatus& status)
25 : cmFileCopier(status, "INSTALL")
26 , InstallType(cmInstallType_FILES)
27 , InstallMode(cmInstallMode::COPY)
28 , Optional(false)
29 , MessageAlways(false)
30 , MessageLazy(false)
31 , MessageNever(false)
32 , DestDirLength(0)
33 {
34 // Installation does not use source permissions by default.
35 this->UseSourcePermissions = false;
36 // Check whether to copy files always or only if they have changed.
37 std::string install_always;
38 if (cmSystemTools::GetEnv("CMAKE_INSTALL_ALWAYS", install_always)) {
39 this->Always = cmIsOn(install_always);
40 }
41 // Get the current manifest.
42 this->Manifest =
43 this->Makefile->GetSafeDefinition("CMAKE_INSTALL_MANIFEST_FILES");
44 }
~cmFileInstaller()45 cmFileInstaller::~cmFileInstaller()
46 {
47 // Save the updated install manifest.
48 this->Makefile->AddDefinition("CMAKE_INSTALL_MANIFEST_FILES",
49 this->Manifest);
50 }
51
ManifestAppend(std::string const & file)52 void cmFileInstaller::ManifestAppend(std::string const& file)
53 {
54 if (!this->Manifest.empty()) {
55 this->Manifest += ";";
56 }
57 this->Manifest += file.substr(this->DestDirLength);
58 }
59
ToName(std::string const & fromName)60 std::string const& cmFileInstaller::ToName(std::string const& fromName)
61 {
62 return this->Rename.empty() ? fromName : this->Rename;
63 }
64
ReportCopy(const std::string & toFile,Type type,bool copy)65 void cmFileInstaller::ReportCopy(const std::string& toFile, Type type,
66 bool copy)
67 {
68 if (!this->MessageNever && (copy || !this->MessageLazy)) {
69 std::string message =
70 cmStrCat((copy ? "Installing: " : "Up-to-date: "), toFile);
71 this->Makefile->DisplayStatus(message, -1);
72 }
73 if (type != TypeDir) {
74 // Add the file to the manifest.
75 this->ManifestAppend(toFile);
76 }
77 }
ReportMissing(const std::string & fromFile)78 bool cmFileInstaller::ReportMissing(const std::string& fromFile)
79 {
80 return (this->Optional || this->cmFileCopier::ReportMissing(fromFile));
81 }
Install(const std::string & fromFile,const std::string & toFile)82 bool cmFileInstaller::Install(const std::string& fromFile,
83 const std::string& toFile)
84 {
85 // Support installing from empty source to make a directory.
86 if (this->InstallType == cmInstallType_DIRECTORY && fromFile.empty()) {
87 return this->InstallDirectory(fromFile, toFile, MatchProperties());
88 }
89 return this->cmFileCopier::Install(fromFile, toFile);
90 }
91
InstallFile(const std::string & fromFile,const std::string & toFile,MatchProperties match_properties)92 bool cmFileInstaller::InstallFile(const std::string& fromFile,
93 const std::string& toFile,
94 MatchProperties match_properties)
95 {
96 if (this->InstallMode == cmInstallMode::COPY) {
97 return this->cmFileCopier::InstallFile(fromFile, toFile, match_properties);
98 }
99
100 std::string newFromFile;
101
102 if (this->InstallMode == cmInstallMode::REL_SYMLINK ||
103 this->InstallMode == cmInstallMode::REL_SYMLINK_OR_COPY ||
104 this->InstallMode == cmInstallMode::SYMLINK ||
105 this->InstallMode == cmInstallMode::SYMLINK_OR_COPY) {
106 // Try to get a relative path.
107 std::string toDir = cmSystemTools::GetParentDirectory(toFile);
108 newFromFile = cmSystemTools::ForceToRelativePath(toDir, fromFile);
109
110 // Double check that we can restore the original path.
111 std::string reassembled =
112 cmSystemTools::CollapseFullPath(newFromFile, toDir);
113 if (!cmSystemTools::ComparePath(reassembled, fromFile)) {
114 if (this->InstallMode == cmInstallMode::SYMLINK ||
115 this->InstallMode == cmInstallMode::SYMLINK_OR_COPY) {
116 // User does not mind, silently proceed with absolute path.
117 newFromFile = fromFile;
118 } else if (this->InstallMode == cmInstallMode::REL_SYMLINK_OR_COPY) {
119 // User expects a relative symbolic link or a copy.
120 // Since an absolute symlink won't do, copy instead.
121 return this->cmFileCopier::InstallFile(fromFile, toFile,
122 match_properties);
123 } else {
124 // We cannot meet user's expectation (REL_SYMLINK)
125 auto e = cmStrCat(this->Name,
126 " cannot determine relative path for symlink to \"",
127 newFromFile, "\" at \"", toFile, "\".");
128 this->Status.SetError(e);
129 return false;
130 }
131 }
132 } else {
133 newFromFile = fromFile; // stick with absolute path
134 }
135
136 // Compare the symlink value to that at the destination if not
137 // always installing.
138 bool copy = true;
139 if (!this->Always) {
140 std::string oldSymlinkTarget;
141 if (cmSystemTools::ReadSymlink(toFile, oldSymlinkTarget)) {
142 if (newFromFile == oldSymlinkTarget) {
143 copy = false;
144 }
145 }
146 }
147
148 // Inform the user about this file installation.
149 this->ReportCopy(toFile, TypeLink, copy);
150
151 if (copy) {
152 // Remove the destination file so we can always create the symlink.
153 cmSystemTools::RemoveFile(toFile);
154
155 // Create destination directory if it doesn't exist
156 cmSystemTools::MakeDirectory(cmSystemTools::GetFilenamePath(toFile));
157
158 // Create the symlink.
159 if (!cmSystemTools::CreateSymlink(newFromFile, toFile)) {
160 if (this->InstallMode == cmInstallMode::ABS_SYMLINK_OR_COPY ||
161 this->InstallMode == cmInstallMode::REL_SYMLINK_OR_COPY ||
162 this->InstallMode == cmInstallMode::SYMLINK_OR_COPY) {
163 // Failed to create a symbolic link, fall back to copying.
164 return this->cmFileCopier::InstallFile(newFromFile, toFile,
165 match_properties);
166 }
167
168 auto e = cmStrCat(this->Name, " cannot create symlink to \"",
169 newFromFile, "\" at \"", toFile,
170 "\": ", cmSystemTools::GetLastSystemError(), "\".");
171 this->Status.SetError(e);
172 return false;
173 }
174 }
175
176 return true;
177 }
178
DefaultFilePermissions()179 void cmFileInstaller::DefaultFilePermissions()
180 {
181 this->cmFileCopier::DefaultFilePermissions();
182 // Add execute permissions based on the target type.
183 switch (this->InstallType) {
184 case cmInstallType_SHARED_LIBRARY:
185 case cmInstallType_MODULE_LIBRARY:
186 if (this->Makefile->IsOn("CMAKE_INSTALL_SO_NO_EXE")) {
187 break;
188 }
189 CM_FALLTHROUGH;
190 case cmInstallType_EXECUTABLE:
191 case cmInstallType_PROGRAMS:
192 this->FilePermissions |= mode_owner_execute;
193 this->FilePermissions |= mode_group_execute;
194 this->FilePermissions |= mode_world_execute;
195 break;
196 default:
197 break;
198 }
199 }
200
Parse(std::vector<std::string> const & args)201 bool cmFileInstaller::Parse(std::vector<std::string> const& args)
202 {
203 if (!this->cmFileCopier::Parse(args)) {
204 return false;
205 }
206
207 if (!this->Rename.empty()) {
208 if (!this->FilesFromDir.empty()) {
209 this->Status.SetError("INSTALL option RENAME may not be "
210 "combined with FILES_FROM_DIR.");
211 return false;
212 }
213 if (this->InstallType != cmInstallType_FILES &&
214 this->InstallType != cmInstallType_PROGRAMS) {
215 this->Status.SetError("INSTALL option RENAME may be used "
216 "only with FILES or PROGRAMS.");
217 return false;
218 }
219 if (this->Files.size() > 1) {
220 this->Status.SetError("INSTALL option RENAME may be used "
221 "only with one file.");
222 return false;
223 }
224 }
225
226 if (!this->HandleInstallDestination()) {
227 return false;
228 }
229
230 if (((this->MessageAlways ? 1 : 0) + (this->MessageLazy ? 1 : 0) +
231 (this->MessageNever ? 1 : 0)) > 1) {
232 this->Status.SetError("INSTALL options MESSAGE_ALWAYS, "
233 "MESSAGE_LAZY, and MESSAGE_NEVER "
234 "are mutually exclusive.");
235 return false;
236 }
237
238 static const std::map<cm::string_view, cmInstallMode> install_mode_dict{
239 { "ABS_SYMLINK"_s, cmInstallMode::ABS_SYMLINK },
240 { "ABS_SYMLINK_OR_COPY"_s, cmInstallMode::ABS_SYMLINK_OR_COPY },
241 { "REL_SYMLINK"_s, cmInstallMode::REL_SYMLINK },
242 { "REL_SYMLINK_OR_COPY"_s, cmInstallMode::REL_SYMLINK_OR_COPY },
243 { "SYMLINK"_s, cmInstallMode::SYMLINK },
244 { "SYMLINK_OR_COPY"_s, cmInstallMode::SYMLINK_OR_COPY }
245 };
246
247 std::string install_mode;
248 cmSystemTools::GetEnv("CMAKE_INSTALL_MODE", install_mode);
249 if (install_mode.empty() || install_mode == "COPY"_s) {
250 this->InstallMode = cmInstallMode::COPY;
251 } else {
252 auto it = install_mode_dict.find(install_mode);
253 if (it != install_mode_dict.end()) {
254 this->InstallMode = it->second;
255 } else {
256 auto e = cmStrCat("Unrecognized value '", install_mode,
257 "' for environment variable CMAKE_INSTALL_MODE");
258 this->Status.SetError(e);
259 return false;
260 }
261 }
262
263 return true;
264 }
265
CheckKeyword(std::string const & arg)266 bool cmFileInstaller::CheckKeyword(std::string const& arg)
267 {
268 if (arg == "TYPE") {
269 if (this->CurrentMatchRule) {
270 this->NotAfterMatch(arg);
271 } else {
272 this->Doing = DoingType;
273 }
274 } else if (arg == "FILES") {
275 if (this->CurrentMatchRule) {
276 this->NotAfterMatch(arg);
277 } else {
278 this->Doing = DoingFiles;
279 }
280 } else if (arg == "RENAME") {
281 if (this->CurrentMatchRule) {
282 this->NotAfterMatch(arg);
283 } else {
284 this->Doing = DoingRename;
285 }
286 } else if (arg == "OPTIONAL") {
287 if (this->CurrentMatchRule) {
288 this->NotAfterMatch(arg);
289 } else {
290 this->Doing = DoingNone;
291 this->Optional = true;
292 }
293 } else if (arg == "MESSAGE_ALWAYS") {
294 if (this->CurrentMatchRule) {
295 this->NotAfterMatch(arg);
296 } else {
297 this->Doing = DoingNone;
298 this->MessageAlways = true;
299 }
300 } else if (arg == "MESSAGE_LAZY") {
301 if (this->CurrentMatchRule) {
302 this->NotAfterMatch(arg);
303 } else {
304 this->Doing = DoingNone;
305 this->MessageLazy = true;
306 }
307 } else if (arg == "MESSAGE_NEVER") {
308 if (this->CurrentMatchRule) {
309 this->NotAfterMatch(arg);
310 } else {
311 this->Doing = DoingNone;
312 this->MessageNever = true;
313 }
314 } else if (arg == "PERMISSIONS") {
315 if (this->CurrentMatchRule) {
316 this->Doing = DoingPermissionsMatch;
317 } else {
318 // file(INSTALL) aliases PERMISSIONS to FILE_PERMISSIONS
319 this->Doing = DoingPermissionsFile;
320 this->UseGivenPermissionsFile = true;
321 }
322 } else if (arg == "DIR_PERMISSIONS") {
323 if (this->CurrentMatchRule) {
324 this->NotAfterMatch(arg);
325 } else {
326 // file(INSTALL) aliases DIR_PERMISSIONS to DIRECTORY_PERMISSIONS
327 this->Doing = DoingPermissionsDir;
328 this->UseGivenPermissionsDir = true;
329 }
330 } else if (arg == "COMPONENTS" || arg == "CONFIGURATIONS" ||
331 arg == "PROPERTIES") {
332 std::ostringstream e;
333 e << "INSTALL called with old-style " << arg << " argument. "
334 << "This script was generated with an older version of CMake. "
335 << "Re-run this cmake version on your build tree.";
336 this->Status.SetError(e.str());
337 this->Doing = DoingError;
338 } else {
339 return this->cmFileCopier::CheckKeyword(arg);
340 }
341 return true;
342 }
343
CheckValue(std::string const & arg)344 bool cmFileInstaller::CheckValue(std::string const& arg)
345 {
346 switch (this->Doing) {
347 case DoingType:
348 if (!this->GetTargetTypeFromString(arg)) {
349 this->Doing = DoingError;
350 }
351 break;
352 case DoingRename:
353 this->Rename = arg;
354 break;
355 default:
356 return this->cmFileCopier::CheckValue(arg);
357 }
358 return true;
359 }
360
GetTargetTypeFromString(const std::string & stype)361 bool cmFileInstaller::GetTargetTypeFromString(const std::string& stype)
362 {
363 if (stype == "EXECUTABLE") {
364 this->InstallType = cmInstallType_EXECUTABLE;
365 } else if (stype == "FILE") {
366 this->InstallType = cmInstallType_FILES;
367 } else if (stype == "PROGRAM") {
368 this->InstallType = cmInstallType_PROGRAMS;
369 } else if (stype == "STATIC_LIBRARY") {
370 this->InstallType = cmInstallType_STATIC_LIBRARY;
371 } else if (stype == "SHARED_LIBRARY") {
372 this->InstallType = cmInstallType_SHARED_LIBRARY;
373 } else if (stype == "MODULE") {
374 this->InstallType = cmInstallType_MODULE_LIBRARY;
375 } else if (stype == "DIRECTORY") {
376 this->InstallType = cmInstallType_DIRECTORY;
377 } else {
378 std::ostringstream e;
379 e << "Option TYPE given unknown value \"" << stype << "\".";
380 this->Status.SetError(e.str());
381 return false;
382 }
383 return true;
384 }
385
HandleInstallDestination()386 bool cmFileInstaller::HandleInstallDestination()
387 {
388 std::string& destination = this->Destination;
389
390 // allow for / to be a valid destination
391 if (destination.size() < 2 && destination != "/") {
392 this->Status.SetError("called with inappropriate arguments. "
393 "No DESTINATION provided or .");
394 return false;
395 }
396
397 std::string sdestdir;
398 if (cmSystemTools::GetEnv("DESTDIR", sdestdir) && !sdestdir.empty()) {
399 cmSystemTools::ConvertToUnixSlashes(sdestdir);
400 char ch1 = destination[0];
401 char ch2 = destination[1];
402 char ch3 = 0;
403 if (destination.size() > 2) {
404 ch3 = destination[2];
405 }
406 int skip = 0;
407 if (ch1 != '/') {
408 int relative = 0;
409 if (((ch1 >= 'a' && ch1 <= 'z') || (ch1 >= 'A' && ch1 <= 'Z')) &&
410 ch2 == ':') {
411 // Assume windows
412 // let's do some destdir magic:
413 skip = 2;
414 if (ch3 != '/') {
415 relative = 1;
416 }
417 } else {
418 relative = 1;
419 }
420 if (relative) {
421 // This is relative path on unix or windows. Since we are doing
422 // destdir, this case does not make sense.
423 this->Status.SetError(
424 "called with relative DESTINATION. This "
425 "does not make sense when using DESTDIR. Specify "
426 "absolute path or remove DESTDIR environment variable.");
427 return false;
428 }
429 } else {
430 if (ch2 == '/') {
431 // looks like a network path.
432 std::string message =
433 cmStrCat("called with network path DESTINATION. This "
434 "does not make sense when using DESTDIR. Specify local "
435 "absolute path or remove DESTDIR environment variable."
436 "\nDESTINATION=\n",
437 destination);
438 this->Status.SetError(message);
439 return false;
440 }
441 }
442 destination = sdestdir + destination.substr(skip);
443 this->DestDirLength = int(sdestdir.size());
444 }
445
446 // check if default dir creation permissions were set
447 mode_t default_dir_mode_v = 0;
448 mode_t* default_dir_mode = &default_dir_mode_v;
449 if (!this->GetDefaultDirectoryPermissions(&default_dir_mode)) {
450 return false;
451 }
452
453 if (this->InstallType != cmInstallType_DIRECTORY) {
454 if (!cmSystemTools::FileExists(destination)) {
455 if (!cmSystemTools::MakeDirectory(destination, default_dir_mode)) {
456 std::string errstring = "cannot create directory: " + destination +
457 ". Maybe need administrative privileges.";
458 this->Status.SetError(errstring);
459 return false;
460 }
461 }
462 if (!cmSystemTools::FileIsDirectory(destination)) {
463 std::string errstring =
464 "INSTALL destination: " + destination + " is not a directory.";
465 this->Status.SetError(errstring);
466 return false;
467 }
468 }
469 return true;
470 }
471