Kill of RPMTAG_RHNPLATFORM: don't add to header, mark deprecated.
[platform/upstream/rpm.git] / build / parseSpec.c
1 /** \ingroup rpmbuild
2  * \file build/parseSpec.c
3  *  Top level dispatcher for spec file parsing.
4  */
5
6 #include "system.h"
7
8 #include <rpmio_internal.h>
9 #include <rpmbuild.h>
10 #include "rpmds.h"
11 #include "rpmts.h"
12 #include "debug.h"
13
14 /*@access FD_t @*/      /* compared with NULL */
15
16 /**
17  */
18 /*@unchecked@*/
19 static struct PartRec {
20     int part;
21     int len;
22 /*@observer@*/ /*@null@*/
23     const char * token;
24 } partList[] = {
25     { PART_PREAMBLE,      0, "%package"},
26     { PART_PREP,          0, "%prep"},
27     { PART_BUILD,         0, "%build"},
28     { PART_INSTALL,       0, "%install"},
29     { PART_CHECK,         0, "%check"},
30     { PART_CLEAN,         0, "%clean"},
31     { PART_PREUN,         0, "%preun"},
32     { PART_POSTUN,        0, "%postun"},
33     { PART_PRETRANS,      0, "%pretrans"},
34     { PART_POSTTRANS,     0, "%posttrans"},
35     { PART_PRE,           0, "%pre"},
36     { PART_POST,          0, "%post"},
37     { PART_FILES,         0, "%files"},
38     { PART_CHANGELOG,     0, "%changelog"},
39     { PART_DESCRIPTION,   0, "%description"},
40     { PART_TRIGGERPOSTUN, 0, "%triggerpostun"},
41     { PART_TRIGGERUN,     0, "%triggerun"},
42     { PART_TRIGGERIN,     0, "%triggerin"},
43     { PART_TRIGGERIN,     0, "%trigger"},
44     { PART_VERIFYSCRIPT,  0, "%verifyscript"},
45     {0, 0, 0}
46 };
47
48 /**
49  */
50 static inline void initParts(struct PartRec *p)
51         /*@modifies p->len @*/
52 {
53     for (; p->token != NULL; p++)
54         p->len = strlen(p->token);
55 }
56
57 rpmParseState isPart(const char *line)
58 {
59     struct PartRec *p;
60
61 /*@-boundsread@*/
62     if (partList[0].len == 0)
63         initParts(partList);
64 /*@=boundsread@*/
65     
66     for (p = partList; p->token != NULL; p++) {
67         char c;
68         if (xstrncasecmp(line, p->token, p->len))
69             continue;
70 /*@-boundsread@*/
71         c = *(line + p->len);
72 /*@=boundsread@*/
73         if (c == '\0' || xisspace(c))
74             break;
75     }
76
77     return (p->token ? p->part : PART_NONE);
78 }
79
80 /**
81  */
82 static int matchTok(const char *token, const char *line)
83         /*@*/
84 {
85     const char *b, *be = line;
86     size_t toklen = strlen(token);
87     int rc = 0;
88
89 /*@-boundsread@*/
90     while ( *(b = be) != '\0' ) {
91         SKIPSPACE(b);
92         be = b;
93         SKIPNONSPACE(be);
94         if (be == b)
95             break;
96         if (toklen != (be-b) || xstrncasecmp(token, b, (be-b)))
97             continue;
98         rc = 1;
99         break;
100     }
101 /*@=boundsread@*/
102
103     return rc;
104 }
105
106 /*@-boundswrite@*/
107 void handleComments(char *s)
108 {
109     SKIPSPACE(s);
110     if (*s == '#')
111         *s = '\0';
112 }
113 /*@=boundswrite@*/
114
115 /**
116  */
117 static void forceIncludeFile(Spec spec, const char * fileName)
118         /*@modifies spec->fileStack @*/
119 {
120     OFI_t * ofi;
121
122     ofi = newOpenFileInfo();
123     ofi->fileName = xstrdup(fileName);
124     ofi->next = spec->fileStack;
125     spec->fileStack = ofi;
126 }
127
128 /**
129  */
130 /*@-boundswrite@*/
131 static int copyNextLine(Spec spec, OFI_t *ofi, int strip)
132         /*@globals rpmGlobalMacroContext, h_errno,
133                 fileSystem @*/
134         /*@modifies spec->nextline, spec->nextpeekc, spec->lbuf, spec->line,
135                 ofi->readPtr,
136                 rpmGlobalMacroContext, fileSystem @*/
137 {
138     char *last;
139     char ch;
140
141     /* Restore 1st char in (possible) next line */
142     if (spec->nextline != NULL && spec->nextpeekc != '\0') {
143         *spec->nextline = spec->nextpeekc;
144         spec->nextpeekc = '\0';
145     }
146     /* Expand next line from file into line buffer */
147     if (!(spec->nextline && *spec->nextline)) {
148         int pc = 0, bc = 0, nc = 0;
149         char *from, *to, *p;
150         to = spec->lbufPtr ? spec->lbufPtr : spec->lbuf;
151         from = ofi->readPtr;
152         ch = ' ';
153         while (*from && ch != '\n')
154             ch = *to++ = *from++;
155 /*@-mods@*/
156         spec->lbufPtr = to;
157 /*@=mods@*/
158         *to++ = '\0';
159         ofi->readPtr = from;
160
161         /* Check if we need another line before expanding the buffer. */
162         for (p = spec->lbuf; *p; p++) {
163             switch (*p) {
164                 case '\\':
165                     switch (*(p+1)) {
166                         case '\n': p++, nc = 1; /*@innerbreak@*/ break;
167                         case '\0': /*@innerbreak@*/ break;
168                         default: p++; /*@innerbreak@*/ break;
169                     }
170                     /*@switchbreak@*/ break;
171                 case '\n': nc = 0; /*@switchbreak@*/ break;
172                 case '%':
173                     switch (*(p+1)) {
174                         case '{': p++, bc++; /*@innerbreak@*/ break;
175                         case '(': p++, pc++; /*@innerbreak@*/ break;
176                         case '%': p++; /*@innerbreak@*/ break;
177                     }
178                     /*@switchbreak@*/ break;
179                 case '{': if (bc > 0) bc++; /*@switchbreak@*/ break;
180                 case '}': if (bc > 0) bc--; /*@switchbreak@*/ break;
181                 case '(': if (pc > 0) pc++; /*@switchbreak@*/ break;
182                 case ')': if (pc > 0) pc--; /*@switchbreak@*/ break;
183             }
184         }
185         
186         /* If it doesn't, ask for one more line. We need a better
187          * error code for this. */
188         if (pc || bc || nc ) {
189 /*@-observertrans -readonlytrans@*/
190             spec->nextline = "";
191 /*@=observertrans =readonlytrans@*/
192             return RPMERR_UNMATCHEDIF;
193         }
194 /*@-mods@*/
195         spec->lbufPtr = spec->lbuf;
196 /*@=mods@*/
197
198         /* Don't expand macros (eg. %define) in false branch of %if clause */
199         if (spec->readStack->reading &&
200             expandMacros(spec, spec->macros, spec->lbuf, sizeof(spec->lbuf))) {
201                 rpmError(RPMERR_BADSPEC, _("line %d: %s\n"),
202                         spec->lineNum, spec->lbuf);
203                 return RPMERR_BADSPEC;
204         }
205         spec->nextline = spec->lbuf;
206     }
207
208     /* Find next line in expanded line buffer */
209     spec->line = last = spec->nextline;
210     ch = ' ';
211     while (*spec->nextline && ch != '\n') {
212         ch = *spec->nextline++;
213         if (!xisspace(ch))
214             last = spec->nextline;
215     }
216
217     /* Save 1st char of next line in order to terminate current line. */
218     if (*spec->nextline != '\0') {
219         spec->nextpeekc = *spec->nextline;
220         *spec->nextline = '\0';
221     }
222     
223     if (strip & STRIP_COMMENTS)
224         handleComments(spec->line);
225     
226     if (strip & STRIP_TRAILINGSPACE)
227         *last = '\0';
228
229     return 0;
230 }
231 /*@=boundswrite@*/
232
233 /*@-boundswrite@*/
234 int readLine(Spec spec, int strip)
235 {
236 #ifdef  DYING
237     const char *arch;
238     const char *os;
239 #endif
240     char  *s;
241     int match;
242     struct ReadLevelEntry *rl;
243     OFI_t *ofi = spec->fileStack;
244     int rc;
245
246 retry:
247     /* Make sure the current file is open */
248     /*@-branchstate@*/
249     if (ofi->fd == NULL) {
250         ofi->fd = Fopen(ofi->fileName, "r.fpio");
251         if (ofi->fd == NULL || Ferror(ofi->fd)) {
252             /* XXX Fstrerror */
253             rpmError(RPMERR_BADSPEC, _("Unable to open %s: %s\n"),
254                      ofi->fileName, Fstrerror(ofi->fd));
255             return RPMERR_BADSPEC;
256         }
257         spec->lineNum = ofi->lineNum = 0;
258     }
259     /*@=branchstate@*/
260
261     /* Make sure we have something in the read buffer */
262     if (!(ofi->readPtr && *(ofi->readPtr))) {
263         /*@-type@*/ /* FIX: cast? */
264         FILE * f = fdGetFp(ofi->fd);
265         /*@=type@*/
266         if (f == NULL || !fgets(ofi->readBuf, BUFSIZ, f)) {
267             /* EOF */
268             if (spec->readStack->next) {
269                 rpmError(RPMERR_UNMATCHEDIF, _("Unclosed %%if\n"));
270                 return RPMERR_UNMATCHEDIF;
271             }
272
273             /* remove this file from the stack */
274             spec->fileStack = ofi->next;
275             (void) Fclose(ofi->fd);
276             ofi->fileName = _free(ofi->fileName);
277             ofi = _free(ofi);
278
279             /* only on last file do we signal EOF to caller */
280             ofi = spec->fileStack;
281             if (ofi == NULL)
282                 return 1;
283
284             /* otherwise, go back and try the read again. */
285             goto retry;
286         }
287         ofi->readPtr = ofi->readBuf;
288         ofi->lineNum++;
289         spec->lineNum = ofi->lineNum;
290         if (spec->sl) {
291             speclines sl = spec->sl;
292             if (sl->sl_nlines == sl->sl_nalloc) {
293                 sl->sl_nalloc += 100;
294                 sl->sl_lines = (char **) xrealloc(sl->sl_lines, 
295                         sl->sl_nalloc * sizeof(*(sl->sl_lines)));
296             }
297             sl->sl_lines[sl->sl_nlines++] = xstrdup(ofi->readBuf);
298         }
299     }
300     
301 #ifdef  DYING
302     arch = NULL;
303     rpmGetArchInfo(&arch, NULL);
304     os = NULL;
305     rpmGetOsInfo(&os, NULL);
306 #endif
307
308     /* Copy next file line into the spec line buffer */
309     if ((rc = copyNextLine(spec, ofi, strip)) != 0) {
310         if (rc == RPMERR_UNMATCHEDIF)
311             goto retry;
312         return rc;
313     }
314
315     s = spec->line;
316     SKIPSPACE(s);
317
318     match = -1;
319     if (!spec->readStack->reading && !strncmp("%if", s, sizeof("%if")-1)) {
320         match = 0;
321     } else if (! strncmp("%ifarch", s, sizeof("%ifarch")-1)) {
322         const char *arch = rpmExpand("%{_target_cpu}", NULL);
323         s += 7;
324         match = matchTok(arch, s);
325         arch = _free(arch);
326     } else if (! strncmp("%ifnarch", s, sizeof("%ifnarch")-1)) {
327         const char *arch = rpmExpand("%{_target_cpu}", NULL);
328         s += 8;
329         match = !matchTok(arch, s);
330         arch = _free(arch);
331     } else if (! strncmp("%ifos", s, sizeof("%ifos")-1)) {
332         const char *os = rpmExpand("%{_target_os}", NULL);
333         s += 5;
334         match = matchTok(os, s);
335         os = _free(os);
336     } else if (! strncmp("%ifnos", s, sizeof("%ifnos")-1)) {
337         const char *os = rpmExpand("%{_target_os}", NULL);
338         s += 6;
339         match = !matchTok(os, s);
340         os = _free(os);
341     } else if (! strncmp("%if", s, sizeof("%if")-1)) {
342         s += 3;
343         match = parseExpressionBoolean(spec, s);
344         if (match < 0) {
345             rpmError(RPMERR_UNMATCHEDIF,
346                         _("%s:%d: parseExpressionBoolean returns %d\n"),
347                         ofi->fileName, ofi->lineNum, match);
348             return RPMERR_BADSPEC;
349         }
350     } else if (! strncmp("%else", s, sizeof("%else")-1)) {
351         s += 5;
352         if (! spec->readStack->next) {
353             /* Got an else with no %if ! */
354             rpmError(RPMERR_UNMATCHEDIF,
355                         _("%s:%d: Got a %%else with no %%if\n"),
356                         ofi->fileName, ofi->lineNum);
357             return RPMERR_UNMATCHEDIF;
358         }
359         spec->readStack->reading =
360             spec->readStack->next->reading && ! spec->readStack->reading;
361         spec->line[0] = '\0';
362     } else if (! strncmp("%endif", s, sizeof("%endif")-1)) {
363         s += 6;
364         if (! spec->readStack->next) {
365             /* Got an end with no %if ! */
366             rpmError(RPMERR_UNMATCHEDIF,
367                         _("%s:%d: Got a %%endif with no %%if\n"),
368                         ofi->fileName, ofi->lineNum);
369             return RPMERR_UNMATCHEDIF;
370         }
371         rl = spec->readStack;
372         spec->readStack = spec->readStack->next;
373         free(rl);
374         spec->line[0] = '\0';
375     } else if (! strncmp("%include", s, sizeof("%include")-1)) {
376         char *fileName, *endFileName, *p;
377
378         s += 8;
379         fileName = s;
380         if (! xisspace(*fileName)) {
381             rpmError(RPMERR_BADSPEC, _("malformed %%include statement\n"));
382             return RPMERR_BADSPEC;
383         }
384         SKIPSPACE(fileName);
385         endFileName = fileName;
386         SKIPNONSPACE(endFileName);
387         p = endFileName;
388         SKIPSPACE(p);
389         if (*p != '\0') {
390             rpmError(RPMERR_BADSPEC, _("malformed %%include statement\n"));
391             return RPMERR_BADSPEC;
392         }
393         *endFileName = '\0';
394
395         forceIncludeFile(spec, fileName);
396
397         ofi = spec->fileStack;
398         goto retry;
399     }
400
401     if (match != -1) {
402         rl = xmalloc(sizeof(*rl));
403         rl->reading = spec->readStack->reading && match;
404         rl->next = spec->readStack;
405         spec->readStack = rl;
406         spec->line[0] = '\0';
407     }
408
409     if (! spec->readStack->reading) {
410         spec->line[0] = '\0';
411     }
412
413     /*@-compmempass@*/ /* FIX: spec->readStack->next should be dependent */
414     return 0;
415     /*@=compmempass@*/
416 }
417 /*@=boundswrite@*/
418
419 void closeSpec(Spec spec)
420 {
421     OFI_t *ofi;
422
423     while (spec->fileStack) {
424         ofi = spec->fileStack;
425         spec->fileStack = spec->fileStack->next;
426         if (ofi->fd) (void) Fclose(ofi->fd);
427         ofi->fileName = _free(ofi->fileName);
428         ofi = _free(ofi);
429     }
430 }
431
432 /*@-redecl@*/
433 /*@unchecked@*/
434 extern int noLang;              /* XXX FIXME: pass as arg */
435 /*@=redecl@*/
436
437 /*@todo Skip parse recursion if os is not compatible. @*/
438 /*@-boundswrite@*/
439 int parseSpec(rpmts ts, const char *specFile, const char *rootURL,
440                 const char *buildRootURL, int recursing, const char *passPhrase,
441                 const char *cookie, int anyarch, int force)
442 {
443     rpmParseState parsePart = PART_PREAMBLE;
444     int initialPackage = 1;
445 #ifdef  DYING
446     const char *saveArch;
447 #endif
448     Package pkg;
449     Spec spec;
450     
451     /* Set up a new Spec structure with no packages. */
452     spec = newSpec();
453
454     /*
455      * Note: rpmGetPath should guarantee a "canonical" path. That means
456      * that the following pathologies should be weeded out:
457      *          //bin//sh
458      *          //usr//bin/
459      *          /.././../usr/../bin//./sh (XXX FIXME: dots not handled yet)
460      */
461     spec->specFile = rpmGetPath(specFile, NULL);
462     spec->fileStack = newOpenFileInfo();
463     spec->fileStack->fileName = xstrdup(spec->specFile);
464     if (buildRootURL) {
465         const char * buildRoot;
466         (void) urlPath(buildRootURL, &buildRoot);
467         /*@-branchstate@*/
468         if (*buildRoot == '\0') buildRoot = "/";
469         /*@=branchstate@*/
470         if (!strcmp(buildRoot, "/")) {
471             rpmError(RPMERR_BADSPEC,
472                      _("BuildRoot can not be \"/\": %s\n"), buildRootURL);
473             return RPMERR_BADSPEC;
474         }
475         spec->gotBuildRootURL = 1;
476         spec->buildRootURL = xstrdup(buildRootURL);
477         addMacro(spec->macros, "buildroot", NULL, buildRoot, RMIL_SPEC);
478     }
479     addMacro(NULL, "_docdir", NULL, "%{_defaultdocdir}", RMIL_SPEC);
480     spec->recursing = recursing;
481     spec->anyarch = anyarch;
482     spec->force = force;
483
484     if (rootURL)
485         spec->rootURL = xstrdup(rootURL);
486     if (passPhrase)
487         spec->passPhrase = xstrdup(passPhrase);
488     if (cookie)
489         spec->cookie = xstrdup(cookie);
490
491     spec->timeCheck = rpmExpandNumeric("%{_timecheck}");
492
493     /* All the parse*() functions expect to have a line pre-read */
494     /* in the spec's line buffer.  Except for parsePreamble(),   */
495     /* which handles the initial entry into a spec file.         */
496     
497     /*@-infloops@*/     /* LCL: parsePart is modified @*/
498     while (parsePart < PART_LAST && parsePart != PART_NONE) {
499         switch (parsePart) {
500         case PART_PREAMBLE:
501             parsePart = parsePreamble(spec, initialPackage);
502             initialPackage = 0;
503             /*@switchbreak@*/ break;
504         case PART_PREP:
505             parsePart = parsePrep(spec);
506             /*@switchbreak@*/ break;
507         case PART_BUILD:
508         case PART_INSTALL:
509         case PART_CHECK:
510         case PART_CLEAN:
511             parsePart = parseBuildInstallClean(spec, parsePart);
512             /*@switchbreak@*/ break;
513         case PART_CHANGELOG:
514             parsePart = parseChangelog(spec);
515             /*@switchbreak@*/ break;
516         case PART_DESCRIPTION:
517             parsePart = parseDescription(spec);
518             /*@switchbreak@*/ break;
519
520         case PART_PRE:
521         case PART_POST:
522         case PART_PREUN:
523         case PART_POSTUN:
524         case PART_PRETRANS:
525         case PART_POSTTRANS:
526         case PART_VERIFYSCRIPT:
527         case PART_TRIGGERIN:
528         case PART_TRIGGERUN:
529         case PART_TRIGGERPOSTUN:
530             parsePart = parseScript(spec, parsePart);
531             /*@switchbreak@*/ break;
532
533         case PART_FILES:
534             parsePart = parseFiles(spec);
535             /*@switchbreak@*/ break;
536
537         case PART_NONE:         /* XXX avoid gcc whining */
538         case PART_LAST:
539         case PART_BUILDARCHITECTURES:
540             /*@switchbreak@*/ break;
541         }
542
543         if (parsePart >= PART_LAST) {
544             spec = freeSpec(spec);
545             return parsePart;
546         }
547
548         if (parsePart == PART_BUILDARCHITECTURES) {
549             int index;
550             int x;
551
552             closeSpec(spec);
553
554             /* LCL: sizeof(spec->BASpecs[0]) -nullderef whine here */
555             spec->BASpecs = xcalloc(spec->BACount, sizeof(*spec->BASpecs));
556             index = 0;
557             if (spec->BANames != NULL)
558             for (x = 0; x < spec->BACount; x++) {
559
560                 /* Skip if not arch is not compatible. */
561                 if (!rpmMachineScore(RPM_MACHTABLE_BUILDARCH, spec->BANames[x]))
562                     /*@innercontinue@*/ continue;
563 #ifdef  DYING
564                 rpmGetMachine(&saveArch, NULL);
565                 saveArch = xstrdup(saveArch);
566                 rpmSetMachine(spec->BANames[x], NULL);
567 #else
568                 addMacro(NULL, "_target_cpu", NULL, spec->BANames[x], RMIL_RPMRC);
569 #endif
570                 spec->BASpecs[index] = NULL;
571                 if (parseSpec(ts, specFile, spec->rootURL, buildRootURL, 1,
572                                   passPhrase, cookie, anyarch, force)
573                  || (spec->BASpecs[index] = rpmtsSetSpec(ts, NULL)) == NULL)
574                 {
575                         spec->BACount = index;
576 /*@-nullstate@*/
577                         spec = freeSpec(spec);
578                         return RPMERR_BADSPEC;
579 /*@=nullstate@*/
580                 }
581 #ifdef  DYING
582                 rpmSetMachine(saveArch, NULL);
583                 saveArch = _free(saveArch);
584 #else
585                 delMacro(NULL, "_target_cpu");
586 #endif
587                 index++;
588             }
589
590             spec->BACount = index;
591             if (! index) {
592                 rpmError(RPMERR_BADSPEC,
593                         _("No compatible architectures found for build\n"));
594 /*@-nullstate@*/
595                 spec = freeSpec(spec);
596                 return RPMERR_BADSPEC;
597 /*@=nullstate@*/
598             }
599
600             /*
601              * Return the 1st child's fully parsed Spec structure.
602              * The restart of the parse when encountering BuildArch
603              * causes problems for "rpm -q --specfile". This is
604              * still a hack because there may be more than 1 arch
605              * specified (unlikely but possible.) There's also the
606              * further problem that the macro context, particularly
607              * %{_target_cpu}, disagrees with the info in the header.
608              */
609             /*@-branchstate@*/
610             if (spec->BACount >= 1) {
611                 Spec nspec = spec->BASpecs[0];
612                 spec->BASpecs = _free(spec->BASpecs);
613                 spec = freeSpec(spec);
614                 spec = nspec;
615             }
616             /*@=branchstate@*/
617
618             (void) rpmtsSetSpec(ts, spec);
619             return 0;
620         }
621     }
622     /*@=infloops@*/     /* LCL: parsePart is modified @*/
623
624     /* Check for description in each package and add arch and os */
625   {
626 #ifdef  DYING
627     const char *arch = NULL;
628     const char *os = NULL;
629     char *myos = NULL;
630
631     rpmGetArchInfo(&arch, NULL);
632     rpmGetOsInfo(&os, NULL);
633     /*
634      * XXX Capitalizing the 'L' is needed to insure that old
635      * XXX os-from-uname (e.g. "Linux") is compatible with the new
636      * XXX os-from-platform (e.g "linux" from "sparc-*-linux").
637      * XXX A copy of this string is embedded in headers.
638      */
639     if (!strcmp(os, "linux")) {
640         myos = xstrdup(os);
641         *myos = 'L';
642         os = myos;
643     }
644 #else
645     const char *platform = rpmExpand("%{_target_platform}", NULL);
646     const char *arch = rpmExpand("%{_target_cpu}", NULL);
647     const char *os = rpmExpand("%{_target_os}", NULL);
648 #endif
649
650     for (pkg = spec->packages; pkg != NULL; pkg = pkg->next) {
651         if (!headerIsEntry(pkg->header, RPMTAG_DESCRIPTION)) {
652             const char * name;
653             (void) headerNVR(pkg->header, &name, NULL, NULL);
654             rpmError(RPMERR_BADSPEC, _("Package has no %%description: %s\n"),
655                         name);
656             spec = freeSpec(spec);
657             return RPMERR_BADSPEC;
658         }
659
660         (void) headerAddEntry(pkg->header, RPMTAG_OS, RPM_STRING_TYPE, os, 1);
661         (void) headerAddEntry(pkg->header, RPMTAG_ARCH,
662                 RPM_STRING_TYPE, arch, 1);
663         (void) headerAddEntry(pkg->header, RPMTAG_PLATFORM,
664                 RPM_STRING_TYPE, platform, 1);
665
666         pkg->ds = rpmdsThis(pkg->header, RPMTAG_REQUIRENAME, RPMSENSE_EQUAL);
667
668     }
669
670 #ifdef  DYING
671     myos = _free(myos);
672 #else
673     platform = _free(platform);
674     arch = _free(arch);
675     os = _free(os);
676 #endif
677   }
678
679     closeSpec(spec);
680     (void) rpmtsSetSpec(ts, spec);
681
682     return 0;
683 }
684 /*@=boundswrite@*/