607d79772b4ced628e08bcd8eb12d57d2822251f
[platform/upstream/cmake.git] / Source / CPack / cmCPackFreeBSDGenerator.cxx
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 "cmCPackFreeBSDGenerator.h"
4
5 #include "cmArchiveWrite.h"
6 #include "cmCPackArchiveGenerator.h"
7 #include "cmCPackLog.h"
8 #include "cmGeneratedFileStream.h"
9 #include "cmStringAlgorithms.h"
10 #include "cmSystemTools.h"
11 #include "cmWorkingDirectory.h"
12
13 // Needed for ::open() and ::stat()
14 #include <algorithm>
15 #include <ostream>
16 #include <utility>
17 #include <vector>
18
19 #include <fcntl.h>
20 #include <pkg.h>
21
22 #include <sys/stat.h>
23
24 // Suffix used to tell libpkg what compression to use
25 static const char FreeBSDPackageCompression[] = "txz";
26 // Resulting package file-suffix, for < 1.17 and >= 1.17 versions of libpkg
27 static const char FreeBSDPackageSuffix_10[] = ".txz";
28 static const char FreeBSDPackageSuffix_17[] = ".pkg";
29
30 cmCPackFreeBSDGenerator::cmCPackFreeBSDGenerator()
31   : cmCPackArchiveGenerator(cmArchiveWrite::CompressXZ, "paxr",
32                             FreeBSDPackageSuffix_17)
33 {
34 }
35
36 int cmCPackFreeBSDGenerator::InitializeInternal()
37 {
38   this->SetOptionIfNotSet("CPACK_PACKAGING_INSTALL_PREFIX", "/usr/local");
39   this->SetOption("CPACK_INCLUDE_TOPLEVEL_DIRECTORY", "0");
40   return this->Superclass::InitializeInternal();
41 }
42
43 cmCPackFreeBSDGenerator::~cmCPackFreeBSDGenerator() = default;
44
45 // This is a wrapper for struct pkg_create and pkg_create()
46 //
47 // Instantiate this class with suitable parameters, then
48 // check isValid() to check if it's ok. Afterwards, call
49 // Create() to do the actual work. This will leave a package
50 // in the given `output_dir`.
51 //
52 // This wrapper cleans up the struct pkg_create.
53 class PkgCreate
54 {
55 public:
56   PkgCreate()
57     : d(nullptr)
58   {
59   }
60   PkgCreate(const std::string& output_dir, const std::string& toplevel_dir,
61             const std::string& manifest_name)
62     : d(pkg_create_new())
63     , manifest(manifest_name)
64
65   {
66     if (d) {
67       pkg_create_set_format(d, FreeBSDPackageCompression);
68       pkg_create_set_compression_level(d, 0); // Explicitly set default
69       pkg_create_set_overwrite(d, false);
70       pkg_create_set_rootdir(d, toplevel_dir.c_str());
71       pkg_create_set_output_dir(d, output_dir.c_str());
72     }
73   }
74   ~PkgCreate()
75   {
76     if (d)
77       pkg_create_free(d);
78   }
79
80   bool isValid() const { return d; }
81
82   bool Create()
83   {
84     if (!isValid())
85       return false;
86     int r = pkg_create(d, manifest.c_str(), nullptr, false);
87     return r == 0;
88   }
89
90 private:
91   struct pkg_create* d;
92   std::string manifest;
93 };
94
95 // This is a wrapper, for use only in stream-based output,
96 // that will output a string in UCL escaped fashion (in particular,
97 // quotes and backslashes are escaped). The list of characters
98 // to escape is taken from https://github.com/vstakhov/libucl
99 // (which is the reference implementation pkg(8) refers to).
100 class EscapeQuotes
101 {
102 public:
103   const std::string& value;
104
105   EscapeQuotes(const std::string& s)
106     : value(s)
107   {
108   }
109 };
110
111 // Output a string as "string" with escaping applied.
112 cmGeneratedFileStream& operator<<(cmGeneratedFileStream& s,
113                                   const EscapeQuotes& v)
114 {
115   s << '"';
116   for (char c : v.value) {
117     switch (c) {
118       case '\n':
119         s << "\\n";
120         break;
121       case '\r':
122         s << "\\r";
123         break;
124       case '\b':
125         s << "\\b";
126         break;
127       case '\t':
128         s << "\\t";
129         break;
130       case '\f':
131         s << "\\f";
132         break;
133       case '\\':
134         s << "\\\\";
135         break;
136       case '"':
137         s << "\\\"";
138         break;
139       default:
140         s << c;
141         break;
142     }
143   }
144   s << '"';
145   return s;
146 }
147
148 // The following classes are all helpers for writing out the UCL
149 // manifest file (it also looks like JSON). ManifestKey just has
150 // a (string-valued) key; subclasses add a specific kind of
151 // value-type to the key, and implement write_value() to output
152 // the corresponding UCL.
153 class ManifestKey
154 {
155 public:
156   std::string key;
157
158   ManifestKey(std::string k)
159     : key(std::move(k))
160   {
161   }
162
163   virtual ~ManifestKey() = default;
164
165   // Output the value associated with this key to the stream @p s.
166   // Format is to be decided by subclasses.
167   virtual void write_value(cmGeneratedFileStream& s) const = 0;
168 };
169
170 // Basic string-value (e.g. "name": "cmake")
171 class ManifestKeyValue : public ManifestKey
172 {
173 public:
174   std::string value;
175
176   ManifestKeyValue(const std::string& k, std::string v)
177     : ManifestKey(k)
178     , value(std::move(v))
179   {
180   }
181
182   void write_value(cmGeneratedFileStream& s) const override
183   {
184     s << EscapeQuotes(value);
185   }
186 };
187
188 // List-of-strings values (e.g. "licenses": ["GPLv2", "LGPLv2"])
189 class ManifestKeyListValue : public ManifestKey
190 {
191 public:
192   using VList = std::vector<std::string>;
193   VList value;
194
195   ManifestKeyListValue(const std::string& k)
196     : ManifestKey(k)
197   {
198   }
199
200   ManifestKeyListValue& operator<<(const std::string& v)
201   {
202     value.push_back(v);
203     return *this;
204   }
205
206   ManifestKeyListValue& operator<<(const std::vector<std::string>& v)
207   {
208     for (std::string const& e : v) {
209       (*this) << e;
210     }
211     return *this;
212   }
213
214   void write_value(cmGeneratedFileStream& s) const override
215   {
216     bool with_comma = false;
217
218     s << '[';
219     for (std::string const& elem : value) {
220       s << (with_comma ? ',' : ' ');
221       s << EscapeQuotes(elem);
222       with_comma = true;
223     }
224     s << " ]";
225   }
226 };
227
228 // Deps: actually a dictionary, but we'll treat it as a
229 // list so we only name the deps, and produce dictionary-
230 // like output via write_value()
231 class ManifestKeyDepsValue : public ManifestKeyListValue
232 {
233 public:
234   ManifestKeyDepsValue(const std::string& k)
235     : ManifestKeyListValue(k)
236   {
237   }
238
239   void write_value(cmGeneratedFileStream& s) const override
240   {
241     s << "{\n";
242     for (std::string const& elem : value) {
243       s << "  \"" << elem << R"(": {"origin": ")" << elem << "\"},\n";
244     }
245     s << '}';
246   }
247 };
248
249 // Write one of the key-value classes (above) to the stream @p s
250 cmGeneratedFileStream& operator<<(cmGeneratedFileStream& s,
251                                   const ManifestKey& v)
252 {
253   s << '"' << v.key << "\": ";
254   v.write_value(s);
255   s << ",\n";
256   return s;
257 }
258
259 // Look up variable; if no value is set, returns an empty string;
260 // basically a wrapper that handles the NULL-ptr return from GetOption().
261 std::string cmCPackFreeBSDGenerator::var_lookup(const char* var_name)
262 {
263   cmValue pv = this->GetOption(var_name);
264   if (!pv) {
265     return {};
266   }
267   return *pv;
268 }
269
270 // Produce UCL in the given @p manifest file for the common
271 // manifest fields (common to the compact and regular formats),
272 // by reading the CPACK_FREEBSD_* variables.
273 void cmCPackFreeBSDGenerator::write_manifest_fields(
274   cmGeneratedFileStream& manifest)
275 {
276   manifest << ManifestKeyValue("name",
277                                var_lookup("CPACK_FREEBSD_PACKAGE_NAME"));
278   manifest << ManifestKeyValue("origin",
279                                var_lookup("CPACK_FREEBSD_PACKAGE_ORIGIN"));
280   manifest << ManifestKeyValue("version",
281                                var_lookup("CPACK_FREEBSD_PACKAGE_VERSION"));
282   manifest << ManifestKeyValue("maintainer",
283                                var_lookup("CPACK_FREEBSD_PACKAGE_MAINTAINER"));
284   manifest << ManifestKeyValue("comment",
285                                var_lookup("CPACK_FREEBSD_PACKAGE_COMMENT"));
286   manifest << ManifestKeyValue(
287     "desc", var_lookup("CPACK_FREEBSD_PACKAGE_DESCRIPTION"));
288   manifest << ManifestKeyValue("www", var_lookup("CPACK_FREEBSD_PACKAGE_WWW"));
289   std::vector<std::string> licenses =
290     cmExpandedList(var_lookup("CPACK_FREEBSD_PACKAGE_LICENSE"));
291   std::string licenselogic("single");
292   if (licenses.empty()) {
293     cmSystemTools::SetFatalErrorOccurred();
294   } else if (licenses.size() > 1) {
295     licenselogic = var_lookup("CPACK_FREEBSD_PACKAGE_LICENSE_LOGIC");
296   }
297   manifest << ManifestKeyValue("licenselogic", licenselogic);
298   manifest << (ManifestKeyListValue("licenses") << licenses);
299   std::vector<std::string> categories =
300     cmExpandedList(var_lookup("CPACK_FREEBSD_PACKAGE_CATEGORIES"));
301   manifest << (ManifestKeyListValue("categories") << categories);
302   manifest << ManifestKeyValue("prefix", var_lookup("CMAKE_INSTALL_PREFIX"));
303   std::vector<std::string> deps =
304     cmExpandedList(var_lookup("CPACK_FREEBSD_PACKAGE_DEPS"));
305   if (!deps.empty()) {
306     manifest << (ManifestKeyDepsValue("deps") << deps);
307   }
308 }
309
310 // Package only actual files; others are ignored (in particular,
311 // intermediate subdirectories are ignored).
312 static bool ignore_file(const std::string& filename)
313 {
314   struct stat statbuf;
315   return stat(filename.c_str(), &statbuf) < 0 ||
316     (statbuf.st_mode & S_IFMT) != S_IFREG;
317 }
318
319 // Write the given list of @p files to the manifest stream @p s,
320 // as the UCL field "files" (which is dictionary-valued, to
321 // associate filenames with hashes). All the files are transformed
322 // to paths relative to @p toplevel, with a leading / (since the paths
323 // in FreeBSD package files are supposed to be absolute).
324 void write_manifest_files(cmGeneratedFileStream& s,
325                           const std::string& toplevel,
326                           const std::vector<std::string>& files)
327 {
328   s << "\"files\": {\n";
329   for (std::string const& file : files) {
330     s << "  \"/" << cmSystemTools::RelativePath(toplevel, file) << "\": \""
331       << "<sha256>" // this gets replaced by libpkg by the actual SHA256
332       << "\",\n";
333   }
334   s << "  },\n";
335 }
336
337 int cmCPackFreeBSDGenerator::PackageFiles()
338 {
339   if (!this->ReadListFile("Internal/CPack/CPackFreeBSD.cmake")) {
340     cmCPackLogger(cmCPackLog::LOG_ERROR,
341                   "Error while executing CPackFreeBSD.cmake" << std::endl);
342     return 0;
343   }
344
345   cmWorkingDirectory wd(toplevel);
346
347   files.erase(std::remove_if(files.begin(), files.end(), ignore_file),
348               files.end());
349
350   std::string manifestname = toplevel + "/+MANIFEST";
351   {
352     cmGeneratedFileStream manifest(manifestname);
353     manifest << "{\n";
354     write_manifest_fields(manifest);
355     write_manifest_files(manifest, toplevel, files);
356     manifest << "}\n";
357   }
358
359   cmCPackLogger(cmCPackLog::LOG_DEBUG, "Toplevel: " << toplevel << std::endl);
360
361   if (WantsComponentInstallation()) {
362     // CASE 1 : COMPONENT ALL-IN-ONE package
363     // If ALL COMPONENTS in ONE package has been requested
364     // then the package file is unique and should be open here.
365     if (componentPackageMethod == ONE_PACKAGE) {
366       return PackageComponentsAllInOne();
367     }
368     // CASE 2 : COMPONENT CLASSICAL package(s) (i.e. not all-in-one)
369     // There will be 1 package for each component group
370     // however one may require to ignore component group and
371     // in this case you'll get 1 package for each component.
372     return PackageComponents(componentPackageMethod ==
373                              ONE_PACKAGE_PER_COMPONENT);
374   }
375
376   // There should be one name in the packageFileNames (already, see comment
377   // in cmCPackGenerator::DoPackage(), which holds what CPack guesses
378   // will be the package filename. libpkg does something else, though,
379   // so update the single filename to what we know will be right.
380   if (this->packageFileNames.size() == 1) {
381     std::string currentPackage = this->packageFileNames[0];
382     auto lastSlash = currentPackage.rfind('/');
383
384     // If there is a pathname, preserve that; libpkg will write out
385     // a file with the package name and version as specified in the
386     // manifest, so we look those up (again). lastSlash is the slash
387     // itself, we need that as path separator to the calculated package name.
388     std::string actualPackage =
389       ((lastSlash != std::string::npos)
390          ? std::string(currentPackage, 0, lastSlash + 1)
391          : std::string()) +
392       var_lookup("CPACK_FREEBSD_PACKAGE_NAME") + '-' +
393       var_lookup("CPACK_FREEBSD_PACKAGE_VERSION") + FreeBSDPackageSuffix_17;
394
395     this->packageFileNames.clear();
396     this->packageFileNames.emplace_back(actualPackage);
397   }
398
399   if (!pkg_initialized() && pkg_init(NULL, NULL) != EPKG_OK) {
400     cmCPackLogger(cmCPackLog::LOG_ERROR,
401                   "Can not initialize FreeBSD libpkg." << std::endl);
402     return 0;
403   }
404
405   std::string output_dir = cmSystemTools::CollapseFullPath("../", toplevel);
406   PkgCreate package(output_dir, toplevel, manifestname);
407   if (package.isValid()) {
408     if (!package.Create()) {
409       cmCPackLogger(cmCPackLog::LOG_ERROR,
410                     "Error during pkg_create()" << std::endl);
411       return 0;
412     }
413   } else {
414     cmCPackLogger(cmCPackLog::LOG_ERROR,
415                   "Error before pkg_create()" << std::endl);
416     return 0;
417   }
418
419   // Specifically looking for packages suffixed with the TAG, either extension
420   std::string broken_suffix_10 =
421     cmStrCat('-', var_lookup("CPACK_TOPLEVEL_TAG"), FreeBSDPackageSuffix_10);
422   std::string broken_suffix_17 =
423     cmStrCat('-', var_lookup("CPACK_TOPLEVEL_TAG"), FreeBSDPackageSuffix_17);
424   for (std::string& name : packageFileNames) {
425     cmCPackLogger(cmCPackLog::LOG_DEBUG, "Packagefile " << name << std::endl);
426     if (cmHasSuffix(name, broken_suffix_10)) {
427       name.replace(name.size() - broken_suffix_10.size(), std::string::npos,
428                    FreeBSDPackageSuffix_10);
429       break;
430     }
431     if (cmHasSuffix(name, broken_suffix_17)) {
432       name.replace(name.size() - broken_suffix_17.size(), std::string::npos,
433                    FreeBSDPackageSuffix_17);
434       break;
435     }
436   }
437   // If the name uses a *new* style name, which doesn't exist, but there
438   // is an *old* style name, then use that instead. This indicates we used
439   // an older libpkg, which still creates .txz instead of .pkg files.
440   for (std::string& name : packageFileNames) {
441     if (cmHasSuffix(name, FreeBSDPackageSuffix_17) &&
442         !cmSystemTools::FileExists(name)) {
443       const std::string badSuffix(FreeBSDPackageSuffix_17);
444       const std::string goodSuffix(FreeBSDPackageSuffix_10);
445       std::string repairedName(name);
446       repairedName.replace(repairedName.size() - badSuffix.size(),
447                            std::string::npos, goodSuffix);
448       if (cmSystemTools::FileExists(repairedName)) {
449         name = repairedName;
450         cmCPackLogger(cmCPackLog::LOG_DEBUG,
451                       "Repaired packagefile " << name << std::endl);
452       }
453     }
454   }
455
456   return 1;
457 }