TIZEN RPM Package Guidelines

Contents

Build Environment

The Build environment consists of a minimal set of packages needed to initiate building a package based on information provided in the spec file. The Build environment is recreated every time a package build is initiated and depending on the number of dependencies it can take some time to set up the environment and make it ready for building the package.

Build Environment Initialization Speedup

A few things can be done to speed up the initialization and setup of the build environment.

  • Install only the most basic packages: do not install anything that is uncommon among majority of packages
  • Install only needed files: There is no need to install documentation files or translation of the tool-chain and the console tools, this just takes space and time and those files are never used during the build process.

Packaging Guidelines

Legal

Installing License files

License files must be installed with the package. Up to Tizen 2.x, license files need to be installed as documents:

%files
%doc LICENSE COPYING

Starting with rpm 4.11, you can install the license using a new file macro %license, this will separate the license files from documents and install them in a special directory in /usr/share

%files
%license LICENSE COPYING

License Tag

Having gone through the majority of the packages in Tizen we notice that developer use different names for the same licenses which makes it very difficult to do any surveys and evaluate the licenses being used in Tizen. There are short and long formats, sometimes official full names are used, other times abbreviations. Some licenses are quoted with the version number of the license, other are not. We need a consistent way to identify the licenses in Tizen.

To solve this problem, we started using SPDX license list and identifiers. SPDX is the "Software Package Data Exchange" (http://spdx.org/licenses/) which has a list of commonly used licenses in open-source with a short form that can be used in packages. Using this short form will help us unify and cleanup the license naming mess we currently have.

In addition to the use of the predefined syntax for declaring licenses, the following license grammar should be used in spec files to specify multiple licenses. This can be achieved using operators such as 'and' to declare an aggregation of licenses, or 'or' to show alternative licenses a package can follow. You can also use brackets to gather licenses, for example:

License: (MIT or GPL-2.0) and LGPL-2.1+

which is the declaration for a package with an executable binary and a corresponding LGPL-2.1+ licensed library.

Please also note that in the case of sub-packages, the sub-packages will inherit the license from the main package if no license is specified for the sub-packages.

Missing licenses and licenses special to Tizen will be added as an extension to the SPDX list. If you find such a license, please let us know and we can propose a short form that can be used in all packages.

Package Naming

  • Dash '-' must be used as the delimiter for name parts.
  • Do NOT use an underscore '_', a plus '+', or a period '.' as a delimiter.
  • The spec file should be named using the %{name}.spec scheme which should also correspond to the package name within a project in the build system.

Version and Release

Package Versions look like : X.Y.Z-R.B

  • X.Y.Z is the 'Version' number - determined by the source package.
  • R is the 'Release' number which is automatically incremented by OBS whenever a source/packaging changes (eg a check-in or request acceptance)
  • B is the build number which is incremented when the package is rebuilt due to a dependency change.

Version

The Version field in the spec file is where you should put the current version of the software being packaged. There are four cases where the version contains non-numeric characters:

  • Pre-release packages: Packages released as "pre-release" versions, prior to a "final" version. Example tags include "alpha", "beta", "rc", "cvs", "git", "svn", etc... Details can be found below: Non-Numeric Version.
  • Post-release packages: Packages released after a "final" version. These packages contain the same numeric version as the "final" version, but have an additional non-numeric identifier. This mechanism may also be used for packaging only changes to an upstream package.
  • Snapshot packages: Packages built from SCM snapshots. These packages could be either "pre" or "post" release packages.

Release

This field is handled by the build system to be able to manage automated builds.

The initial setting in the spec file is used by the build system but in many cases it does not need to be changed.

There is no need for the %{dist} macro in the release field. This is also handled directly by the build system.

The release number is set to zero (Release: 0) with any version update.

It is increased by one with any change in the package.

Note: A release number set to one is also acceptable (Release: 1).

We can put letters into the version tag, so we do not use the Release field for this. Details can be found above.

If you build the package outside of the OBS or if you copy a package then you will of course not get the correct Release or Build values.

Tags

  • The Packager tag should not be used in spec files. The identities of the packagers are evident from the changelog entries. By not using the Packager tag, you also avoid seeing bad binaries rebuilt by someone else with your name in the header. See also the Maximum RPM definition of the Packager tag at www.rpm.org . If you need to include information about the packager in the rpms you built, use %packager in your ~/.rpmmacros instead.
  • The Vendor tag should not be used. It is set automatically by the build system.
  • Usually, the PreReq tag should be replaced by plain Requires. For more info, see Maximum RPM snapshot's fine grained dependencies chapter .
  • The Source tag documents where to find the upstream sources for the rpm. In most cases this should be a complete URL to the upstream tarball.

Summary Tag

The summary is a single line string describing the package. The maximum length is 80 characters. It should fit all standard situations and not assume any special context. It should be helpful alone, in alphabetically sorted or unsorted lists of some selected packages, and in alphabetically sorted or unsorted lists of all packages.

It should describe the package's main function and point out any special properties of the package to support the user comparing similar packages. For example, the two words "Web Browser" summarize any web browser, but using additional adjectives (like minimalistic, complex, GNOME, KDE, text-based, fast, or author's) helps characterize a specific package.

The RPM spec file contains only the English version to keep the RPM database small.

  • The Summary tag value should not end in a period. If this bothers you from a grammatical point of view, sit down, take a deep breath, and get over it.


Group Tag

Note:To simplify packaging and maintenance of groups we now require one group defined per package which should be applied on all sub-packages. Sub packages however need to follow a strict naming convention to allow grouping of supporting files based on function and content provided inside the sub-packages.

  • Git tree name SHOULD always correspond to the the upstream project name.
  • The main package name SHOULD always correspond to the the upstream project name.
  • The main package MIGHT be virtual and DOES NOT have to produce a binary package.
  • The main package in most cases will contain services and configuration files.
  • Libraries: Libraries SHOULD be packaged individually in lib<LIB NAME> sub-packages where it makes sense. Multiple package CAN be packages in the same lib<PKG NAME> package.
  • Utilities and Tools: Utilities SHOULD be package as part of a -tools sub-package.
  • Development: Development files and headers SHOULD be packaged in -devel sub-packages
  • Documentation: Documentation files and auto-generated documentation SHOULD be packaged in -docs sub-packages
  • Testing: Testing scripts and files SHOULD be packaged in a -test sub-package
  • Locale: Locale files that are not directly part of any sub-package SHOULD be installed as part of a -locale sub-package (see %lang_package macro).


The following groups have been defined based on the architecture. rpmlint is checking for them, so using different groups increases the "badness" of a package and may cause the package to be rejected when the "badness" goes beyond a certain threshold. The following list is what rpmlint in OBS currently checks for. However, it is outdated and may get updated at some point (see https://bugs.tizen.org/jira/browse/TC-1499):

Application Framework/Alarm
Application Framework/Application State Management
Application Framework/Database
Application Framework/Notifications
Application Framework/Package Management
Application Framework/Settings
Application Framework/API
Application Framework/Configuration
Application Framework/Development
Application Framework/Documentation
Application Framework/Libraries
Application Framework/Other
Application Framework/Service
Application Framework/Testing
Application Framework/Utilities
Applications/Core Applications
Applications/Game
Applications/Messaging
Applications/Multimedia
Applications/Music
Applications/Native Applications
Applications/Navigation
Applications/Network
Applications/Other
Applications/PIM
Applications/Photo
Applications/Social
Applications/Tasks
Applications/Telephony
Applications/Video
Applications/Web
Applications/Web Applications
Automotive/API
Automotive/Configuration
Automotive/Development
Automotive/Documentation
Automotive/Hardware Adaptation
Automotive/Libraries
Automotive/Other
Automotive/Service
Automotive/Testing
Automotive/Utilities
Base/API
Base/Compression
Base/Configuration
Base/Development
Base/Device Management
Base/Documentation
Base/File Systems
Base/Hardware Adaptation
Base/IPC
Base/Libraries
Base/Other
Base/Package Management
Base/Service
Base/Startup
Base/Testing
Base/Toolchain
Base/Utilities
Network & Connectivity/API
Network & Connectivity/Bluetooth
Network & Connectivity/Configuration
Network & Connectivity/Connection Management
Network & Connectivity/DNS
Network & Connectivity/Development
Network & Connectivity/Documentation
Network & Connectivity/HTTP
Network & Connectivity/Hardware Adaptation
Network & Connectivity/Libraries
Network & Connectivity/NFC
Network & Connectivity/Other
Network & Connectivity/Service
Network & Connectivity/Testing
Network & Connectivity/Utilities
Network & Connectivity/Wireless
Social & Content/API
Social & Content/Configuration
Social & Content/Development
Social & Content/Documentation
Social & Content/Libraries
Social & Content/Other
Social & Content/Service
Social & Content/Testing
Social & Content/Utilities
Social & Content/Calendar
Social & Content/Contacts
Development/Building
Development/Cross Compilation
Development/Languages
Development/Perl
Development/Python
Development/Testing
Development/Tools
Graphics & UI Framework/API
Graphics & UI Framework/Configuration
Graphics & UI Framework/Development
Graphics & UI Framework/Documentation
Graphics & UI Framework/Fonts
Graphics & UI Framework/Hardware Adaptation
Graphics & UI Framework/Input
Graphics & UI Framework/Input Service Framework
Graphics & UI Framework/Libraries
Graphics & UI Framework/Other
Graphics & UI Framework/Service
Graphics & UI Framework/Testing
Graphics & UI Framework/Utilities
Graphics & UI Framework/Voice Framework
Graphics & UI Framework/Wayland Window System
Graphics & UI Framework/X Window System
Graphics & UI Framework/Mobile UI
Graphics & UI Framework/Automotive UI
Location/API
Location/Configuration
Location/Development
Location/Documentation
Location/Geolocation
Location/Hardware Adaptation
Location/Libraries
Location/Other
Location/Service
Location/Testing
Location/Utilities
Messaging/API
Messaging/Configuration
Messaging/Development
Messaging/Documentation
Messaging/Email
Messaging/Instant Messaging
Messaging/Libraries
Messaging/Other
Messaging/SMS
Messaging/Service
Messaging/Testing
Messaging/Utilities
Multimedia/API
Multimedia/Audio
Multimedia/Camera
Multimedia/Configuration
Multimedia/Development
Multimedia/Documentation
Multimedia/Framework
Multimedia/Hardware Adaptation
Multimedia/Libraries
Multimedia/Other
Multimedia/Policy Management
Multimedia/Service
Multimedia/Testing
Multimedia/Utilities
Multimedia/Video
SDK/Configuration
SDK/Development
SDK/Documentation
SDK/Hardware Adaptation
SDK/Libraries
SDK/Other
SDK/Service
SDK/Testing
SDK/Utilities
Security/API
Security/Access Control
Security/Accounts
Security/Certificate Management
Security/Configuration
Security/Crypto Libraries
Security/DRM
Security/Development
Security/Documentation
Security/Libraries
Security/Network
Security/Other
Security/Secure Storage
Security/Service
Security/Testing
Security/Utilities
System/API
System/Audio
System/Configuration
System/Development
System/Documentation
System/Hardware Adaptation
System/Kernel
System/Libraries
System/Localization
System/Logging
System/Management
System/Monitoring
System/Network
System/Other
System/Power Management
System/Sensor Framework
System/Service
System/System Info
System/Testing
System/Utilities
Telephony/API
Telephony/Cellular
Telephony/Configuration
Telephony/Development
Telephony/Documentation
Telephony/Hardware Adaptation
Telephony/Libraries
Telephony/Other
Telephony/Service
Telephony/Testing
Telephony/Utilities
Web Framework/API
Web Framework/Configuration
Web Framework/Development
Web Framework/Documentation
Web Framework/Libraries
Web Framework/Other
Web Framework/Service
Web Framework/Testing
Web Framework/Utilities
Web Framework/Web Engine
Web Framework/Web Run Time

BuildRequires Tag

Please put every single package requirement in a new line. This will make the spec file readable and would make it easier to review changes to the package dependencies.

Also prefer using "BuildRequires: pkgconfig(foo) >= 42" than devel packages "BuildRequires: foo-devel >= 42" ... same for typelib(bar) ...

Requires Tag

Please put every single package requirement in a new line. This will make the spec file readable and would make it easier to review changes to the package dependencies.

PreReq

Packages should not use the PreReq tag. Once upon a time, in dependency loops PreReq used to "win" over the conventional Requires when RPM determined the installation order in a transaction. This is no longer the case.

BuildRoot Tag

BuildRoot:      %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)

is not longer needed. This is done by RPM automatically.


Patches

Each problem should be solved in a separate patch. To allow easy maintenance of patches, every patch should have a header providing the following information:

  • Authors' names
  • Detailed description of the fixed problem
  • URL of the original source of the patch if any

The name of a patch file consists of:

  • The name and version of the source tarball from which the patched file is derived
  • Some words that characterize the patch content
  • The filename suffix .patch

Patches are in the unified format (diff -u) and should be applied with 1 strip level in the spec file (%patch -p1). The only exceptions are the patches obtained from an another primary source site. The original name, suffix, and format is preserved in this case.

Each patch should be compressed with bzip2 if its size is greater than 100kB. The macros %name and %version should be used whenever possible.

Example:

Source:   %{name}-%{version}.tar.bz2
Patch0:   %{name}-%{version}-autoconf.patch
Patch1:   %{name}-%{version}-gcc31.patch

For the patches to be applied, the patches should be mentioned under %setup. For the above example, this could be done as

%setup -q
%patch0 -p1
%patch1 -p1


Patches have to be marked as such in the spec file and should be applied using the internal patch routines available in rpm. Use of alternate patch management system not supported by rpm is not allowed.


 %build section

Please use

make %{?_smp_mflags} 

instead of

make %{?jobs:-j%jobs}}

 %install section

%install
rm -rf $RPM_BUILD_ROOT
make install DESTDIR=$RPM_BUILD_ROOT
rm $RPM_BUILD_ROOT%{_libdir}/libxxx.la

can be replaced with a single macro:

%make_install

Note: If you want to keep any static libraries, you should not use this macro. This macro will delete all .a and .la files.

%clean section

%clean
rm -rf $RPM_BUILD_ROOT

is no longer needed, this is done by RPM automatically.

%files section

%files
%defattr(-,root,root,-) 
%doc AUTHORS COPYING COPYING.lib LICENSE

%defattr(-,root,root,-) is not needed, it is automatically inserted for all packages.


Shared Libraries

Whenever possible (and feasible), Tizen Packages containing libraries should build them as shared libraries. In addition, every binary RPM package which contains shared library files (not just symlinks) in any of the dynamic linker's default paths, must call ldconfig in %post and %postun. If the package has multiple subpackages with libraries, each subpackage should also have a %post/%postun section that calls /sbin/ldconfig. An example of the correct syntax for this is:

%post -p /sbin/ldconfig

%postun -p /sbin/ldconfig

Note that this specific syntax only works if /sbin/ldconfig is the only call in %post and %postun. If you have additional commands to run during the scriptlet, call /sbin/ldconfig at the beginning of the scriptlet, like this:

%post
/sbin/ldconfig
/usr/bin/foo --add

%postun
/sbin/ldconfig
/usr/bin/foo --remove

Handling Locale Files

If the package includes translations, add

BuildRequires: gettext

If you don't, your package could fail to generate translation files in the buildroot.

Tizen includes an rpm macro called %find_lang. This macro will locate all of the locale files that belong to your package (by name), and put this list in a file. You can then use that file to include all of the locales. %find_lang should be run in the %install section of your spec file, after all of the files have been installed into the buildroot. The correct syntax for %find_lang is usually:

%find_lang %{name}

In some cases, the application may use a different "name" for its locales. You may have to look at the locale files and see what they are named. If they are named myapp.mo, then you will need to pass myapp to %find_lang instead of %{name}.After %find_lang is run, it will generate a file in the active directory (by default, the top level of the source dir). This file will be named based on what you passed as the option to the %find_lang macro. Usually, it will be named %{name}.lang. You should then use this file in the %files list to include the locales detected by %find_lang. To do this, you should include it with the -f parameter to %files.

%files -f %{name}.lang
%defattr(-,root,root,-)
%{_bindir}/foobar
...

If you are already using the -f parameter for the %files section where the locales should live, just append the contents of %{name}.lang to the end of the file that you are already using with -f. (Note that only one file may be used with %files -f.)

Here is an example of proper usage of %find_lang, in foo.spec:

...
%prep
%setup -q

%build
%configure --with-cheese
make %{?_smp_mflags}

%install
make DESTDIR=%{buildroot} install
%find_lang %{name}


%files -f %{name}.lang
%defattr(-,root,root,-)
%doc LICENSE README
%{_bindir}/foobar



Why do we need to use %find_lang?

Using %find_lang helps keep the spec file simple, and helps avoid several other packaging mistakes.

  • Packages that use %{_datadir}/* to grab all the locale files in one line also grab ownership of the locale directories, which is not permitted.
  • Most packages that have locales have lots of locales. Using %find_lang is much easier in the spec file than having to do:
%{_datadir}/locale/ar/LC_MESSAGES/%{name}.mo
%{_datadir}/locale/be/LC_MESSAGES/%{name}.mo
%{_datadir}/locale/cs/LC_MESSAGES/%{name}.mo
%{_datadir}/locale/de/LC_MESSAGES/%{name}.mo
%{_datadir}/locale/es/LC_MESSAGES/%{name}.mo
...
  • As new locale files appear in later package revisions, %find_lang will automatically include them when it is run, preventing you from having to update the spec any more than is necessary.

Keep in mind that usage of %find_lang in packages containing locales is a MUST.

Scriptlets

Great care should be taken when using scriptlets in Tizen packages. If scriptlets are used, those scriptlets must be sane.


Scriptlets requirements

Do not use the Requires(pre,post) style notation for scriptlet dependencies, because of two bugs in RPM. Instead, they should be split like this:

Requires(pre): ...
Requires(post): ...

For more information, see www.redhat.com .

Running scriptlets only in certain situations

When the rpm command executes the scriptlets in a package it indicates if the action preformed is an install, erase, upgrade or reinstall by passing an integer argument to the script in question according to the following:

          install   erase   upgrade  reinstall
%pre         1        -         2         2
%post        1        -         2         2
%preun       -        0         1         -
%postun      -        0         1         -

This means that for example a package that installs an init script with the chkconfig command should uninstall it only on erase and not upgrade with the following snippet:

%preun
if [ $1 -eq 0 ] ; then
/sbin/chkconfig --del %{name}
fi

See also /usr/share/doc/rpm-*/triggers, which gives a more formal, generalized definition about the integer value(s) passed to various scripts.

Scriplets are only allowed to write in certain directories

Build scripts of packages (%prep, %build, %install, %check and %clean) may only alter files (create, modify, delete) under %{buildroot}, %{_builddir} and valid temporary locations like /tmp, /var/tmp (or $TMPDIR or %{_tmppath} as set by the rpmbuild process) according to the following matrix

 /tmp, /var/tmp, $TMPDIR, %{_tmppath} %{_builddir} %{buildroot}
%prepyesyesno
%buildyesyesno
%installyesyesyes
%checkyesyesno
%cleanyesyesyes

Further clarification: That should hold true irrespective of the builder's uid.

Use of Epochs

The Epoch tag in RPM is to be used only as a last resort, and should be avoided whenever possible. However, it is sometimes necessary to use an Epoch to handle upstream versioning changes or to ease transition from third party repositories.

Configurations and Documentation

RPM keeps special track of files within a package that hold documentation or configuration data. You need to identify these files with special directives.The %doc directive marks a file as a documentation file. For example:

%files
/usr/X11R6/bin/xtoolwait
%doc /usr/X11R6/man/man1/xtoolwait.*

This example lists all the included files in /usr/X11R6/man/man1 as documentation files.If you don’t include the full path to a documentation file or files, the RPM system will create a special documentation directory for the package, and place those files into that directory. For example:

%doc README NEWS

This example places the files README and NEWS into a newly created package-specific directory, typically a subdirectory under /usr/share/doc or /usr/doc.The %docdir directive names a directory that holds documentation. All files under that directory in the package will get automatically marked as documentation files. For example:

%files
/usr/X11R6/bin/xtoolwait
%docdir /usr/X11R6/man/man1
/usr/X11R6/man/man1/xtoolwait.*

Note: In addition to the marked directories, the standard Linux documentation directories, such as /usr/share/man, are automatically assumed to be documentation directories.Similar to the %doc directive, the %config directive marks a file as configuration. For example:

%files
/sbin/ypbind
%config /etc/rc.d/init.d/*
%config /etc/yp.conf
%doc README NEWS

A special option to the %config directive, noreplace, tells RPM not to overwrite, or replace a configuration file. For example:

%files
/sbin/ypbind
%config /etc/rc.d/init.d/*
%config(noreplace) /etc/yp.conf
%doc README NEWS

Use this option to help protect local modifications. If you use %config(noreplace), the file will not overwrite an existing file that has been modified. If the file has not been modified on disk, the rpm command will overwrite the file. But, if the file has been modified on disk, the rpm command will copy the new file with an extra file-name extension of .rpmnew.Similarly, %config(missingok) means that the file does not have to exist on disk. You can use this modifier for files or links that are created during the %post scripts but will need to be removed if the package is removed.Another special modifier, %ghost, tells the rpm command that the file should not be included in the package. You can use this to name the needed attributes for a file that the program, when installed, will create. For example, you may want to ensure that a program’s log file has certain attributes.

RPM spec files have a macro, %config, that is used to mark config files so that edits to config files won't get lost during a subsequent upgrade. Without this, the config files from an upgrade would tend to overrite the edited files from the previous version. %config can also apper as %config(noreplace).There are three things that can vary about files in an RPM that is being upgraded: how the files are marked in the spec file (default, %config, or %config(noreplace)), whether the file itself changed between RPM versions, and whether the file on disk has been edited between installing one version of the RPM and the next.

The following table shows what we ended up with after installing an RPM, optionally editing the resulting files, and then upgrading the RPM.

File marked asChanged in update RPM?On-disk file untouchedOn-disk file edited
[default]NoFile from updateFile from update
YesFile from updateFile from update
 %configNoFile from updateEdited file
YesFile from updateFile from update, edited file in .rpmsave
%config(noreplace)NoFile from updateEdited file
YesFile from updateEdited file, file from the update in .rpmnew


For the two cases where (noreplace) has an effect, there is also the question of what happens if the status of the file as defined in the spec file changes. And the answer is:

File marked asChanged in update RPM?On-disk file edited
Was %config(noreplace), becomes %configYesFile from update, edited file in .rpmsave
Was %config, becomes %config(noreplace)YesEdited file, file from the update in .rpmnew

In summary: if a file is not marked as a config file, or if a file has not been altered since installation, then it will be silently replaced by the version from an update RPM. If a config file has been edited on disk, but is not actually different from one RPM to another then the edited version will be silently left in place. It is only when a config file has been edited and is different from one RPM to the next that what happens depends on the(noreplace) option. If absent, the new file will be installed, and the the old edited version will be renamed with a .rpmsave suffix. If present, the edited version will be left in place, and the new version will be installed with a .rpmnew suffix. I don't know what happens if RPM needs to create an .rpmsave or .rpmnew file and one already exists - at least in some cases it seems that the new file isn't written under these circumstances.

This suggests that in general config files should be marked (noreplace), unless the change being implemented is sufficiently major that a config file derived from a previous install is simply not going to work. Even then it seems questionable to me if installing a new 'default' configuration files is better or worse than leaving behind an edited one that may not work.

Comment a macro

you can't just use a '#' at the beguining of the line to comment a rpm macro (comment a rpm raise a rpmlint error).

a good case is %cmake

rpm -E "#%cmake"

so even comment the rpm macro is expended and execute ...

If you want to comment a rpm macro :

%cmake

should become

#%%cmake

Macros

The following macros are widely used in many of the packages:

Documentation Macros

  •  %remove_docs: Remove all documentation files from well known locations where documentation is usually installed
  •  %docs_package: create a -doc sub-package will documentation from standard documentation directories

Locale

  •  %lang_package: package translations into a sub-package (-locale). This package will assume a file with <package name>.lang that has all the locale files. Usually this file is created with the %find_lang macro.

This macro takes 2 arguments for special handing of packages with inconsistent naming:

  • -n <name>: Then the main package is virtual and does not have files, you will need the locale package to match the main package that actually get installed on the system
  • -f <file>: point to a different file name with locale files (by default %name.lang is taken)

Environments

Python Packages

All Python module packages, whether pure Python or C-based, should be called python-modulename. modulename should be the name of this module on the Python Package Index, the official third-party software repository for the Python programming language.

Credits

Some of the guidelines above are based on both opensuse and fedora packaging guidelines.


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值