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