Commit c6132a05 authored by Friedemann Kleint's avatar Friedemann Kleint
Browse files

Customwizards: Add a way of wrapping a Generator script.

Add attribute to XML syntax specifying a script to generate files.
The script must provide a --dry-run mode in which it prints the files
it intends to create and their attributes to stdout.
Rework the CustomWizardContext structure to contain target path
and parameter mappings, simplify some code there.
parent b719bbda
Qt Creator custom wizard are located in this directory.
Qt Creator custom wizards are located in this directory.
The subdirectories 'helloworld' and 'listmodel' are provided as examples.
The subdirectories 'helloworld', 'listmodel' and 'scriptgeneratedproject'
are provided as examples.
To see how they work in Qt Creator, rename the 'wizard_sample.xml' files
to 'wizard.xml'.
......
#!/usr/bin/perl -w
# *************************************************************************
#
# This file is part of Qt Creator
#
# Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
#
# Contact: Nokia Corporation (qt-info@nokia.com)
#
# Commercial Usage
#
# Licensees holding valid Qt Commercial licenses may use this file in
# accordance with the Qt Commercial License Agreement provided with the
# Software or, alternatively, in accordance with the terms contained in
# a written agreement between you and Nokia.
#
# GNU Lesser General Public License Usage
#
# Alternatively, this file may be used under the terms of the GNU Lesser
# General Public License version 2.1 as published by the Free Software
# Foundation and appearing in the file LICENSE.LGPL included in the
# packaging of this file. Please review the following information to
# ensure the GNU Lesser General Public License version 2.1 requirements
# will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
#
# If you are unsure which license is appropriate for your use, please
# contact the sales department at http://qt.nokia.com/contact.
#
# *************************************************************************
use strict;
use Getopt::Long;
use IO::File;
my $optDryRun = 0;
my $fieldClassName = 'MyClass';
my $standardFieldProjectName = 'MyProject';
my $standardFieldCppHeaderSuffix = 'h';
my $standardFieldCppSourceSuffix = 'cpp';
my $USAGE=<<EOF;
Usage: generate.pl [--dry-run] <parameter-mappings>
Custom wizard project generation example script.
Known parameters: ClassName=<value>
EOF
if (!GetOptions("dry-run" => \$optDryRun)) {
print $USAGE;
exit (1);
}
if (scalar(@ARGV) == 0) {
print $USAGE;
exit (1);
}
# -- Parse the 'field=value' pairs
foreach my $arg (@ARGV) {
my ($key, $value) = split('=', $arg);
$fieldClassName = $value if ($key eq 'ClassName');
# -- Standard fields defined by the custom project wizard
$standardFieldProjectName = $value if ($key eq 'ProjectName');
$standardFieldCppHeaderSuffix = $value if ($key eq 'CppHeaderSuffix');
$standardFieldCppSourceSuffix = $value if ($key eq 'CppSourceSuffix');
}
# -- Determine file names
my $baseFileName = lc($fieldClassName);
my $sourceFileName = $baseFileName . '.' . $standardFieldCppSourceSuffix;
my $headerFileName = $baseFileName . '.' . $standardFieldCppHeaderSuffix;
my $mainSourceFileName = 'main.' . $standardFieldCppSourceSuffix;
my $projectFileName = lc($standardFieldProjectName) . '.pro';
if ($optDryRun) {
# -- Step 1) Dry run: Print file names along with attributes
print $sourceFileName,",openeditor\n";
print $headerFileName,",openeditor\n";
print $mainSourceFileName,",openeditor\n";
print $projectFileName,",openproject\n";
} else {
# -- Step 2) Actual file creation
print 'Generating ', $headerFileName, ' ', $sourceFileName, ' ',
$mainSourceFileName, ' ', $projectFileName, "\n";
my $headerFile = new IO::File('>' . $headerFileName) or die ('Unable to open ' . $headerFileName . ' :' . $!);
print $headerFile '#ifndef ', uc($fieldClassName), "_H\n#define ", uc($fieldClassName), "_H\n\n",
'class ', $fieldClassName, "{\npublic:\n ", $fieldClassName, "();\n\n};\n\n#endif\n";
$headerFile->close();
my $sourceFile = new IO::File('>' . $sourceFileName) or die ('Unable to open ' . $sourceFileName . ' :' . $!);
print $sourceFile '#include "', $headerFileName ,"\"\n\n",
$fieldClassName,'::', $fieldClassName, "()\n{\n}\n";
$sourceFile->close();
my $mainSourceFile = new IO::File('>' . $mainSourceFileName) or die ('Unable to open ' . $mainSourceFileName . ' :' . $!);
print $mainSourceFile '#include "', $headerFileName ,"\"\n\n",
"int main(int argc, char *argv[])\n{\n ", $fieldClassName,' ', lc($fieldClassName),
";\n return 0;\n}\n";
$mainSourceFile->close();
my $projectFile = new IO::File('>' . $projectFileName) or die ('Unable to open ' . $projectFileName . ' :' . $!);
print $projectFile "TEMPLATE = app\nQT -= core\nCONFIG += console\nTARGET = ", $standardFieldProjectName,
"\nSOURCES += ", $sourceFileName, ' ',$headerFileName, ' ', $mainSourceFileName,
"\nHEADERS += ", $headerFileName,"\n";
$projectFile->close();
}
<?xml version="1.0" encoding="UTF-8"?>
<!--
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** Commercial Usage
**
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
**
** GNU Lesser General Public License Usage
**
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** If you are unsure which license is appropriate for your use, please
** contact the sales department at http://qt.nokia.com/contact.
**
**************************************************************************/
Custom class wizard example configuration file. -->
<wizard version="1" class="qt4project" firstpage="10" kind="project" id="A.ScriptGeneratedProject" category="B.CustomProjects">
<description>Creates a simple project using a generator script</description>
<displayname>Simple Script-Generated Project</displayname>;
<displaycategory>Custom Projects</displaycategory>
<!-- Specify the generator script -->
<files generatorscript="generate.pl"/>
<!-- Create parameter wizard page -->
<fieldpagetitle>Simple Script-Generated Project Parameters</fieldpagetitle>
<fields>
<field name="ClassName">
<fieldcontrol class="QLineEdit" validator="^[a-zA-Z0-9_]+$" defaulttext="MyClass" />
<fielddescription>Class name:</fielddescription>
</field>
</fields>
</wizard>
......@@ -552,12 +552,11 @@ QStringList BaseFileWizard::runWizard(const QString &path, QWidget *parent)
}
// Write
foreach (const GeneratedFile &generatedFile, files) {
if (!generatedFile.write(&errorMessage)) {
QMessageBox::critical(parent, tr("File Generation Failure"), errorMessage);
return QStringList();
}
if (!writeFiles(files, &errorMessage)) {
QMessageBox::critical(parent, tr("File Generation Failure"), errorMessage);
return QStringList();
}
bool removeOpenProjectAttribute = false;
// Run the extensions
foreach (IFileWizardExtension *ex, extensions) {
......@@ -585,6 +584,17 @@ QStringList BaseFileWizard::runWizard(const QString &path, QWidget *parent)
return result;
}
// Write
bool BaseFileWizard::writeFiles(const GeneratedFiles &files, QString *errorMessage)
{
foreach (const GeneratedFile &generatedFile, files)
if (!(generatedFile.attributes() & GeneratedFile::CustomGeneratorAttribute))
if (!generatedFile.write(errorMessage))
return false;
return true;
}
void BaseFileWizard::setupWizard(QWizard *w)
{
w->setOption(QWizard::NoCancelButton, false);
......
......@@ -65,7 +65,14 @@ class GeneratedFilePrivate;
class CORE_EXPORT GeneratedFile
{
public:
enum Attribute { OpenEditorAttribute = 0x01, OpenProjectAttribute = 0x02 };
enum Attribute { // Open this file in editor
OpenEditorAttribute = 0x01,
// Open project
OpenProjectAttribute = 0x02,
/* File is generated by external scripts, do not write out,
* see BaseFileWizard::writeFiles() */
CustomGeneratorAttribute = 0x4
};
Q_DECLARE_FLAGS(Attributes, Attribute)
GeneratedFile();
......@@ -200,6 +207,10 @@ protected:
virtual GeneratedFiles generateFiles(const QWizard *w,
QString *errorMessage) const = 0;
/* Physically write files. Re-implement (calling the base implementation)
* to create files with CustomGeneratorAttribute set. */
virtual bool writeFiles(const GeneratedFiles &files, QString *errorMessage);
// Overwrite for ProjectWizard kind and return the path to the generated project file
virtual QString generatedProjectFilePath(const QWizard *wizard) const;
......
......@@ -32,6 +32,7 @@
#include "customwizardpage.h"
#include "projectexplorer.h"
#include "baseprojectwizarddialog.h"
#include "customwizardscriptgenerator.h"
#include <coreplugin/icore.h>
#include <coreplugin/messagemanager.h>
......@@ -186,30 +187,80 @@ Core::GeneratedFiles CustomWizard::generateFiles(const QWizard *dialog, QString
// Look for the Custom field page to find the path
const Internal::CustomWizardPage *cwp = findWizardPage<Internal::CustomWizardPage>(dialog);
QTC_ASSERT(cwp, return Core::GeneratedFiles())
QString path = cwp->path();
const FieldReplacementMap fieldMap = replacementMap(dialog);
CustomWizardContextPtr ctx = context();
ctx->targetPath = cwp->path();
ctx->replacements = replacementMap(dialog);
if (CustomWizardPrivate::verbose) {
QString logText;
QTextStream str(&logText);
str << "CustomWizard::generateFiles: " << path << '\n';
const FieldReplacementMap::const_iterator cend = fieldMap.constEnd();
for (FieldReplacementMap::const_iterator it = fieldMap.constBegin(); it != cend; ++it)
str << "CustomWizard::generateFiles: " << ctx->targetPath << '\n';
const FieldReplacementMap::const_iterator cend = context()->replacements.constEnd();
for (FieldReplacementMap::const_iterator it = context()->replacements.constBegin(); it != cend; ++it)
str << " '" << it.key() << "' -> '" << it.value() << "'\n";
qWarning("%s", qPrintable(logText));
}
return generateWizardFiles(path, fieldMap, errorMessage);
return generateWizardFiles(errorMessage);
}
Core::GeneratedFiles CustomWizard::generateWizardFiles(const QString &targetPath,
const FieldReplacementMap &fieldReplacementMap,
QString *errorMessage) const
bool CustomWizard::writeFiles(const Core::GeneratedFiles &files, QString *errorMessage)
{
if (!Core::BaseFileWizard::writeFiles(files, errorMessage))
return false;
if (d->m_parameters->filesGeneratorScript.isEmpty())
return true;
// Prepare run of the custom script to generate. In the case of a
// project wizard that is entirely created by a script,
// the target project directory might not exist.
const CustomWizardContextPtr ctx = context();
QDir targetPathDir(ctx->targetPath);
if (!targetPathDir.exists()) {
if (CustomWizardPrivate::verbose)
qDebug("Creating directory %s", qPrintable(ctx->targetPath));
if (!targetPathDir.mkpath(ctx->targetPath)) {
*errorMessage = QString::fromLatin1("Unable to create the target directory '%1'").arg(ctx->targetPath);
return false;
}
}
// Run the custom script to actually generate the files.
if (!Internal::runCustomWizardGeneratorScript(ctx->targetPath, d->m_parameters->filesGeneratorScriptFullPath(),
ctx->replacements, errorMessage))
return false;
// Paranoia: Check on the files generated by the script:
foreach (const Core::GeneratedFile &generatedFile, files)
if (generatedFile.attributes() & Core::GeneratedFile::CustomGeneratorAttribute)
if (!QFileInfo(generatedFile.path()).isFile()) {
*errorMessage = QString::fromLatin1("%1 failed to generate %2").
arg(d->m_parameters->filesGeneratorScript, generatedFile.path());
return false;
}
return true;
}
Core::GeneratedFiles CustomWizard::generateWizardFiles(QString *errorMessage) const
{
if (CustomWizardPrivate::verbose)
qDebug() << "Replacements" << fieldReplacementMap;
// Create files
Core::GeneratedFiles rc;
const CustomWizardContextPtr ctx = context();
QTC_ASSERT(!ctx->targetPath.isEmpty(), return rc)
if (CustomWizardPrivate::verbose)
qDebug() << "CustomWizard::generateWizardFiles: in "
<< ctx->targetPath << ", using: " << ctx->replacements;
// If generator script is non-empty, do a dry run to get it's files.
if (!d->m_parameters->filesGeneratorScript.isEmpty()) {
rc += Internal::dryRunCustomWizardGeneratorScript(ctx->targetPath,
d->m_parameters->filesGeneratorScriptFullPath(),
ctx->replacements, errorMessage);
if (rc.isEmpty())
return rc;
}
// Add the template files specified by the <file> elements.
foreach(const Internal::CustomWizardFile &file, d->m_parameters->files)
if (!createFile(file, d->m_parameters->directory, targetPath, fieldReplacementMap, &rc, errorMessage))
if (!createFile(file, d->m_parameters->directory, ctx->targetPath, context()->replacements, &rc, errorMessage))
return Core::GeneratedFiles();
return rc;
}
......@@ -437,13 +488,22 @@ Core::GeneratedFiles CustomProjectWizard::generateFiles(const QWizard *w, QStrin
{
const BaseProjectWizardDialog *dialog = qobject_cast<const BaseProjectWizardDialog *>(w);
QTC_ASSERT(dialog, return Core::GeneratedFiles())
const QString targetPath = dialog->path() + QLatin1Char('/') + dialog->projectName();
// Add project name as macro.
// Add project name as macro. Path is here under project directory
CustomWizardContextPtr ctx = context();
ctx->targetPath = dialog->path() + QLatin1Char('/') + dialog->projectName();
FieldReplacementMap fieldReplacementMap = replacementMap(dialog);
fieldReplacementMap.insert(QLatin1String("ProjectName"), dialog->projectName());
ctx->replacements = fieldReplacementMap;
if (CustomWizardPrivate::verbose)
qDebug() << "CustomProjectWizard::generateFiles" << dialog << targetPath << fieldReplacementMap;
return generateWizardFiles(targetPath, fieldReplacementMap, errorMessage);
qDebug() << "CustomProjectWizard::generateFiles" << dialog << ctx->targetPath << ctx->replacements;
const Core::GeneratedFiles generatedFiles = generateWizardFiles(errorMessage);
// Find the project file and store in context
foreach(const Core::GeneratedFile &f, generatedFiles)
if (f.attributes() & Core::GeneratedFile::OpenProjectAttribute) {
ctx->projectFilePath = f.path();
break;
}
return generatedFiles;
}
bool CustomProjectWizard::postGenerateOpen(const Core::GeneratedFiles &l, QString *errorMessage)
......@@ -461,23 +521,11 @@ bool CustomProjectWizard::postGenerateOpen(const Core::GeneratedFiles &l, QStrin
return BaseFileWizard::postGenerateOpenEditors(l, errorMessage);
}
QString CustomProjectWizard::generatedProjectFilePath(const QWizard *wizard) const
QString CustomProjectWizard::generatedProjectFilePath(const QWizard *) const
{
const BaseProjectWizardDialog *dialog = qobject_cast<const BaseProjectWizardDialog *>(wizard);
QTC_ASSERT(dialog, return QString())
const QString targetPath = dialog->path() + QLatin1Char('/') + dialog->projectName();
const QChar slash = QLatin1Char('/');
// take the first from parameters()->files list which have cwFile.openProject set
foreach(const Internal::CustomWizardFile &file, parameters()->files) {
if (file.openProject) {
FieldReplacementMap fieldReplacementMap = replacementMap(dialog);
fieldReplacementMap.insert(QLatin1String("ProjectName"), dialog->projectName());
QString target = file.target;
Internal::CustomWizardContext::replaceFields(fieldReplacementMap, &target);
return QDir::toNativeSeparators(targetPath + slash + target);
}
}
return QString();
if (CustomWizardPrivate::verbose)
qDebug("CustomProjectWizard::generatedProjectFilePath: '%s'", qPrintable(context()->projectFilePath));
return context()->projectFilePath;
}
bool CustomProjectWizard::postGenerateFiles(const QWizard *, const Core::GeneratedFiles &l, QString *errorMessage)
......
......@@ -119,11 +119,10 @@ protected:
const WizardPageList &extensionPages) const;
// generate files in path
Core::GeneratedFiles generateWizardFiles(const QString &path,
const FieldReplacementMap &defaultFields,
QString *errorMessage) const;
Core::GeneratedFiles generateWizardFiles(QString *errorMessage) const;
// Create replacement map as static base fields + QWizard fields
FieldReplacementMap replacementMap(const QWizard *w) const;
virtual bool writeFiles(const Core::GeneratedFiles &files, QString *errorMessage);
CustomWizardParametersPtr parameters() const;
CustomWizardContextPtr context() const;
......
......@@ -2,8 +2,10 @@ INCLUDEPATH *= $$PWD
HEADERS += $$PWD/customwizard.h \
$$PWD/customwizardparameters.h \
$$PWD/customwizardpage.h \
customwizard/customwizardpreprocessor.h
customwizard/customwizardpreprocessor.h \
customwizard/customwizardscriptgenerator.h
SOURCES += $$PWD/customwizard.cpp \
$$PWD/customwizardparameters.cpp \
$$PWD/customwizardpage.cpp \
customwizard/customwizardpreprocessor.cpp
customwizard/customwizardpreprocessor.cpp \
customwizard/customwizardscriptgenerator.cpp
......@@ -70,12 +70,10 @@ static const char fieldMandatoryAttributeC[] = "mandatory";
static const char fieldControlElementC[] = "fieldcontrol";
static const char filesElementC[] = "files";
static const char filesGeneratorScriptAttributeC[] = "generatorscript";
static const char fileElementC[] = "file";
static const char fileNameSourceAttributeC[] = "source";
static const char fileNameTargetAttributeC[] = "target";
static const char fileNameOpenEditorAttributeC[] = "openeditor";
static const char fileNameOpenProjectAttributeC[] = "openproject";
static const char fileSourceAttributeC[] = "source";
static const char fileTargetAttributeC[] = "target";
enum ParseState {
ParseBeginning,
......@@ -95,6 +93,9 @@ enum ParseState {
namespace ProjectExplorer {
namespace Internal {
const char customWizardFileOpenEditorAttributeC[] = "openeditor";
const char customWizardFileOpenProjectAttributeC[] = "openproject";
CustomWizardField::CustomWizardField() :
mandatory(false)
{
......@@ -134,6 +135,7 @@ void CustomWizardParameters::clear()
directory.clear();
files.clear();
fields.clear();
filesGeneratorScript.clear();
firstPageId = -1;
}
......@@ -496,12 +498,15 @@ CustomWizardParameters::ParseResult
state = ParseWithinComboEntry;
}
break;
case ParseWithinFiles:
filesGeneratorScript = attributeValue(reader, filesGeneratorScriptAttributeC);
break;
case ParseWithinFile: { // file attribute
CustomWizardFile file;
file.source = attributeValue(reader, fileNameSourceAttributeC);
file.target = attributeValue(reader, fileNameTargetAttributeC);
file.openEditor = booleanAttributeValue(reader, fileNameOpenEditorAttributeC, false);
file.openProject = booleanAttributeValue(reader, fileNameOpenProjectAttributeC, false);
file.source = attributeValue(reader, fileSourceAttributeC);
file.target = attributeValue(reader, fileTargetAttributeC);
file.openEditor = booleanAttributeValue(reader, customWizardFileOpenEditorAttributeC, false);
file.openProject = booleanAttributeValue(reader, customWizardFileOpenProjectAttributeC, false);
if (file.target.isEmpty())
file.target = file.source;
if (file.source.isEmpty()) {
......@@ -554,11 +559,23 @@ CustomWizardParameters::ParseResult
return parse(configFile, configFileFullPath, bp, errorMessage);
}
QString CustomWizardParameters::filesGeneratorScriptFullPath() const
{
if (filesGeneratorScript.isEmpty())
return QString();
QString rc = directory;
rc += QLatin1Char('/');
rc += filesGeneratorScript;
return rc;
}
QString CustomWizardParameters::toString() const
{
QString rc;
QTextStream str(&rc);
str << "Directory: " << directory << " Klass: '" << klass << "'\n";
if (!filesGeneratorScript.isEmpty())
str << "Script: '" << filesGeneratorScript << "'\n";
foreach(const CustomWizardFile &f, files) {
str << " File source: " << f.source << " Target: " << f.target;
if (f.openEditor)
......@@ -652,6 +669,9 @@ void CustomWizardContext::reset()
mdb->preferredSuffixByType(QLatin1String(CppTools::Constants::CPP_SOURCE_MIMETYPE)));
baseReplacements.insert(QLatin1String("CppHeaderSuffix"),
mdb->preferredSuffixByType(QLatin1String(CppTools::Constants::CPP_HEADER_MIMETYPE)));
replacements.clear();
targetPath.clear();
projectFilePath.clear();
}
QString CustomWizardContext::processFile(const FieldReplacementMap &fm, QString in)
......
......@@ -81,9 +81,12 @@ public:
Core::BaseFileWizardParameters *bp, QString *errorMessage);
QString toString() const;
QString filesGeneratorScriptFullPath() const;
QString directory;
QString klass;
QList<CustomWizardFile> files;
QString filesGeneratorScript;
QString fieldPageTitle;
QList<CustomWizardField> fields;
int firstPageId;
......@@ -110,8 +113,16 @@ struct CustomWizardContext {
static QString processFile(const FieldReplacementMap &fm, QString in);
FieldReplacementMap baseReplacements;
FieldReplacementMap replacements;
// Where files should be created, that is, choosen path for simple wizards
// or "path/project" for project wizards.
QString targetPath;
QString projectFilePath;
};
extern const char customWizardFileOpenEditorAttributeC[];
extern const char customWizardFileOpenProjectAttributeC[];
} // namespace Internal
} // namespace ProjectExplorer
......
/**************************************************************************
**
** This file is part of Qt Creator
**
** Copyright (c) 2010 Nokia Corporation and/or its subsidiary(-ies).
**
** Contact: Nokia Corporation (qt-info@nokia.com)
**
** Commercial Usage
**
** Licensees holding valid Qt Commercial licenses may use this file in
** accordance with the Qt Commercial License Agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and Nokia.
**
** GNU Lesser General Public License Usage
**
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 2.1 requirements
** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** If you are unsure which license is appropriate for your use, please
** contact the sales department at http://qt.nokia.com/contact.
**
**************************************************************************/
#include "customwizardscriptgenerator.h"
#include "customwizard.h"
#include "customwizardparameters.h" // XML attributes
#include <QtCore/QProcess>
#include <QtCore/QDir>
#include <QtCore/QFileInfo>
#include <QtCore/QDebug>
#include <QtCore/QTemporaryFile>
#include <QtCore/QSharedPointer>
namespace ProjectExplorer {
namespace Internal {
typedef QSharedPointer<QTemporaryFile> TemporaryFilePtr;
// Format pattern for temporary files