1 /* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
2    file Copyright.txt or https://cmake.org/licensing for details.  */
3 #include "cmFindProgramCommand.h"
4 
5 #include <algorithm>
6 #include <string>
7 #include <utility>
8 
9 #include "cmMakefile.h"
10 #include "cmMessageType.h"
11 #include "cmPolicies.h"
12 #include "cmStateTypes.h"
13 #include "cmStringAlgorithms.h"
14 #include "cmSystemTools.h"
15 
16 class cmExecutionStatus;
17 
18 #if defined(__APPLE__)
19 #  include <CoreFoundation/CoreFoundation.h>
20 #endif
21 
22 struct cmFindProgramHelper
23 {
cmFindProgramHelpercmFindProgramHelper24   cmFindProgramHelper(std::string debugName, cmMakefile* makefile,
25                       cmFindBase const* base)
26     : DebugSearches(std::move(debugName), base)
27     , Makefile(makefile)
28     , PolicyCMP0109(makefile->GetPolicyStatus(cmPolicies::CMP0109))
29   {
30 #if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__)
31     // Consider platform-specific extensions.
32     this->Extensions.push_back(".com");
33     this->Extensions.push_back(".exe");
34 #endif
35     // Consider original name with no extensions.
36     this->Extensions.emplace_back();
37   }
38 
39   // List of valid extensions.
40   std::vector<std::string> Extensions;
41 
42   // Keep track of the best program file found so far.
43   std::string BestPath;
44 
45   // Current names under consideration.
46   std::vector<std::string> Names;
47 
48   // Current name with extension under consideration.
49   std::string TestNameExt;
50 
51   // Current full path under consideration.
52   std::string TestPath;
53 
54   // Debug state
55   cmFindBaseDebugState DebugSearches;
56   cmMakefile* Makefile;
57 
58   cmPolicies::PolicyStatus PolicyCMP0109;
59 
AddNamecmFindProgramHelper60   void AddName(std::string const& name) { this->Names.push_back(name); }
SetNamecmFindProgramHelper61   void SetName(std::string const& name)
62   {
63     this->Names.clear();
64     this->AddName(name);
65   }
CheckCompoundNamescmFindProgramHelper66   bool CheckCompoundNames()
67   {
68     return std::any_of(this->Names.begin(), this->Names.end(),
69                        [this](std::string const& n) -> bool {
70                          // Only perform search relative to current directory
71                          // if the file name contains a directory separator.
72                          return n.find('/') != std::string::npos &&
73                            this->CheckDirectoryForName("", n);
74                        });
75   }
CheckDirectorycmFindProgramHelper76   bool CheckDirectory(std::string const& path)
77   {
78     return std::any_of(this->Names.begin(), this->Names.end(),
79                        [this, &path](std::string const& n) -> bool {
80                          // Only perform search relative to current directory
81                          // if the file name contains a directory separator.
82                          return this->CheckDirectoryForName(path, n);
83                        });
84   }
CheckDirectoryForNamecmFindProgramHelper85   bool CheckDirectoryForName(std::string const& path, std::string const& name)
86   {
87     return std::any_of(this->Extensions.begin(), this->Extensions.end(),
88                        [this, &path, &name](std::string const& ext) -> bool {
89                          if (!ext.empty() && cmHasSuffix(name, ext)) {
90                            return false;
91                          }
92                          this->TestNameExt = cmStrCat(name, ext);
93                          this->TestPath = cmSystemTools::CollapseFullPath(
94                            this->TestNameExt, path);
95                          bool exists = this->FileIsExecutable(this->TestPath);
96                          exists ? this->DebugSearches.FoundAt(this->TestPath)
97                                 : this->DebugSearches.FailedAt(this->TestPath);
98                          if (exists) {
99                            this->BestPath = this->TestPath;
100                            return true;
101                          }
102                          return false;
103                        });
104   }
FileIsExecutablecmFindProgramHelper105   bool FileIsExecutable(std::string const& file) const
106   {
107     switch (this->PolicyCMP0109) {
108       case cmPolicies::OLD:
109         return cmSystemTools::FileExists(file, true);
110       case cmPolicies::NEW:
111       case cmPolicies::REQUIRED_ALWAYS:
112       case cmPolicies::REQUIRED_IF_USED:
113         return cmSystemTools::FileIsExecutable(file);
114       default:
115         break;
116     }
117     bool const isExeOld = cmSystemTools::FileExists(file, true);
118     bool const isExeNew = cmSystemTools::FileIsExecutable(file);
119     if (isExeNew == isExeOld) {
120       return isExeNew;
121     }
122     if (isExeNew) {
123       this->Makefile->IssueMessage(
124         MessageType::AUTHOR_WARNING,
125         cmStrCat(cmPolicies::GetPolicyWarning(cmPolicies::CMP0109),
126                  "\n"
127                  "The file\n"
128                  "  ",
129                  file,
130                  "\n"
131                  "is executable but not readable.  "
132                  "CMake is ignoring it for compatibility."));
133     } else {
134       this->Makefile->IssueMessage(
135         MessageType::AUTHOR_WARNING,
136         cmStrCat(cmPolicies::GetPolicyWarning(cmPolicies::CMP0109),
137                  "\n"
138                  "The file\n"
139                  "  ",
140                  file,
141                  "\n"
142                  "is readable but not executable.  "
143                  "CMake is using it for compatibility."));
144     }
145     return isExeOld;
146   }
147 };
148 
cmFindProgramCommand(cmExecutionStatus & status)149 cmFindProgramCommand::cmFindProgramCommand(cmExecutionStatus& status)
150   : cmFindBase("find_program", status)
151 {
152   this->NamesPerDirAllowed = true;
153   this->VariableDocumentation = "Path to a program.";
154   this->VariableType = cmStateEnums::FILEPATH;
155 }
156 
157 // cmFindProgramCommand
InitialPass(std::vector<std::string> const & argsIn)158 bool cmFindProgramCommand::InitialPass(std::vector<std::string> const& argsIn)
159 {
160   this->DebugMode = this->ComputeIfDebugModeWanted();
161   this->CMakePathName = "PROGRAM";
162 
163   // call cmFindBase::ParseArguments
164   if (!this->ParseArguments(argsIn)) {
165     return false;
166   }
167 
168   if (this->AlreadyDefined) {
169     this->NormalizeFindResult();
170     return true;
171   }
172 
173   std::string const result = this->FindProgram();
174   this->StoreFindResult(result);
175   return true;
176 }
177 
FindProgram()178 std::string cmFindProgramCommand::FindProgram()
179 {
180   std::string program;
181 
182   if (this->SearchAppBundleFirst || this->SearchAppBundleOnly) {
183     program = this->FindAppBundle();
184   }
185   if (program.empty() && !this->SearchAppBundleOnly) {
186     program = this->FindNormalProgram();
187   }
188 
189   if (program.empty() && this->SearchAppBundleLast) {
190     program = this->FindAppBundle();
191   }
192   return program;
193 }
194 
FindNormalProgram()195 std::string cmFindProgramCommand::FindNormalProgram()
196 {
197   if (this->NamesPerDir) {
198     return this->FindNormalProgramNamesPerDir();
199   }
200   return this->FindNormalProgramDirsPerName();
201 }
202 
FindNormalProgramNamesPerDir()203 std::string cmFindProgramCommand::FindNormalProgramNamesPerDir()
204 {
205   // Search for all names in each directory.
206   cmFindProgramHelper helper(this->FindCommandName, this->Makefile, this);
207   for (std::string const& n : this->Names) {
208     helper.AddName(n);
209   }
210 
211   // Check for the names themselves if they contain a directory separator.
212   if (helper.CheckCompoundNames()) {
213     return helper.BestPath;
214   }
215 
216   // Search every directory.
217   for (std::string const& sp : this->SearchPaths) {
218     if (helper.CheckDirectory(sp)) {
219       return helper.BestPath;
220     }
221   }
222   // Couldn't find the program.
223   return "";
224 }
225 
FindNormalProgramDirsPerName()226 std::string cmFindProgramCommand::FindNormalProgramDirsPerName()
227 {
228   // Search the entire path for each name.
229   cmFindProgramHelper helper(this->FindCommandName, this->Makefile, this);
230   for (std::string const& n : this->Names) {
231     // Switch to searching for this name.
232     helper.SetName(n);
233 
234     // Check for the names themselves if they contain a directory separator.
235     if (helper.CheckCompoundNames()) {
236       return helper.BestPath;
237     }
238 
239     // Search every directory.
240     for (std::string const& sp : this->SearchPaths) {
241       if (helper.CheckDirectory(sp)) {
242         return helper.BestPath;
243       }
244     }
245   }
246   // Couldn't find the program.
247   return "";
248 }
249 
FindAppBundle()250 std::string cmFindProgramCommand::FindAppBundle()
251 {
252   for (std::string const& name : this->Names) {
253 
254     std::string appName = name + std::string(".app");
255     std::string appPath =
256       cmSystemTools::FindDirectory(appName, this->SearchPaths, true);
257 
258     if (!appPath.empty()) {
259       std::string executable = this->GetBundleExecutable(appPath);
260       if (!executable.empty()) {
261         return cmSystemTools::CollapseFullPath(executable);
262       }
263     }
264   }
265 
266   // Couldn't find app bundle
267   return "";
268 }
269 
GetBundleExecutable(std::string const & bundlePath)270 std::string cmFindProgramCommand::GetBundleExecutable(
271   std::string const& bundlePath)
272 {
273   std::string executable;
274   (void)bundlePath;
275 #if defined(__APPLE__)
276   // Started with an example on developer.apple.com about finding bundles
277   // and modified from that.
278 
279   // Get a CFString of the app bundle path
280   // XXX - Is it safe to assume everything is in UTF8?
281   CFStringRef bundlePathCFS = CFStringCreateWithCString(
282     kCFAllocatorDefault, bundlePath.c_str(), kCFStringEncodingUTF8);
283 
284   // Make a CFURLRef from the CFString representation of the
285   // bundle’s path.
286   CFURLRef bundleURL = CFURLCreateWithFileSystemPath(
287     kCFAllocatorDefault, bundlePathCFS, kCFURLPOSIXPathStyle, true);
288 
289   // Make a bundle instance using the URLRef.
290   CFBundleRef appBundle = CFBundleCreate(kCFAllocatorDefault, bundleURL);
291 
292   // returned executableURL is relative to <appbundle>/Contents/MacOS/
293   CFURLRef executableURL = CFBundleCopyExecutableURL(appBundle);
294 
295   if (executableURL != nullptr) {
296     const int MAX_OSX_PATH_SIZE = 1024;
297     UInt8 buffer[MAX_OSX_PATH_SIZE];
298 
299     if (CFURLGetFileSystemRepresentation(executableURL, false, buffer,
300                                          MAX_OSX_PATH_SIZE)) {
301       executable = bundlePath + "/Contents/MacOS/" +
302         std::string(reinterpret_cast<char*>(buffer));
303     }
304     // Only release CFURLRef if it's not null
305     CFRelease(executableURL);
306   }
307 
308   // Any CF objects returned from functions with "create" or
309   // "copy" in their names must be released by us!
310   CFRelease(bundlePathCFS);
311   CFRelease(bundleURL);
312   CFRelease(appBundle);
313 #endif
314 
315   return executable;
316 }
317 
cmFindProgram(std::vector<std::string> const & args,cmExecutionStatus & status)318 bool cmFindProgram(std::vector<std::string> const& args,
319                    cmExecutionStatus& status)
320 {
321   return cmFindProgramCommand(status).InitialPass(args);
322 }
323