diff --git a/.changeset/warm-emus-collect.md b/.changeset/warm-emus-collect.md new file mode 100644 index 00000000..7aeaf3d9 --- /dev/null +++ b/.changeset/warm-emus-collect.md @@ -0,0 +1,6 @@ +--- +'@trapezedev/project': minor +'@trapezedev/configure': minor +--- + +Add support for iOS SPM (SwiftPackageManager) packages diff --git a/packages/configure/src/definitions.ts b/packages/configure/src/definitions.ts index 6f0b1f1d..51e1c472 100644 --- a/packages/configure/src/definitions.ts +++ b/packages/configure/src/definitions.ts @@ -1,4 +1,4 @@ -import { AndroidGradleInjectType } from '@trapezedev/project'; +import { AndroidGradleInjectType, IosSPMPackageDefinition } from '@trapezedev/project'; export type OperationMeta = string[]; export interface Operation { @@ -144,3 +144,7 @@ export type IosXCConfigOperationValue = { file?: string; set?: any; } + +export interface IosSpmPackagesOperation { + value: IosSPMPackageDefinition[]; +} diff --git a/packages/configure/src/op.ts b/packages/configure/src/op.ts index 14652cf1..01f2aeea 100644 --- a/packages/configure/src/op.ts +++ b/packages/configure/src/op.ts @@ -213,6 +213,8 @@ function createOpDisplayText(op: Partial) { return (Array.isArray(op.value) ? op.value : op.value.entries).map((v: any) => Object.keys(v)).join(', '); case 'ios.frameworks': return op.value.join(', '); + case 'ios.spmPackages': + return op.value.map((v: any) => `${v.name} (${v.libs.join(', ')})`).join(', '); case 'ios.plist': return `${op.value.entries.length} modifications`; case 'ios.xml': diff --git a/packages/configure/src/operations/ios/spmPackages.ts b/packages/configure/src/operations/ios/spmPackages.ts new file mode 100644 index 00000000..386e7ac3 --- /dev/null +++ b/packages/configure/src/operations/ios/spmPackages.ts @@ -0,0 +1,12 @@ +import { Context } from '../../ctx'; +import { Operation, OperationMeta } from '../../definitions'; + +export default async function execute(ctx: Context, op: Operation) { + for (let spmPackage of op.value) { + ctx.project.ios?.addSPMPackage(op.iosTarget, spmPackage); + } +} + +export const OPS: OperationMeta = [ + 'ios.spmPackages' +] \ No newline at end of file diff --git a/packages/configure/test/ops/ios.spmPackages.test.ts b/packages/configure/test/ops/ios.spmPackages.test.ts new file mode 100644 index 00000000..c6edc7f1 --- /dev/null +++ b/packages/configure/test/ops/ios.spmPackages.test.ts @@ -0,0 +1,48 @@ +import { copy } from '@ionic/utils-fs'; +import { XCConfigFile } from '@trapezedev/project/src'; +import { join } from 'path'; +import tempy from 'tempy'; + +import { Context, loadContext } from '../../src/ctx'; +import { IosSpmPackagesOperation, Operation } from '../../src/definitions'; +import Op from '../../src/operations/ios/spmPackages'; + +describe('op: ios.spmPackages', () => { + let dir: string; + let ctx: Context; + + beforeEach(async () => { + dir = tempy.directory(); + + await copy('../common/test/fixtures/ios-and-android', dir); + + ctx = await loadContext(dir); + ctx.args.quiet = true; + }); + + it('should set ios.spmPackages', async () => { + const op: IosSpmPackagesOperation = { + value: [ + { + name: 'swift-numerics', + libs: ['Numberics'], + repositoryURL: 'https://github.com/apple/swift-numerics.git', + version: '1.0.0' + }, + { + name: 'local-swift-numerics', + libs: ['Numberics'], + path: '../path/to/local-swift-numerics' + }, + ], + }; + + await Op(ctx, op as Operation); + + const pbxProject = ctx.project.ios?.getPbxProject() + const pbxProjectText = pbxProject?.writeSync() + + expect(pbxProjectText).toContain('https://github.com/apple/swift-numerics.git') + expect(pbxProjectText).toContain('../path/to/local-swift-numerics') + }); +}); diff --git a/packages/project/src/definitions.ts b/packages/project/src/definitions.ts index 20a14999..2f56f19c 100644 --- a/packages/project/src/definitions.ts +++ b/packages/project/src/definitions.ts @@ -7,6 +7,8 @@ export interface IosPbxProject { [key: string]: any; } +export type IosPbxArrayValue = { value: string; comment: string }; + export interface IosEntitlements { [key: string]: any; } @@ -48,6 +50,21 @@ export type IosBuildName = 'Debug' | 'Release' | string; export type IosTargetName = string; export type IosProjectName = string; +export interface IosRemoteSPMPackageDefinition { + name: string; + libs: string[]; + repositoryURL: string; + version: string; +} + +export interface IosLocalSPMPackageDefinition { + name: string; + libs: string[]; + path: string; +} + +export type IosSPMPackageDefinition = IosRemoteSPMPackageDefinition | IosLocalSPMPackageDefinition; + /** * Android definitions */ diff --git a/packages/project/src/ios/project.ts b/packages/project/src/ios/project.ts index 68e3280d..d3cfd4c6 100644 --- a/packages/project/src/ios/project.ts +++ b/packages/project/src/ios/project.ts @@ -5,13 +5,14 @@ import { copy, pathExists, readdir, writeFile } from '@ionic/utils-fs'; import { parsePbxProject, pbxReadString, pbxSerializeString } from "../util/pbx"; import { MobileProject } from "../project"; -import { IosPbxProject, IosEntitlements, IosFramework, IosBuildName, IosTarget, IosTargetName, IosTargetBuildConfiguration, IosFrameworkOpts } from '../definitions'; +import { IosPbxProject, IosEntitlements, IosFramework, IosBuildName, IosTarget, IosTargetName, IosTargetBuildConfiguration, IosFrameworkOpts, IosSPMPackageDefinition } from '../definitions'; import { VFSRef, VFSFile } from '../vfs'; import { XmlFile } from '../xml'; import { PlistFile } from '../plist'; import { PlatformProject } from '../platform-project'; import { Logger } from '../logger'; import { assertParentDirs } from '../util/fs'; +import { addSPMPackageToProject } from './spm'; const defaultEntitlementsPlist = ` @@ -362,6 +363,18 @@ export class IosProject extends PlatformProject { return this.pbxProject?.pbxFrameworksBuildPhaseObj(target.id)?.files?.map((f: any) => f.comment.split(' ')[0]); } + /** + * Add a SPM framework for the given target. + * If the `targetName` is null the main app target is used. + */ + addSPMPackage(targetName: IosTargetName | null, packageDef: IosSPMPackageDefinition, opts: IosFrameworkOpts = {}) { + targetName = this.assertTargetName(targetName || null); + const target = this.getTarget(targetName); + if (this.pbxProject) { + addSPMPackageToProject(this.pbxProject, target!.id, packageDef, this.project.projectRoot); + } + } + /** * Get the path to the entitlements file for the given target and build. * If the `targetName` is null the main app target is used. If the `buildName` is null the first diff --git a/packages/project/src/ios/spm.ts b/packages/project/src/ios/spm.ts new file mode 100644 index 00000000..98e63f81 --- /dev/null +++ b/packages/project/src/ios/spm.ts @@ -0,0 +1,238 @@ +import { + IosPbxArrayValue, + IosPbxProject, + IosSPMPackageDefinition, +} from '../definitions'; +import path from 'path'; +import * as semver from 'semver'; + +// requirement = { +// kind = versionRange; +// maximumVersion = 2.0.0; +// minimumVersion = 1.0.0; +// }; +// requirement = { +// kind = exactVersion; +// version = 1.0.0; +// }; +// requirement = { +// kind = upToNextMinorVersion; +// minimumVersion = 1.0.0; +// }; +// requirement = { +// kind = upToNextMajorVersion; +// minimumVersion = 1.0.0; +// }; +// requirement = { +// branch = asd; +// kind = branch; +// }; +// requirement = { +// kind = revision; +// revision = 5f03bfdc8cb6300ef8355695a3d27d11ba19f6a3; +// }; + +export function classifyVersion(version: string) { + if (version.startsWith('#')) { + return { + kind: 'revision', + revision: version.replace('#', ''), + }; + } + + if (semver.valid(version)) { + return { + kind: 'exactVersion', + version, + }; + } + + const range = semver.validRange(version); + if (range) { + const minimumVersion = semver.minVersion(range)?.version; + if (version.startsWith('^')) { + return { + kind: 'upToNextMajorVersion', + minimumVersion, + }; + } else if (version.startsWith('~')) { + return { + kind: 'upToNextMinorVersion', + minimumVersion, + }; + } else { + const maximumVersion = semver.coerce( + version.replace(minimumVersion ?? '', ''), + )?.version; + + if (maximumVersion && maximumVersion !== minimumVersion) { + return { + kind: 'versionRange', + minimumVersion, + maximumVersion, + }; + } + + return { + kind: 'upToNextMajorVersion', + minimumVersion, + }; + } + } + + return { + kind: 'branch', + branch: version, + }; +} + +export function addSPMPackageToProject( + project: IosPbxProject, + targetId: string, + pkg: IosSPMPackageDefinition, + projectRoot: string, +) { + const helper = new SPMHelper(project); + const target = project.pbxNativeTargetSection()[targetId]; + const firstProject = project.getFirstProject().firstProject; + const packageReferences: IosPbxArrayValue[] = (firstProject[ + 'packageReferences' + ] ??= []); + const packageProductReferences: IosPbxArrayValue[] = (target[ + 'packageProductDependencies' + ] ??= []); + const frameworkBuildPhaseObj = project.pbxFrameworksBuildPhaseObj(targetId); + const frameworkBuildPhaseFiles: IosPbxArrayValue[] = (frameworkBuildPhaseObj[ + 'files' + ] ??= []); + + let packageReferenceComment: string; + let packageReferenceSection: string; + let packageReferenceSectionContent: Record; + + if ('path' in pkg) { + // local package + const relativePath = path.relative( + projectRoot, + path.resolve(projectRoot, pkg.path), + ); + packageReferenceComment = `XCLocalSwiftPackageReference "${relativePath}"`; + packageReferenceSection = 'XCLocalSwiftPackageReference'; + packageReferenceSectionContent = { + isa: packageReferenceSection, + relativePath: JSON.stringify(relativePath), + }; + } else { + // remote package + packageReferenceComment = `XCRemoteSwiftPackageReference "${pkg.name}"`; + packageReferenceSection = 'XCRemoteSwiftPackageReference'; + packageReferenceSectionContent = { + isa: packageReferenceSection, + repositoryURL: JSON.stringify(pkg.repositoryURL), + requirement: classifyVersion(pkg.version), + }; + } + + const { uuid: spmPackageReferenceUUID, comment: spmPackageReferenceComment } = + helper.addOrUpdateEntry( + packageReferenceSection, + packageReferenceComment, + packageReferenceSectionContent, + ); + + helper.addOrUpdateArrayEntry(packageReferences, spmPackageReferenceUUID, { + value: spmPackageReferenceUUID, + comment: packageReferenceComment, + }); + + for (const lib of pkg.libs) { + const { uuid: spmProductDependencyUUID } = helper.addOrUpdateEntry( + 'XCSwiftPackageProductDependency', + lib, + { + isa: 'XCSwiftPackageProductDependency', + package: spmPackageReferenceUUID, + package_comment: spmPackageReferenceComment, + productName: lib, + }, + ); + + const libComment = `${lib} in Frameworks`; + + const { uuid: spmBuildFileUuid } = helper.addOrUpdateEntry( + 'PBXBuildFile', + libComment, + { + isa: 'PBXBuildFile', + productRef: spmProductDependencyUUID, + productRef_comment: lib, + }, + ); + + helper.addOrUpdateArrayEntry( + packageProductReferences, + spmProductDependencyUUID, + { + value: spmProductDependencyUUID, + comment: lib, + }, + ); + + helper.addOrUpdateArrayEntry(frameworkBuildPhaseFiles, spmBuildFileUuid, { + value: spmBuildFileUuid, + comment: libComment, + }); + } +} + +class SPMHelper { + constructor(private pbxProject: IosPbxProject) {} + + addOrUpdateArrayEntry( + array: IosPbxArrayValue[], + lookupValue: string, + value: IosPbxArrayValue, + ) { + const existing = array.find(entry => entry.value === lookupValue); + + if (existing) { + Object.assign(existing, value); + return; + } + + array.push(value); + } + + addOrUpdateEntry(section: string, entryComment: string, entry: any) { + const pbxSection = this.getOrCreateSection(section); + const entryUuid = this.getExistingOrGenerateUUID(section, entryComment); + + const entryCommentKey = `${entryUuid}_comment`; + pbxSection[entryCommentKey] = entryComment; + pbxSection[entryUuid] = entry; + + return { + uuid: entryUuid, + comment: entryComment, + }; + } + + getExistingOrGenerateUUID(section: string, comment: string) { + const existingUUID = Object.keys( + this.pbxProject.hash.project.objects[section], + ) + .find(key => { + if (key.endsWith('_comment')) { + return this.pbxProject.hash.project.objects[section][key] === comment; + } + return false; + }) + ?.replace('_comment', ''); + + return existingUUID ?? this.pbxProject.generateUuid(); + } + + getOrCreateSection(section: string) { + return (this.pbxProject.hash.project.objects[section] ??= new Object()); + } +} diff --git a/packages/project/test/project.ios.test.ts b/packages/project/test/project.ios.test.ts index e558c32c..2f1f7695 100644 --- a/packages/project/test/project.ios.test.ts +++ b/packages/project/test/project.ios.test.ts @@ -1,7 +1,7 @@ import tempy from 'tempy'; import { join } from 'path'; import { copy, pathExists, readFile, rm } from '@ionic/utils-fs'; -import { MobileProject, StringsFile, XCConfigFile, XmlFile } from '../src'; +import { IosPbxArrayValue, MobileProject, StringsFile, XCConfigFile, XmlFile } from '../src'; import { MobileProjectConfig } from '../src/config'; import { PlistFile } from '../src/plist'; @@ -163,6 +163,228 @@ describe('project - ios standard', () => { expect(fwks.every(f => (frameworks?.indexOf(f) ?? -1) >= 0)).toBe(true); }); + it('should add remote spm packages', async () => { + const pbx = project.ios?.getPbxProject(); + const sections = pbx!.hash.project.objects; + // Make sure there are no SPM packages to start + expect(sections.XCRemoteSwiftPackageReference).not.toBeDefined(); + expect(sections.XCSwiftPackageProductDependency).not.toBeDefined(); + + const pkgs = [ + { + name: 'swift-numerics', + libs: ['RealModule', 'ComplexModule'], + repositoryURL: 'https://github.com/apple/swift-numerics.git', + version: '1.0.0', + }, + ]; + pkgs.forEach(p => project.ios?.addSPMPackage('App', p)); + + // ensure the frameworks are added + const frameworks = project.ios?.getFrameworks('App'); + expect( + pkgs.every(p => { + return p.libs.every(l => (frameworks?.indexOf(l) ?? -1) >= 0); + }), + ).toBe(true); + + expect( + pkgs.every(p => { + return Object.values(sections.XCRemoteSwiftPackageReference) + .filter(s => typeof s !== 'string') + .some((v: any) => v.repositoryURL.indexOf(p.repositoryURL) >= 0); + }), + ).toBe(true); + + expect( + pkgs.every(p => { + return p.libs.every(l => { + return Object.values(sections.XCSwiftPackageProductDependency) + .filter(s => typeof s !== 'string') + .some((v: any) => v.productName.indexOf(l) >= 0); + }); + }), + ).toBe(true); + + // ensure that the package is added to the project's packageReferences + expect( + pkgs.every(p => { + return pbx! + .getFirstProject() + .firstProject.packageReferences.some( + (v: IosPbxArrayValue) => v.comment.indexOf(p.name) >= 0, + ); + }), + ).toBe(true); + + // ensure that the package is added to the correct target's packageProductDependencies + const targetId = project.ios?.getTarget('App')?.id; + const targetSection = pbx!.pbxNativeTargetSection()[targetId!]; + expect( + pkgs.every(p => { + return p.libs.every(l => { + return targetSection.packageProductDependencies.some( + (v: IosPbxArrayValue) => v.comment.indexOf(l) >= 0, + ); + }); + }), + ).toBe(true); + + // ensure that the package is added to the BuildFiles + expect( + pkgs.every(p => { + return p.libs.every(l => { + return Object.values(sections.PBXBuildFile) + .filter(s => typeof s === 'string') + .some((v: any) => v.indexOf(l) >= 0); + }); + }), + ).toBe(true); + + // Make sure the SPM packages were added + expect(sections.XCRemoteSwiftPackageReference).toBeDefined(); + expect(sections.XCSwiftPackageProductDependency).toBeDefined(); + }); + + it('should add remote spm packages only once', async () => { + const pbx = project.ios?.getPbxProject(); + const sections = pbx!.hash.project.objects; + + // Make sure there are no SPM packages to start + expect(sections.XCRemoteSwiftPackageReference).not.toBeDefined(); + expect(sections.XCSwiftPackageProductDependency).not.toBeDefined(); + + const pkgs = [ + { + name: 'swift-numerics', + libs: ['Numerics'], + repositoryURL: 'https://github.com/apple/swift-numerics.git', + version: '1.0.0', + }, + ]; + pkgs.forEach(p => project.ios?.addSPMPackage('App', p)); + + expect(Object.values(sections.XCRemoteSwiftPackageReference).length).toBe(2); + expect(Object.values(sections.XCSwiftPackageProductDependency).length).toBe(2); + + // add the same package again + pkgs.forEach(p => project.ios?.addSPMPackage('App', p)); + + // ensure that the package isn't added again + expect(Object.values(sections.XCRemoteSwiftPackageReference).length).toBe(2); + expect(Object.values(sections.XCSwiftPackageProductDependency).length).toBe(2); + }); + + it('should add local spm packages', async () => { + const pbx = project.ios?.getPbxProject(); + const sections = pbx!.hash.project.objects; + // Make sure there are no SPM packages to start + expect(sections.XCLocalSwiftPackageReference).not.toBeDefined(); + expect(sections.XCSwiftPackageProductDependency).not.toBeDefined(); + + const pkgs = [ + { + name: 'local-swift-numerics', + libs: ['LocalRealModule', 'LocalComplexModule'], + path: 'path/to/package', + }, + ]; + pkgs.forEach(p => project.ios?.addSPMPackage('App', p)); + + // ensure the frameworks are added + const frameworks = project.ios?.getFrameworks('App'); + expect( + pkgs.every(p => { + return p.libs.every(l => (frameworks?.indexOf(l) ?? -1) >= 0); + }), + ).toBe(true); + + expect( + pkgs.every(p => { + return Object.values(sections.XCLocalSwiftPackageReference) + .filter(s => typeof s !== 'string') + .some((v: any) => v.relativePath.indexOf(p.path) >= 0); + }), + ).toBe(true); + + expect( + pkgs.every(p => { + return p.libs.every(l => { + return Object.values(sections.XCSwiftPackageProductDependency) + .filter(s => typeof s !== 'string') + .some((v: any) => v.productName.indexOf(l) >= 0); + }); + }), + ).toBe(true); + + // ensure that the package is added to the project's packageReferences + expect( + pkgs.every(p => { + return pbx! + .getFirstProject() + .firstProject.packageReferences.some( + (v: IosPbxArrayValue) => v.comment.indexOf(p.path) >= 0, + ); + }), + ).toBe(true); + + // ensure that the package is added to the correct target's packageProductDependencies + const targetId = project.ios?.getTarget('App')?.id; + const targetSection = pbx!.pbxNativeTargetSection()[targetId!]; + expect( + pkgs.every(p => { + return p.libs.every(l => { + return targetSection.packageProductDependencies.some( + (v: IosPbxArrayValue) => v.comment.indexOf(l) >= 0, + ); + }); + }), + ).toBe(true); + + // ensure that the package is added to the BuildFiles + expect( + pkgs.every(p => { + return p.libs.every(l => { + return Object.values(sections.PBXBuildFile) + .filter(s => typeof s === 'string') + .some((v: any) => v.indexOf(l) >= 0); + }); + }), + ).toBe(true); + + // Make sure the SPM packages were added + expect(sections.XCLocalSwiftPackageReference).toBeDefined(); + expect(sections.XCSwiftPackageProductDependency).toBeDefined(); + }); + + it('should add local spm packages only once', async () => { + const pbx = project.ios?.getPbxProject(); + const sections = pbx!.hash.project.objects; + + // Make sure there are no SPM packages to start + expect(sections.XCLocalSwiftPackageReference).not.toBeDefined(); + expect(sections.XCSwiftPackageProductDependency).not.toBeDefined(); + + const pkgs = [ + { + name: 'local-swift-numerics', + libs: ['Numerics'], + path: 'path/to/package' + }, + ]; + pkgs.forEach(p => project.ios?.addSPMPackage('App', p)); + + expect(Object.values(sections.XCLocalSwiftPackageReference).length).toBe(2); + expect(Object.values(sections.XCSwiftPackageProductDependency).length).toBe(2); + + // add the same package again + pkgs.forEach(p => project.ios?.addSPMPackage('App', p)); + + // ensure that the package isn't added again + expect(Object.values(sections.XCLocalSwiftPackageReference).length).toBe(2); + expect(Object.values(sections.XCSwiftPackageProductDependency).length).toBe(2); + }); + it('should add frameworks to non-app targets', async () => { const fwks = ['WebKit.framework', 'QuartzCore.framework']; fwks.forEach(f => project.ios?.addFramework('My App Clip', f)); diff --git a/packages/project/test/util/spm-version-parsing.test.ts b/packages/project/test/util/spm-version-parsing.test.ts new file mode 100644 index 00000000..af6fb8b1 --- /dev/null +++ b/packages/project/test/util/spm-version-parsing.test.ts @@ -0,0 +1,35 @@ +import { classifyVersion } from '../../src/ios/spm'; + +describe.only('SPM versions', () => { + it('should classify SPM versions', () => { + expect(classifyVersion('1.0.0')).toEqual({ + kind: 'exactVersion', + version: '1.0.0', + }); + expect(classifyVersion('^1.0.0')).toEqual({ + kind: 'upToNextMajorVersion', + minimumVersion: '1.0.0', + }); + expect(classifyVersion('~1.0.0')).toEqual({ + kind: 'upToNextMinorVersion', + minimumVersion: '1.0.0', + }); + expect(classifyVersion('>=1.0.0 <2.0.0')).toEqual({ + kind: 'versionRange', + minimumVersion: '1.0.0', + maximumVersion: '2.0.0', + }); + expect(classifyVersion('>=2.0.0')).toEqual({ + kind: 'upToNextMajorVersion', + minimumVersion: '2.0.0', + }); + expect(classifyVersion('#abcdefghijklmnopqrstuvwxyz')).toEqual({ + kind: 'revision', + revision: 'abcdefghijklmnopqrstuvwxyz', + }); + expect(classifyVersion('asd')).toEqual({ + kind: 'branch', + branch: 'asd', + }); + }); +}); diff --git a/packages/website/docs/Operations/ios.md b/packages/website/docs/Operations/ios.md index 49698946..6deff2bc 100644 --- a/packages/website/docs/Operations/ios.md +++ b/packages/website/docs/Operations/ios.md @@ -288,4 +288,26 @@ platforms: - file: App/Config.xcconfig set: "PRODUCT_NAME": "$(NAME)" +``` + +### `spmPackages` + + +*Since: 7.1.0* + +Add iOS SPM (Swift Package Manager) dependencies to a project. + +```yaml +platforms: + ios: + targets: + App: + spmPackages: + - name: "swift-numerics" + libs: [ "Numerics" ] + repositoryURL: "https://github.com/apple/swift-numerics.git" + version: "1.0.0" + - name: "local-swift-numerics" + libs: [ "ComplexModule", "RealModule" ] + path: "../path/to/local-swift-numerics" ``` \ No newline at end of file diff --git a/packages/website/docs/project-api.md b/packages/website/docs/project-api.md index febf857c..025ebe02 100644 --- a/packages/website/docs/project-api.md +++ b/packages/website/docs/project-api.md @@ -148,6 +148,27 @@ project.ios?.addFramework(targetName, 'Custom.framework', { project.ios?.getFrameworks(targetName); ``` +#### SPM Packages + +SPM (Swift Package Manager) packages can be added + +```typescript +// remote SPM packages +await project.ios?.addSPMPackage(targetName, { + name: 'swift-numerics', + libs: ['Numerics'], + repositoryURL: 'https://github.com/apple/swift-numerics.git', + version: '1.0.0' +}) + +// local SPM packages +await project.ios?.addSPMPackage(targetName, { + name: 'local-swift-numerics', + libs: ['ComplexModule', 'RealModule'], + path: '../path/to/local-swift-numerics', +}) +``` + #### Entitlements Entitlements can be managed: