如何在应用程序的设置捆绑包中显示应用程序版本修订?


91

我想在应用程序的设置包中包含应用程序版本和内部修订,例如1.0.1(r1243)。

Root.plist文件包含这样的片段...

     <dict>
        <key>Type</key>
        <string>PSTitleValueSpecifier</string>
        <key>Title</key>
        <string>Version</string>
        <key>Key</key>
        <string>version_preference</string>
        <key>DefaultValue</key>
        <string>VersionValue</string>
        <key>Values</key>
        <array>
            <string>VersionValue</string>
        </array>
        <key>Titles</key>
        <array>
            <string>VersionValue</string>
        </array>
    </dict>

并且我想在构建时替换“ VersionValue”字符串。

我有一个可以从存储库中提取版本号的脚本,我需要的是一种在构建时处理(预处理)Root.plist文件的方法,并且可以在不影响源文件的情况下替换版本号。

Answers:


69

还有另一种解决方案比以前的任何一个答案都简单得多。Apple在大多数安装程序中捆绑了一个称为PlistBuddy的命令行工具,并将其包含在Leopard中/usr/libexec/PlistBuddy

由于要替换VersionValue,假设您将版本值提取到中$newVersion,则可以使用以下命令:

/usr/libexec/PlistBuddy -c "Set :VersionValue $newVersion" /path/to/Root.plist

无需摆弄sed或正则表达式,此方法非常简单。有关详细说明,请参见手册页。您可以使用PlistBuddy添加,删除或修改属性列表中的任何条目。例如,我的一个朋友在博客写道使用PlistBuddy在Xcode中增加内部版本号

注意:如果仅提供plist的路径,则PlistBuddy将进入交互模式,因此可以在决定保存更改之前发出多个命令。我绝对建议您在将其放入构建脚本之前执行此操作。


20
我似乎需要一段时间才能弄清楚在我的plist中引用版本号的正确方法。在我的情况下,它原来是/ usr / libexec / PlistBuddy Settings.bundle / Root.plist -c“ set PreferenceSpecifiers:0:DefaultValue $ newversion”-希望对其他人有用。
JosephH 2010年

奎因·泰勒(Quinn Taylor),JosephH,感谢您的回答,我得以在Settings.bundle中自动实现我的应用程序版本号。你们俩都+1 ;-)
Niko 2012年

7
在自定义的“运行脚本”构建阶段,我需要包括更多Root.plist路径:/ usr / libexec / PlistBuddy $ {TARGET_BUILD_DIR} / $ {FULL_PRODUCT_NAME} /Settings.bundle/Root.plist -c“ set PreferenceSpecifiers:0:DefaultValue $ newVersion“
Chris Vasselli 2013年

2
仅出于完整性考虑,以下是适用于我的PListBuddy的
koen

最正确的方法是/usr/libexec/PlistBuddy -c "Set :PreferenceSpecifiers:0:DefaultValue ${newVersion}" "${TARGET_BUILD_DIR}/${CONTENTS_FOLDER_PATH}/Settings.bundle/Root.plist"
kambala

66

我懒惰的人的解决方案是从我的应用程序代码中更新版本号。您可以在Root.plist中有一个默认值(或空白),然后在启动代码中的某个位置:

NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
[[NSUserDefaults standardUserDefaults] setObject:version forKey:@"version_preference"];

唯一的问题是您的应用程序必须至少运行一次,更新的版本才能显示在设置面板中。

您可以进一步研究和更新例如更新您的应用程序启动次数的计数器或其他有趣的信息。


9
这将起作用,除非用户启动您的应用之前进入“设置” 。
Moshe

9
@Moshe是的,但是要优雅地处理这个问题,您只需在.plist文件中指定一个默认值,可能类似于“尚未启动”
Carlos P

1
尽管大多数开发人员可能设置CFBundleShortVersionStringCFBundleVersion设置了相同的值,但CFBundleShortVersionString实际上Apple希望您考虑发布的版本,这就是向用户展示的版本CFBundleVersion可能是内部版本号,您可能不应该显示用户(如果有所不同)。
Nate

3
我想念什么吗?这正是我在做什么,但是值没有改变。你们不是在使用Title属性,我相信它是只读的吗?
萨姆森(samson)2012年

2
更新应用程序时还有另一个问题。在更新的应用程序至少启动一次之前,设置包仍将显示旧的构建版本。
Legoless

61

基于@Quinn的答案,这里是我用来执行此操作的完整过程和工作代码。

  • 将设置包添加到您的应用中。不要重命名。
  • 在文本编辑器中打开Settings.bundle / Root.plist

将内容替换为:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"     "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>PreferenceSpecifiers</key>
    <array>
        <dict>
            <key>Title</key>
            <string>About</string>
            <key>Type</key>
            <string>PSGroupSpecifier</string>
        </dict>
        <dict>
            <key>DefaultValue</key>
            <string>DummyVersion</string>
            <key>Key</key>
            <string>version_preference</string>
            <key>Title</key>
            <string>Version</string>
            <key>Type</key>
            <string>PSTitleValueSpecifier</string>
        </dict>
    </array>
    <key>StringsTable</key>
    <string>Root</string>
</dict>
</plist>
  • 创建一个“运行脚本”构建阶段,移至“副本捆绑资源”阶段之后。添加此代码:

    cd "${BUILT_PRODUCTS_DIR}"
    buildVersion=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_PATH}" )
    /usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:1:DefaultValue $buildVersion" "${WRAPPER_NAME}/Settings.bundle/Root.plist"
    
  • 将MyAppName替换为实际应用程序的名称,并将PreferenceSpecifiers之后的1替换为“设置”中Version条目的索引。上面的Root.plist示例在索引1处具有它。


我认为这是最好的方法
Tony

我尝试了这个,然后看到设置包中的“标题”值发生了变化。标题出现在InAppSettingsKit中,但该值与初始版本没有变化。标题永远不会出现在“设置”应用中。我放弃了,当用户在菜单中选择“关于”时,我将弹出一个对话框
bugloaf 2013年

使用此方法时,设置不是只读的。即,我可以点击settings.app中的版本号设置,它是可编辑的。
motionpotion 2014年

8
bash脚本@ ben-clayton不适用于我,因此我根据他的回答进行了重新制作,就是这样:buildVersion=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "${PROJECT_DIR}/${INFOPLIST_FILE}") /usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:3:DefaultValue $buildVersion" "${SRCROOT}/Settings.bundle/Root.plist"
Luis Ascorbe 2014年

1
您可以使用${INFOPLIST_PATH}信息plist路径
Tomer Even

23

使用本克莱顿的plist https://stackoverflow.com/a/12842530/338986

Run script在后面添加以下代码段Copy Bundle Resources

version=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$PROJECT_DIR/$INFOPLIST_FILE")
build=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "$PROJECT_DIR/$INFOPLIST_FILE")
/usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:1:DefaultValue $version ($build)" "$CODESIGNING_FOLDER_PATH/Settings.bundle/Root.plist"

追加CFBundleVersion另外的CFBundleShortVersionString。它发出这样的版本:

通过写$CODESIGNING_FOLDER_PATH/Settings.bundle/Root.plist 而不是写 $SRCROOT有一些好处。

  1. 它不会修改存储库工作副本中的文件。
  2. 您无需为Settings.bundlein中的路径设置大小写$SRCROOT。路径可能会有所不同。

在Xcode 7.3.1上测试


1
如果将脚本添加到项目方案的“构建,预操作”部分,这是IMO的最佳答案。看安迪的答案。
Vahid Amiri

2
这对我有用。只需记住将“ DefaultValue”更改为特定于您。例如,我想更改页脚,所以我使用了“ FooterText”。您还需要更改“ PreferenceSpecifiers”之后的数字,以使其与plist中的项目相关。
理查德·威瑟斯庞

12

基于示例这里,这里是我使用自动更新设置软件包版本号的脚本:

#! /usr/bin/env python
import os
from AppKit import NSMutableDictionary

settings_file_path = 'Settings.bundle/Root.plist' # the relative path from the project folder to your settings bundle
settings_key = 'version_preference' # the key of your settings version

# these are used for testing only
info_path = '/Users/mrwalker/developer/My_App/Info.plist'
settings_path = '/Users/mrwalker/developer/My_App/Settings.bundle/Root.plist'

# these environment variables are set in the XCode build phase
if 'PRODUCT_SETTINGS_PATH' in os.environ.keys():
    info_path = os.environ.get('PRODUCT_SETTINGS_PATH')

if 'PROJECT_DIR' in os.environ.keys():
    settings_path = os.path.join(os.environ.get('PROJECT_DIR'), settings_file_path)

# reading info.plist file
project_plist = NSMutableDictionary.dictionaryWithContentsOfFile_(info_path)
project_bundle_version = project_plist['CFBundleVersion']

# print 'project_bundle_version: '+project_bundle_version

# reading settings plist
settings_plist = NSMutableDictionary.dictionaryWithContentsOfFile_(settings_path)
  for dictionary in settings_plist['PreferenceSpecifiers']:
    if 'Key' in dictionary and dictionary['Key'] == settings_key:
        dictionary['DefaultValue'] = project_bundle_version

# print repr(settings_plist)
settings_plist.writeToFile_atomically_(settings_path, True)

这是我在Settings.bundle中获得的Root.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>PreferenceSpecifiers</key>
    <array>
        <dict>
            <key>Title</key>
            <string>About</string>
            <key>Type</key>
            <string>PSGroupSpecifier</string>
        </dict>
        <dict>
            <key>DefaultValue</key>
            <string>1.0.0.0</string>
            <key>Key</key>
            <string>version_preference</string>
            <key>Title</key>
            <string>Version</string>
            <key>Type</key>
            <string>PSTitleValueSpecifier</string>
        </dict>
    </array>
    <key>StringsTable</key>
    <string>Root</string>
</dict>
</plist>

非常有用-我在从Python执行PlistBuddy时遇到麻烦,而且我从没想过要使用NSDictionary(也没有意识到它也使您能够如此轻松地访问plist文件)
2012年

这次真是万分感谢。一种修改-如您现在所拥有的,它更改了源代码,而不是builddir-这意味着您在设备或模拟器中看到的内容始终是实际构建版本之后的一个构建版本。为了解决这个问题,我修改了脚本,以首先遍历源代码,然后遍历builddir,即。settings_path_build = os.path.join(os.environ.get('TARGET_BUILD_DIR'),settings_file_path_build)
xaphod 2014年

……而且,我附上了githash:gitHash = subprocess.check_output(["git", "rev-parse", "--short", "HEAD"]).rstrip()
xaphod 2014年

8

其他答案由于一个原因而无法正常工作:在打包设置包之后,才会执行运行脚本构建阶段。因此,如果您的Info.plist版本为2.0.11,并且将其更新为2.0.12,然后构建/归档项目,则“设置”捆绑包仍将显示为2.0.11。如果打开“设置”捆绑包Root.plist,则可以看到版本号直到构建过程结束才更新。您可以重新构建项目以正确更新“设置”捆绑包,也可以将脚本添加到预构建阶段...

  • 在XCode中,为您的项目目标编辑方案
  • 单击构建方案上的公开箭头
  • 然后,单击“预操作”项
  • 单击加号,然后选择“新建运行脚本操作”
  • 将shell值设置为/ bin / sh
  • 将“提供构建设置自”设置为项目目标
  • 将您的脚本添加到文本区域。以下脚本为我工作。您可能需要修改路径以匹配您的项目设置:

    versionString = $(/ usr / libexec / PlistBuddy -c“ Print CFBundleVersion”“ $ {PROJECT_DIR} / $ {INFOPLIST_FILE}”))

    / usr / libexec / PlistBuddy“ $ SRCROOT / Settings.bundle / Root.plist” -c“设置PreferenceSpecifiers:0:DefaultValue $ versionString”

在构建/归档过程中打包设置捆绑包之前,这将正确运行脚本。如果打开“设置”捆绑包Root.plist并构建/归档项目,现在您将看到在构建过程开始时版本号已更新,并且“设置”捆绑包将显示正确的版本。


谢谢,只有您的解决方案显示正确的构建版本。其他解决方案需要构建两次。
Boris Y.

这仍然需要使用Xcode 10.0
Patrick

@Patrick iOS设置应用有时会保留旧信息。要查看更改,您必须终止并重新启动“设置”应用程序。
安迪

1
顺便说一句,我找到了一种添加此脚本的更简单方法:转到项目目标的“构建阶段”选项卡,然后单击“ +”图标。选择“新运行脚本阶段”,然后在其中添加脚本代码。这就是关键:单击新的运行脚本并将其拖到“构建阶段”列表的顶部,在“目标依赖项”下,但在“编译源”之前。它的行为与预构建脚本相同,并且更易于查找。
安迪

感谢@Andy,您添加到Build Phases选项卡的解决方案运行得很好。
帕特里克

7

使用Xcode 11.4,可以使用以下步骤在应用程序的设置捆绑包中显示应用程序版本。


集合$(MARKETING_VERSION)$(CURRENT_PROJECT_VERSION)变量

注意:如果Info.plist中的和键 出现$(MARKETING_VERSION)$(CURRENT_PROJECT_VERSION)变量,则可以跳过以下步骤并跳至下一部分。Bundle version string (short)Bundle version

  1. 打开Xcode项目。
  2. 打开项目浏览器cmd1),选择您的项目以显示您的项目设置,然后选择应用程序目标。
  3. 选择常规选项卡。
  4. 在“身份”部分中,将“版本”字段内容更改为某个新值(例如0.1.0),并将“构建”字段内容更改为一些新值(例如12)。这两个更改将在Info.plist文件中创建$(MARKETING_VERSION)$(CURRENT_PROJECT_VERSION)变量。

创建和配置设置包

  1. 项目浏览器中,选择您的项目。
  2. 选择文件>新建>文件…cmdN)。
  3. 选择iOS标签。
  4. 在“资源”部分中选择“设置捆绑包” ,然后单击“下一步”创建
  5. 选择Root.plist并将其作为源代码打开。将其内容替换为以下代码:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>PreferenceSpecifiers</key>
    <array>
        <dict>
            <key>DefaultValue</key>
            <string></string>
            <key>Key</key>
            <string>version_preference</string>
            <key>Title</key>
            <string>Version</string>
            <key>Type</key>
            <string>PSTitleValueSpecifier</string>
        </dict>
    </array>
    <key>StringsTable</key>
    <string>Root</string>
</dict>
</plist>

添加运行脚本

  1. 项目浏览器中,选择您的项目。
  2. 选择应用目标。
  3. 选择构建阶段选项卡。
  4. 单击+ >新建运行脚本阶段
  5. 将新阶段拖放到“复制捆绑资源”部分上方的某个位置。这样,脚本将在编译应用程序之前执行。
  6. 打开新添加的“运行脚本”阶段,并添加以下脚本:
version="$MARKETING_VERSION"
build="$CURRENT_PROJECT_VERSION"
/usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:0:DefaultValue $version ($build)" "${SRCROOT}/Settings.bundle/Root.plist"

启动应用

  1. cmdR在设备或模拟器上运行产品()。
  2. 在设备或模拟器上,启动应用程序后,打开“设置”应用程序,然后在第三方应用程序列表中选择您的应用程序。该应用程序的版本应显示如下:


资料来源


这对我来说是个错误Set: Entry, "PreferenceSpecifiers:0:DefaultValue", Does Not Exist
Jordan H

2
这对我/usr/libexec/PlistBuddy "$SRCROOT/AppName/Settings.bundle/Root.plist" -c "set PreferenceSpecifiers:0:DefaultValue $version"
Jordan H

谢谢。这个世界对我来说。但我的名字叫Settings-Watch.bundle,并($build)
Kurt Lane

3

通过使用pListcompiler(http://sourceforge.net/projects/plistcompiler)开源项目,我设法做到了自己想要的。

  1. 使用此编译器,可以使用以下格式将属性文件写入.plc文件:

    plist {
        dictionary {
            key "StringsTable" value string "Root"
            key "PreferenceSpecifiers" value array [
                dictionary {
                    key "Type" value string "PSGroupSpecifier"
                    key "Title" value string "AboutSection"
                }
                dictionary {
                    key "Type" value string "PSTitleValueSpecifier"
                    key "Title" value string "Version"
                    key "Key" value string "version"
                    key "DefaultValue" value string "VersionValue"
                    key "Values" value array [
                        string "VersionValue"
                    ]
                    key "Titles" value array [
                        string "r" kRevisionNumber
                    ]
                }
            ]
        }
    }
    
  2. 我有一个自定义运行脚本构建阶段,该阶段将存储库修订版提取到.h文件,如brad-larson在此处所述

  3. plc文件可以包含预处理器指令,例如#define,#message,#if,#elif,#include,#warning,#ifdef,#else,#pragma,#error,#ifndef,#endif,xcode环境变量。所以我能够通过添加以下指令来引用变量kRevisionNumber

    #include "Revision.h"
    
  4. 我还在我的xcode目标中添加了一个自定义脚本构建阶段,以在每次项目构建时运行plcompiler。

    /usr/local/plistcompiler0.6/plcompile -dest Settings.bundle -o Root.plist Settings.plc
    

就是这样!


听起来只是为了替换plist文件中的单个值而进行的大量工作……从概念上讲,在构建plist时能够访问变量是很酷的,但是使用工具构建plist文件则容易得多。我在回答中描述了PlistBuddy-试试吧!
奎因·泰勒,

3

我的工作示例基于@Ben Clayton答案以及@Luis Ascorbe和@Vahid Amiri的评论:

注意:此方法会修改存储库工作副本中的Settings.bundle / Root.plist文件

  1. 将设置包添加到项目根目录。不要重命名

  2. 打开Settings.bundle / Root.plist作为源代码

    将内容替换为:

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
        <key>PreferenceSpecifiers</key>
        <array>
            <dict>
                <key>DefaultValue</key>
                <string></string>
                <key>Key</key>
                <string>version_preference</string>
                <key>Title</key>
                <string>Version</string>
                <key>Type</key>
                <string>PSTitleValueSpecifier</string>
            </dict>
        </array>
        <key>StringsTable</key>
        <string>Root</string>
    </dict>
    </plist>
    
  3. 将以下脚本添加到项目(目标)方案的“构建,预操作”部分

    version=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$PROJECT_DIR/$INFOPLIST_FILE")
    build=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "$PROJECT_DIR/$INFOPLIST_FILE")
    
    /usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:0:DefaultValue $version ($build)" "${SRCROOT}/Settings.bundle/Root.plist"
    
  4. 生成并运行当前方案


2

上述答案对我不起作用,因此我创建了自定义脚本。

这将动态更新Root.plist中的条目

使用下面的运行脚本。确保可以在xcode 10.3中进行验证。

“ var buildVersion”是要在标题中显示的版本。

标识符名称是settings.bundle Root.plist中标题下方的“ var version”

cd "${BUILT_PRODUCTS_DIR}"

#set version name to your title identifier's string from settings.bundle
var version = "Version"

#this will be the text displayed in title
longVersion=$(/usr/libexec/PlistBuddy -c "Print CFBundleVersion" "${INFOPLIST_PATH}")
shortVersion=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" ${TARGET_BUILD_DIR}/${INFOPLIST_PATH})
buildVersion="$shortVersion.$longVersion"

path="${WRAPPER_NAME}/Settings.bundle/Root.plist"

settingsCnt=`/usr/libexec/PlistBuddy -c "Print PreferenceSpecifiers:" ${path} | grep "Dict"|wc -l`

for (( idx=0; idx<$settingsCnt; idx++ ))
do
#echo "Welcome $idx times"
val=`/usr/libexec/PlistBuddy -c "Print PreferenceSpecifiers:${idx}:Key" ${path}`
#echo $val

#if ( "$val" == "Version" )
if [ $val == "Version" ]
then
#echo "the index of the entry whose 'Key' is 'version' is $idx."

# now set it
/usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:${idx}:DefaultValue $buildVersion" $path

# just to be sure that it worked
ver=`/usr/libexec/PlistBuddy -c "Print PreferenceSpecifiers:${idx}:DefaultValue" $path`
#echo 'PreferenceSpecifiers:$idx:DefaultValue set to: ' $ver

fi

done

Root.plist中的示例条目

    <dict>
        <key>Type</key>
        <string>PSTitleValueSpecifier</string>
        <key>Title</key>
        <string>Version</string>
        <key>DefaultValue</key>
        <string>We Rock</string>
        <key>Key</key>
        <string>Version</string>
    </dict>

工作样本Root.plist条目


1

我相信您可以使用类似于我在此答案中所描述的方式(基于此帖子)来执行此操作

首先,可以通过将VersionValue重命名为$ {VERSIONVALUE}来使VersionValue成为变量。创建一个名为versionvalue.xcconfig的文件,并将其添加到您的项目中。转到您的应用程序目标,然后转到该目标的“构建”设置。我相信您需要将VERSIONVALUE添加为用户定义的构建设置。在该窗口的右下角,将“基于”值更改为“ versionvalue”。

最后,转到目标并创建“运行脚本”构建阶段。检查“运行脚本”阶段,并将其粘贴到“脚本”文本字段内的脚本中。例如,使用当前Subversion版本标记我的BUILD_NUMBER设置的脚本如下:

REV=`/usr/bin/svnversion -nc ${PROJECT_DIR} | /usr/bin/sed -e 's/^[^:]*://;s/[A-Za-z]//'`
echo "BUILD_NUMBER = $REV" > ${PROJECT_DIR}/buildnumber.xcconfig

当这些值在您的项目中更改时,这应该可以替换变量。


如果我要将版本号嵌入到Info.plist文件中,则此方法有效。但是我无法使其适用于其他plist文件,例如位于Settings.bundle中的Root.plist文件。是否有可用于启用此设置的构建设置?
Panagiotis Korros

0

对我来说,这是最简单的解决方案:

在“复制捆绑资源”步骤之前添加新的脚本构建阶段

贝壳: /usr/bin/env python

内容:

#! /usr/bin/env python
import os
from AppKit import NSMutableDictionary

# Key to replace
settings_key = 'version_preference' # the key of your settings version

# File path
settings_path = os.environ.get('SRCROOT') + "/TheBeautifulNameOfYourOwnApp/Settings.bundle/Root.plist"

# Composing version string
version_string = os.environ.get('MARKETING_VERSION') + " (" + os.environ.get('CURRENT_PROJECT_VERSION') + ")"

# Reading settings plist
settings_plist = NSMutableDictionary.dictionaryWithContentsOfFile_(settings_path)
for dictionary in settings_plist['PreferenceSpecifiers']:
    if 'Key' in dictionary and dictionary['Key'] == settings_key:
        dictionary['DefaultValue'] = version_string

# Save new settings
settings_plist.writeToFile_atomically_(settings_path, True)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.